Files
coder/coderd/database/modelmethods_internal_test.go
T
Thomas Kosiewski d0db9ec88f feat: add multi-scope support to API keys (#19917)
# Canonicalize API Key Scopes

This PR introduces canonical API key scopes with a `coder:` namespace prefix to avoid collisions with low-level resource:action names. It:

1. Renames special API key scopes in the database:
   - `all` → `coder:all`
   - `application_connect` → `coder:application_connect`

2. Adds support for a new `scopes` field in the API key creation request, allowing multiple scopes to be specified while maintaining backward compatibility with the singular `scope` field.

3. Updates the API documentation to reflect these changes, including the new endpoint for listing public API key scopes.

4. Ensures backward compatibility by mapping between legacy and canonical scope names in relevant code paths.
2025-09-26 11:56:34 +02:00

107 lines
3.0 KiB
Go

package database
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
func TestAPIKeyScopesExpand(t *testing.T) {
t.Parallel()
t.Run("builtins", func(t *testing.T) {
t.Parallel()
cases := []struct {
name string
scopes APIKeyScopes
want func(t *testing.T, s rbac.Scope)
}{
{
name: "all",
scopes: APIKeyScopes{ApiKeyScopeCoderAll},
want: func(t *testing.T, s rbac.Scope) {
requirePermission(t, s, rbac.ResourceWildcard.Type, policy.Action(policy.WildcardSymbol))
requireAllowAll(t, s)
},
},
{
name: "application_connect",
scopes: APIKeyScopes{ApiKeyScopeCoderApplicationConnect},
want: func(t *testing.T, s rbac.Scope) {
requirePermission(t, s, rbac.ResourceWorkspace.Type, policy.ActionApplicationConnect)
requireAllowAll(t, s)
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
s, err := tc.scopes.Expand()
require.NoError(t, err)
tc.want(t, s)
})
}
})
t.Run("low_level_pairs", func(t *testing.T) {
t.Parallel()
cases := []struct {
name string
scopes APIKeyScopes
res string
act policy.Action
}{
{name: "workspace:read", scopes: APIKeyScopes{ApiKeyScopeWorkspaceRead}, res: rbac.ResourceWorkspace.Type, act: policy.ActionRead},
{name: "template:use", scopes: APIKeyScopes{ApiKeyScopeTemplateUse}, res: rbac.ResourceTemplate.Type, act: policy.ActionUse},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
s, err := tc.scopes.Expand()
require.NoError(t, err)
requirePermission(t, s, tc.res, tc.act)
requireAllowAll(t, s)
})
}
})
t.Run("merge", func(t *testing.T) {
t.Parallel()
scopes := APIKeyScopes{ApiKeyScopeCoderApplicationConnect, ApiKeyScopeCoderAll, ApiKeyScopeWorkspaceRead}
s, err := scopes.Expand()
require.NoError(t, err)
requirePermission(t, s, rbac.ResourceWildcard.Type, policy.Action(policy.WildcardSymbol))
requirePermission(t, s, rbac.ResourceWorkspace.Type, policy.ActionApplicationConnect)
requirePermission(t, s, rbac.ResourceWorkspace.Type, policy.ActionRead)
requireAllowAll(t, s)
})
t.Run("empty_defaults_to_all", func(t *testing.T) {
t.Parallel()
s, err := (APIKeyScopes{}).Expand()
require.NoError(t, err)
requirePermission(t, s, rbac.ResourceWildcard.Type, policy.Action(policy.WildcardSymbol))
requireAllowAll(t, s)
})
}
// Helpers
func requirePermission(t *testing.T, s rbac.Scope, resource string, action policy.Action) {
t.Helper()
for _, p := range s.Site {
if p.ResourceType == resource && p.Action == action {
return
}
}
t.Fatalf("permission not found: %s:%s", resource, action)
}
func requireAllowAll(t *testing.T, s rbac.Scope) {
t.Helper()
require.Len(t, s.AllowIDList, 1)
require.Equal(t, policy.WildcardSymbol, s.AllowIDList[0].ID)
require.Equal(t, policy.WildcardSymbol, s.AllowIDList[0].Type)
}