Files
coder/scripts/apikeyscopesgen/main.go
T
Thomas Kosiewski b60ae0a0c4 refactor: add wildcard scope entries for API key scopes (#20032)
# 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.
2025-10-06 12:08:17 +02:00

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