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.
This commit is contained in:
Thomas Kosiewski
2025-10-06 12:08:17 +02:00
committed by GitHub
parent d17dd5d787
commit b60ae0a0c4
12 changed files with 1551 additions and 141 deletions
+55 -15
View File
@@ -25,8 +25,8 @@ func main() {
}
func generate() ([]byte, error) {
names := rbac.ExternalScopeNames()
slices.Sort(names)
allNames := collectAllScopeNames()
publicNames := rbac.ExternalScopeNames()
var b bytes.Buffer
if _, err := b.WriteString("// Code generated by scripts/apikeyscopesgen. DO NOT EDIT.\n"); err != nil {
@@ -61,13 +61,9 @@ func generate() ([]byte, error) {
if _, err := b.WriteString("\tAPIKeyScopeApplicationConnect APIKeyScope = \"application_connect\"\n"); err != nil {
return nil, err
}
for _, n := range names {
res, act := splitRA(n)
if act == policy.WildcardSymbol {
act = "All"
}
constName := fmt.Sprintf("APIKeyScope%s%s", pascal(res), pascal(act))
if _, err := fmt.Fprintf(&b, "\t%s APIKeyScope = \"%s\"\n", constName, n); err != nil {
for _, name := range allNames {
constName := constNameForScope(name)
if _, err := fmt.Fprintf(&b, "\t%s APIKeyScope = \"%s\"\n", constName, name); err != nil {
return nil, err
}
}
@@ -82,12 +78,8 @@ func generate() ([]byte, error) {
if _, err := b.WriteString("var PublicAPIKeyScopes = []APIKeyScope{\n"); err != nil {
return nil, err
}
for _, n := range names {
res, act := splitRA(n)
if act == policy.WildcardSymbol {
act = "All"
}
constName := fmt.Sprintf("APIKeyScope%s%s", pascal(res), pascal(act))
for _, name := range publicNames {
constName := constNameForScope(name)
if _, err := fmt.Fprintf(&b, "\t%s,\n", constName); err != nil {
return nil, err
}
@@ -99,6 +91,54 @@ func generate() ([]byte, error) {
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 {
+19 -5
View File
@@ -58,23 +58,37 @@ func main() {
os.Exit(1)
}
// expectedFromRBAC returns the set of <resource>:<action> pairs derived from RBACPermissions.
// expectedFromRBAC returns the set of scope names the DB enum must support.
func expectedFromRBAC() map[string]struct{} {
want := make(map[string]struct{})
// Low-level <resource>:<action>
add := func(name string) {
if name == "" {
return
}
want[name] = struct{}{}
}
// Low-level <resource>:<action> and synthesized <resource>:* wildcards
for resource, def := range policy.RBACPermissions {
if resource == policy.WildcardSymbol {
// Ignore wildcard entry; it has no concrete <resource>:<action> pairs.
continue
}
add(resource + ":" + policy.WildcardSymbol)
for action := range def.Actions {
key := resource + ":" + string(action)
want[key] = struct{}{}
add(resource + ":" + string(action))
}
}
// Composite coder:* names
for _, n := range rbac.CompositeScopeNames() {
want[n] = struct{}{}
add(n)
}
// Built-in coder-prefixed scopes such as coder:all
for _, n := range rbac.BuiltinScopeNames() {
s := string(n)
if !strings.Contains(s, ":") {
continue
}
add(s)
}
return want
}
@@ -1,32 +0,0 @@
package main
import (
"fmt"
"sort"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
func main() {
seen := map[string]struct{}{}
var vals []string
for resource, def := range policy.RBACPermissions {
if resource == policy.WildcardSymbol {
continue
}
for action := range def.Actions {
vals = append(vals, fmt.Sprintf("%s:%s", resource, action))
}
}
// Include composite coder:* scopes as first-class enum values
vals = append(vals, rbac.CompositeScopeNames()...)
sort.Strings(vals)
for _, v := range vals {
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
_, _ = fmt.Printf("ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS '%s';\n", v)
}
}