mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
241 lines
7.0 KiB
Go
241 lines
7.0 KiB
Go
package database
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"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.expandRBACScope()
|
|
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.expandRBACScope()
|
|
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.expandRBACScope()
|
|
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("effective_scope_keep_types", func(t *testing.T) {
|
|
t.Parallel()
|
|
workspaceID := uuid.New()
|
|
|
|
effective := APIKeyScopeSet{
|
|
Scopes: APIKeyScopes{ApiKeyScopeWorkspaceRead},
|
|
AllowList: AllowList{
|
|
{Type: rbac.ResourceWorkspace.Type, ID: workspaceID.String()},
|
|
},
|
|
}
|
|
|
|
expanded, err := effective.Expand()
|
|
require.NoError(t, err)
|
|
require.Len(t, expanded.AllowIDList, 1)
|
|
require.Equal(t, "workspace", expanded.AllowIDList[0].Type)
|
|
require.Equal(t, workspaceID.String(), expanded.AllowIDList[0].ID)
|
|
})
|
|
|
|
t.Run("empty_rejected", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := (APIKeyScopes{}).expandRBACScope()
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "no scopes provided")
|
|
})
|
|
|
|
t.Run("allow_list_overrides", func(t *testing.T) {
|
|
t.Parallel()
|
|
allowID := uuid.NewString()
|
|
set := APIKeyScopes{ApiKeyScopeWorkspaceRead}.WithAllowList(AllowList{
|
|
{Type: rbac.ResourceWorkspace.Type, ID: allowID},
|
|
})
|
|
s, err := set.Expand()
|
|
require.NoError(t, err)
|
|
require.Len(t, s.AllowIDList, 1)
|
|
require.Equal(t, rbac.AllowListElement{Type: rbac.ResourceWorkspace.Type, ID: allowID}, s.AllowIDList[0])
|
|
})
|
|
|
|
t.Run("allow_list_wildcard_keeps_merged", func(t *testing.T) {
|
|
t.Parallel()
|
|
set := APIKeyScopes{ApiKeyScopeWorkspaceRead}.WithAllowList(AllowList{
|
|
{Type: policy.WildcardSymbol, ID: policy.WildcardSymbol},
|
|
})
|
|
s, err := set.Expand()
|
|
require.NoError(t, err)
|
|
requirePermission(t, s, rbac.ResourceWorkspace.Type, policy.ActionRead)
|
|
requireAllowAll(t, s)
|
|
})
|
|
|
|
t.Run("scope_set_helper", func(t *testing.T) {
|
|
t.Parallel()
|
|
allowID := uuid.NewString()
|
|
key := APIKey{
|
|
Scopes: APIKeyScopes{ApiKeyScopeWorkspaceRead},
|
|
AllowList: AllowList{
|
|
{Type: rbac.ResourceWorkspace.Type, ID: allowID},
|
|
},
|
|
}
|
|
s, err := key.ScopeSet().Expand()
|
|
require.NoError(t, err)
|
|
require.Len(t, s.AllowIDList, 1)
|
|
require.Equal(t, rbac.AllowListElement{Type: rbac.ResourceWorkspace.Type, ID: allowID}, s.AllowIDList[0])
|
|
})
|
|
}
|
|
|
|
//nolint:tparallel,paralleltest
|
|
func TestChatACLDisabled(t *testing.T) {
|
|
uid := uuid.NewString()
|
|
gid := uuid.NewString()
|
|
|
|
chat := Chat{
|
|
ID: uuid.New(),
|
|
OrganizationID: uuid.New(),
|
|
OwnerID: uuid.New(),
|
|
UserACL: ChatACL{
|
|
uid: ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
|
},
|
|
GroupACL: ChatACL{
|
|
gid: ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
|
},
|
|
}
|
|
|
|
t.Run("ACLsOmittedWhenDisabled", func(t *testing.T) {
|
|
rbac.SetChatACLDisabled(true)
|
|
t.Cleanup(func() { rbac.SetChatACLDisabled(false) })
|
|
|
|
obj := chat.RBACObject()
|
|
|
|
require.Empty(t, obj.ACLUserList, "user ACLs should be empty when disabled")
|
|
require.Empty(t, obj.ACLGroupList, "group ACLs should be empty when disabled")
|
|
})
|
|
|
|
t.Run("ACLsIncludedWhenEnabled", func(t *testing.T) {
|
|
rbac.SetChatACLDisabled(false)
|
|
|
|
obj := chat.RBACObject()
|
|
|
|
require.NotEmpty(t, obj.ACLUserList, "user ACLs should be present when enabled")
|
|
require.NotEmpty(t, obj.ACLGroupList, "group ACLs should be present when enabled")
|
|
require.Contains(t, obj.ACLUserList, uid)
|
|
require.Contains(t, obj.ACLGroupList, gid)
|
|
})
|
|
}
|
|
|
|
//nolint:tparallel,paralleltest
|
|
func TestWorkspaceACLDisabled(t *testing.T) {
|
|
uid := uuid.NewString()
|
|
gid := uuid.NewString()
|
|
|
|
ws := WorkspaceTable{
|
|
ID: uuid.New(),
|
|
OrganizationID: uuid.New(),
|
|
OwnerID: uuid.New(),
|
|
UserACL: WorkspaceACL{
|
|
uid: WorkspaceACLEntry{Permissions: []policy.Action{policy.ActionSSH}},
|
|
},
|
|
GroupACL: WorkspaceACL{
|
|
gid: WorkspaceACLEntry{Permissions: []policy.Action{policy.ActionSSH}},
|
|
},
|
|
}
|
|
|
|
t.Run("ACLsOmittedWhenDisabled", func(t *testing.T) {
|
|
rbac.SetWorkspaceACLDisabled(true)
|
|
t.Cleanup(func() { rbac.SetWorkspaceACLDisabled(false) })
|
|
|
|
obj := ws.RBACObject()
|
|
|
|
require.Empty(t, obj.ACLUserList, "user ACLs should be empty when disabled")
|
|
require.Empty(t, obj.ACLGroupList, "group ACLs should be empty when disabled")
|
|
})
|
|
|
|
t.Run("ACLsIncludedWhenEnabled", func(t *testing.T) {
|
|
rbac.SetWorkspaceACLDisabled(false)
|
|
|
|
obj := ws.RBACObject()
|
|
|
|
require.NotEmpty(t, obj.ACLUserList, "user ACLs should be present when enabled")
|
|
require.NotEmpty(t, obj.ACLGroupList, "group ACLs should be present when enabled")
|
|
require.Contains(t, obj.ACLUserList, uid)
|
|
require.Contains(t, obj.ACLGroupList, gid)
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|