feat: implement composite API key scopes for workspaces and templates (#19945)

# 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.
This commit is contained in:
Thomas Kosiewski
2025-09-29 13:17:08 +02:00
committed by GitHub
parent 860bcd4d91
commit 79126ab6c7
18 changed files with 267 additions and 7 deletions
+1
View File
@@ -112,6 +112,7 @@ func pascal(s string) string {
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:]
+6
View File
@@ -11,6 +11,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
@@ -60,6 +61,7 @@ func main() {
// expectedFromRBAC returns the set of <resource>:<action> pairs derived from RBACPermissions.
func expectedFromRBAC() map[string]struct{} {
want := make(map[string]struct{})
// Low-level <resource>:<action>
for resource, def := range policy.RBACPermissions {
if resource == policy.WildcardSymbol {
// Ignore wildcard entry; it has no concrete <resource>:<action> pairs.
@@ -70,6 +72,10 @@ func expectedFromRBAC() map[string]struct{} {
want[key] = struct{}{}
}
}
// Composite coder:* names
for _, n := range rbac.CompositeScopeNames() {
want[n] = struct{}{}
}
return want
}
@@ -4,6 +4,7 @@ import (
"fmt"
"sort"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
@@ -18,6 +19,8 @@ func main() {
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 {