Files
coder/coderd/rbac/scopes_catalog_internal_test.go
T
Thomas Kosiewski 79126ab6c7 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.
2025-09-29 13:17:08 +02:00

68 lines
2.0 KiB
Go

package rbac
import (
"sort"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestExternalScopeNames(t *testing.T) {
t.Parallel()
names := ExternalScopeNames()
require.NotEmpty(t, names)
// Ensure sorted ascending
sorted := append([]string(nil), names...)
sort.Strings(sorted)
require.Equal(t, sorted, names)
// Ensure each entry expands to site-only
for _, name := range names {
// Skip `all` and `application_connect` since they do not
// expand into a low level scope.
// They are handled differently.
if name == string(ScopeAll) || name == string(ScopeApplicationConnect) {
continue
}
// Composite coder:* scopes expand to one or more site permissions.
if strings.HasPrefix(name, "coder:") {
s, err := ScopeName(name).Expand()
require.NoErrorf(t, err, "catalog entry should expand: %s", name)
require.NotEmpty(t, s.Site)
expected, ok := CompositeSitePermissions(ScopeName(name))
require.Truef(t, ok, "expected composite scope definition: %s", name)
require.ElementsMatchf(t, expected, s.Site, "unexpected expanded permissions for %s", name)
require.Empty(t, s.Org)
require.Empty(t, s.User)
continue
}
// Low-level scopes must parse to a single permission.
res, act, ok := parseLowLevelScope(ScopeName(name))
require.Truef(t, ok, "catalog entry should parse: %s", name)
s, err := ScopeName(name).Expand()
require.NoErrorf(t, err, "catalog entry should expand: %s", name)
require.Len(t, s.Site, 1)
require.Equal(t, res, s.Site[0].ResourceType)
require.Equal(t, act, s.Site[0].Action)
require.Empty(t, s.Org)
require.Empty(t, s.User)
}
}
func TestIsExternalScope(t *testing.T) {
t.Parallel()
require.True(t, IsExternalScope("workspace:read"))
require.True(t, IsExternalScope("template:use"))
require.True(t, IsExternalScope("workspace:*"))
require.True(t, IsExternalScope("coder:workspaces.create"))
require.False(t, IsExternalScope("debug_info:read")) // internal-only
require.False(t, IsExternalScope("unknown:read"))
}