mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: implement API key scopes database migration (#19861)
Added database migration for API key scopes. Fixes #19845
This commit is contained in:
+60
-4
@@ -2,6 +2,7 @@ package rbac
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
@@ -143,18 +144,73 @@ func AllowListAll() AllowListElement {
|
||||
return AllowListElement{ID: policy.WildcardSymbol, Type: policy.WildcardSymbol}
|
||||
}
|
||||
|
||||
// String encodes the allow list element into the canonical database representation
|
||||
// "type:id". This avoids fragile manual concatenations scattered across the codebase.
|
||||
func (e AllowListElement) String() string {
|
||||
return e.Type + ":" + e.ID
|
||||
}
|
||||
|
||||
func (s Scope) Expand() (Scope, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s Scope) Name() RoleIdentifier {
|
||||
return s.Role.Identifier
|
||||
return s.Identifier
|
||||
}
|
||||
|
||||
func ExpandScope(scope ScopeName) (Scope, error) {
|
||||
role, ok := builtinScopes[scope]
|
||||
if role, ok := builtinScopes[scope]; ok {
|
||||
return role, nil
|
||||
}
|
||||
if res, act, ok := parseLowLevelScope(scope); ok {
|
||||
return expandLowLevel(res, act), nil
|
||||
}
|
||||
return Scope{}, xerrors.Errorf("no scope named %q", scope)
|
||||
}
|
||||
|
||||
// ParseResourceAction parses a scope string formatted as "<resource>:<action>"
|
||||
// and returns the resource and action components. This is the common parsing
|
||||
// logic shared between RBAC and database validation.
|
||||
func ParseResourceAction(scope string) (resource string, action string, ok bool) {
|
||||
parts := strings.SplitN(scope, ":", 2)
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return "", "", false
|
||||
}
|
||||
return parts[0], parts[1], true
|
||||
}
|
||||
|
||||
// parseLowLevelScope parses a low-level scope name formatted as
|
||||
// "<resource>:<action>" and validates it against RBACPermissions.
|
||||
// Returns the resource and action if valid.
|
||||
func parseLowLevelScope(name ScopeName) (resource string, action policy.Action, ok bool) {
|
||||
res, act, ok := ParseResourceAction(string(name))
|
||||
if !ok {
|
||||
return Scope{}, xerrors.Errorf("no scope named %q", scope)
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
def, exists := policy.RBACPermissions[res]
|
||||
if !exists {
|
||||
return "", "", false
|
||||
}
|
||||
if _, exists := def.Actions[policy.Action(act)]; !exists {
|
||||
return "", "", false
|
||||
}
|
||||
return res, policy.Action(act), true
|
||||
}
|
||||
|
||||
// expandLowLevel constructs a site-only Scope with a single permission for the
|
||||
// given resource and action. This mirrors how builtin scopes are represented
|
||||
// but is restricted to site-level only.
|
||||
func expandLowLevel(resource string, action policy.Action) Scope {
|
||||
return Scope{
|
||||
Role: Role{
|
||||
Identifier: RoleIdentifier{Name: fmt.Sprintf("Scope_%s:%s", resource, action)},
|
||||
DisplayName: fmt.Sprintf("%s:%s", resource, action),
|
||||
Site: []Permission{{ResourceType: resource, Action: action}},
|
||||
Org: map[string][]Permission{},
|
||||
User: []Permission{},
|
||||
},
|
||||
// Low-level scopes intentionally return an empty allow list.
|
||||
AllowIDList: []AllowListElement{},
|
||||
}
|
||||
return role, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package rbac_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
)
|
||||
|
||||
func TestExpandScope(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("low_level_pairs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
resource string
|
||||
action policy.Action
|
||||
}{
|
||||
{name: "workspace:start", resource: rbac.ResourceWorkspace.Type, action: policy.ActionWorkspaceStart},
|
||||
{name: "workspace:ssh", resource: rbac.ResourceWorkspace.Type, action: policy.ActionSSH},
|
||||
{name: "template:use", resource: rbac.ResourceTemplate.Type, action: policy.ActionUse},
|
||||
{name: "api_key:read", resource: rbac.ResourceApiKey.Type, action: policy.ActionRead},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := rbac.ScopeName(tc.name).Expand()
|
||||
require.NoError(t, err)
|
||||
|
||||
// site-only single permission
|
||||
require.Len(t, s.Site, 1)
|
||||
require.Equal(t, tc.resource, s.Site[0].ResourceType)
|
||||
require.Equal(t, tc.action, s.Site[0].Action)
|
||||
require.Empty(t, s.Org)
|
||||
require.Empty(t, s.User)
|
||||
|
||||
require.Len(t, s.AllowIDList, 0)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid_low_level", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
invalid := []string{
|
||||
"", // empty
|
||||
"workspace:", // missing action
|
||||
":read", // missing resource
|
||||
"unknown:read", // unknown resource
|
||||
"workspace:bogus", // unknown action
|
||||
"a:b:c", // too many parts
|
||||
}
|
||||
for _, name := range invalid {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := rbac.ScopeName(name).Expand()
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user