mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
b60ae0a0c4
# Add API Key Scope Wildcards This PR adds wildcard API key scopes (`resource:*`) for all RBAC resources to ensure every resource has a matching wildcard value. It also adds all individual `resource:action` scopes to the API documentation and TypeScript definitions. The changes include: - Adding a new database migration (000377) that adds wildcard API key scopes - Updating the API documentation to include all available scopes - Enhancing the scope generation scripts to include all resource wildcards - Updating the TypeScript definitions to match the expanded scope list These changes make creating API keys with comprehensive permissions for specific resource types easier.
162 lines
4.2 KiB
Go
162 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/format"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/coder/coder/v2/coderd/rbac"
|
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
|
)
|
|
|
|
func main() {
|
|
out, err := generate()
|
|
if err != nil {
|
|
_, _ = fmt.Fprintf(os.Stderr, "generate apikey scopes: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if _, err := fmt.Print(string(out)); err != nil {
|
|
_, _ = fmt.Fprintf(os.Stderr, "write output: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func generate() ([]byte, error) {
|
|
allNames := collectAllScopeNames()
|
|
publicNames := rbac.ExternalScopeNames()
|
|
|
|
var b bytes.Buffer
|
|
if _, err := b.WriteString("// Code generated by scripts/apikeyscopesgen. DO NOT EDIT.\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := b.WriteString("package codersdk\n\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
// NOTE: Keep all APIKeyScope constants in a single generated file.
|
|
// Some tooling (e.g. swaggo) can behave non-deterministically when
|
|
// enums are spread across multiple files:
|
|
// https://github.com/swaggo/swag/issues/2038
|
|
// We generate everything into codersdk/apikey_scopes_gen.go as the
|
|
// single source of truth so doc generation remains stable.
|
|
|
|
// Constants
|
|
if _, err := b.WriteString("const (\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
// Always include legacy/deprecated aliases for backward compatibility.
|
|
// These are kept in generated code to ensure consistent availability
|
|
// across releases even if hand-written files change.
|
|
if _, err := b.WriteString("\t// Deprecated: use codersdk.APIKeyScopeCoderAll instead.\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := b.WriteString("\tAPIKeyScopeAll APIKeyScope = \"all\"\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := b.WriteString("\t// Deprecated: use codersdk.APIKeyScopeCoderApplicationConnect instead.\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := b.WriteString("\tAPIKeyScopeApplicationConnect APIKeyScope = \"application_connect\"\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, name := range allNames {
|
|
constName := constNameForScope(name)
|
|
if _, err := fmt.Fprintf(&b, "\t%s APIKeyScope = \"%s\"\n", constName, name); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if _, err := b.WriteString(")\n\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Slices
|
|
if _, err := b.WriteString("// PublicAPIKeyScopes lists all public low-level API key scopes.\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := b.WriteString("var PublicAPIKeyScopes = []APIKeyScope{\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, name := range publicNames {
|
|
constName := constNameForScope(name)
|
|
if _, err := fmt.Fprintf(&b, "\t%s,\n", constName); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if _, err := b.WriteString("}\n\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return format.Source(b.Bytes())
|
|
}
|
|
|
|
func collectAllScopeNames() []string {
|
|
seen := make(map[string]struct{})
|
|
var names []string
|
|
add := func(name string) {
|
|
if name == "" {
|
|
return
|
|
}
|
|
if _, ok := seen[name]; ok {
|
|
return
|
|
}
|
|
seen[name] = struct{}{}
|
|
names = append(names, name)
|
|
}
|
|
|
|
for resource, def := range policy.RBACPermissions {
|
|
if resource == policy.WildcardSymbol {
|
|
continue
|
|
}
|
|
add(resource + ":" + policy.WildcardSymbol)
|
|
for action := range def.Actions {
|
|
add(resource + ":" + string(action))
|
|
}
|
|
}
|
|
|
|
for _, name := range rbac.CompositeScopeNames() {
|
|
add(name)
|
|
}
|
|
|
|
for _, name := range rbac.BuiltinScopeNames() {
|
|
s := string(name)
|
|
if !strings.Contains(s, ":") {
|
|
continue
|
|
}
|
|
add(s)
|
|
}
|
|
|
|
slices.Sort(names)
|
|
return names
|
|
}
|
|
|
|
func constNameForScope(name string) string {
|
|
resource, action := splitRA(name)
|
|
if action == policy.WildcardSymbol {
|
|
action = "All"
|
|
}
|
|
return fmt.Sprintf("APIKeyScope%s%s", pascal(resource), pascal(action))
|
|
}
|
|
|
|
func splitRA(name string) (resource string, action string) {
|
|
parts := strings.SplitN(name, ":", 2)
|
|
if len(parts) != 2 {
|
|
return name, ""
|
|
}
|
|
return parts[0], parts[1]
|
|
}
|
|
|
|
func pascal(s string) string {
|
|
// Replace non-identifier separators with spaces, then Title-case and strip.
|
|
s = strings.ReplaceAll(s, "_", " ")
|
|
s = strings.ReplaceAll(s, "-", " ")
|
|
s = strings.ReplaceAll(s, ":", " ")
|
|
s = strings.ReplaceAll(s, ".", " ")
|
|
words := strings.Fields(s)
|
|
for i := range words {
|
|
words[i] = strings.ToUpper(words[i][:1]) + words[i][1:]
|
|
}
|
|
return strings.Join(words, "")
|
|
}
|