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, "") }