mirror of
https://github.com/coder/coder.git
synced 2026-11-07 21:29:37 +08:00
# Add Composite API Key Scopes This PR adds high-level composite API key scopes to simplify token creation with common permission sets: - `coder:workspaces.create` - Create and update workspaces - `coder:workspaces.operate` - Read and update workspaces - `coder:workspaces.delete` - Read and delete workspaces - `coder:workspaces.access` - Read, SSH, and connect to workspace applications - `coder:templates.build` - Read templates and create/read files - `coder:templates.author` - Full template management with insights - `coder:apikeys.manage_self` - Manage your own API keys These composite scopes are persisted in the database and expanded during authorization, providing a more intuitive way to grant permissions compared to the granular resource:action scopes.
122 lines
3.4 KiB
Go
122 lines
3.4 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) {
|
|
names := rbac.ExternalScopeNames()
|
|
slices.Sort(names)
|
|
|
|
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 _, 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 {
|
|
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 _, 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,\n", constName); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if _, err := b.WriteString("}\n\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return format.Source(b.Bytes())
|
|
}
|
|
|
|
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, "")
|
|
}
|