Files
coder/coderd/rbac/roles_test.go
T
Ethan cbe24a3da5 feat: add chat model config RBAC resource (#27957)
Add the `chat_model_config` RBAC resource, actions, and API key scopes.
This layer includes the scope migration and generated API types. Model
list queries apply token scopes, SQL authorization filters, and row
ACLs.

Write and default-management handlers keep deployment configuration
authorization until the next API layer replaces it.

Depends on #27956

_This pull request description was generated by Coder Agents._
2026-08-24 09:43:45 +00:00

1827 lines
69 KiB
Go

package rbac_test
import (
"context"
"fmt"
"slices"
"testing"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
type hasAuthSubjects interface {
Subjects() []authSubject
}
type authSubjectSet []authSubject
func (a authSubjectSet) Subjects() []authSubject { return a }
type authSubject struct {
// Name is helpful for test assertions
Name string
Actor rbac.Subject
}
func (a authSubject) Subjects() []authSubject { return []authSubject{a} }
// TestBuiltInRoles makes sure our built-in roles are valid by our own policy
// rules. If this is incorrect, that is a mistake.
func TestBuiltInRoles(t *testing.T) {
t.Parallel()
for _, r := range rbac.SiteBuiltInRoles() {
t.Run(r.Identifier.String(), func(t *testing.T) {
t.Parallel()
require.NoError(t, r.Valid(), "invalid role")
})
}
for _, r := range rbac.OrganizationRoles(uuid.New()) {
t.Run(r.Identifier.String(), func(t *testing.T) {
t.Parallel()
require.NoError(t, r.Valid(), "invalid role")
})
}
}
// permissionGranted checks whether a permission list contains a
// matching entry for the target, accounting for wildcard actions.
// It does not evaluate negations that may override a positive grant.
func permissionGranted(perms []rbac.Permission, target rbac.Permission) bool {
return slices.ContainsFunc(perms, func(p rbac.Permission) bool {
return p.Negate == target.Negate &&
p.ResourceType == target.ResourceType &&
(p.Action == target.Action || p.Action == policy.WildcardSymbol)
})
}
func TestOrgSharingPermissions(t *testing.T) {
t.Parallel()
tests := []struct {
name string
permsFunc func(rbac.OrgSettings) rbac.OrgRolePermissions
mode rbac.ShareableWorkspaceOwners
orgReadMembers bool
orgReadGroups bool
orgNegateShare bool
memberNegateShare bool
}{
{"Member/Everyone", rbac.OrgMemberPermissions, rbac.ShareableWorkspaceOwnersEveryone, true, true, false, false},
{"Member/None", rbac.OrgMemberPermissions, rbac.ShareableWorkspaceOwnersNone, false, false, true, true},
{"Member/ServiceAccounts", rbac.OrgMemberPermissions, rbac.ShareableWorkspaceOwnersServiceAccounts, true, false, false, true},
{"ServiceAccount/Everyone", rbac.OrgServiceAccountPermissions, rbac.ShareableWorkspaceOwnersEveryone, true, true, false, false},
{"ServiceAccount/None", rbac.OrgServiceAccountPermissions, rbac.ShareableWorkspaceOwnersNone, false, false, true, false},
{"ServiceAccount/ServiceAccounts", rbac.OrgServiceAccountPermissions, rbac.ShareableWorkspaceOwnersServiceAccounts, true, true, false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
perms := tt.permsFunc(rbac.OrgSettings{
ShareableWorkspaceOwners: tt.mode,
})
assert.Equal(t, tt.orgReadMembers, permissionGranted(perms.Org, rbac.Permission{
ResourceType: rbac.ResourceOrganizationMember.Type,
Action: policy.ActionRead,
}), "org read members")
assert.Equal(t, tt.orgReadGroups, permissionGranted(perms.Org, rbac.Permission{
ResourceType: rbac.ResourceGroup.Type,
Action: policy.ActionRead,
}), "org read groups")
assert.Equal(t, tt.orgNegateShare, permissionGranted(perms.Org, rbac.Permission{
Negate: true,
ResourceType: rbac.ResourceWorkspace.Type,
Action: policy.ActionShare,
}), "org negate share")
assert.Equal(t, tt.memberNegateShare, permissionGranted(perms.Member, rbac.Permission{
Negate: true,
ResourceType: rbac.ResourceWorkspace.Type,
Action: policy.ActionShare,
}), "member negate share")
})
}
}
//nolint:tparallel,paralleltest
func TestChatSharingPermissions(t *testing.T) {
target := rbac.Permission{
Negate: true,
ResourceType: rbac.ResourceChat.Type,
Action: policy.ActionShare,
}
orgID := uuid.New()
userID := uuid.NewString()
resource := rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(userID)
authorizeAgentsAccessUser := func(t *testing.T) error {
t.Helper()
memberRole, err := rbac.RoleByName(rbac.RoleMember())
require.NoError(t, err)
agentsRole, err := rbac.RoleByName(rbac.ScopedRoleAgentsAccess(orgID))
require.NoError(t, err)
auth := rbac.NewStrictAuthorizer(prometheus.NewRegistry())
return auth.Authorize(context.Background(), rbac.Subject{
ID: userID,
Roles: rbac.Roles{memberRole, agentsRole},
Scope: rbac.ScopeAll,
}, policy.ActionShare, resource)
}
t.Run("Default", func(t *testing.T) {
rbac.ReloadBuiltinRoles(nil)
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
memberRole, err := rbac.RoleByName(rbac.RoleMember())
require.NoError(t, err)
assert.False(t, permissionGranted(memberRole.Site, target))
require.NoError(t, authorizeAgentsAccessUser(t))
})
t.Run("Disabled", func(t *testing.T) {
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
NoChatSharing: true,
})
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
memberRole, err := rbac.RoleByName(rbac.RoleMember())
require.NoError(t, err)
assert.True(t, permissionGranted(memberRole.Site, target))
err = authorizeAgentsAccessUser(t)
require.ErrorAs(t, err, &rbac.UnauthorizedError{})
})
}
//nolint:tparallel,paralleltest
func TestOwnerExec(t *testing.T) {
owner := rbac.Subject{
ID: uuid.NewString(),
Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleOwner()},
Scope: rbac.ScopeAll,
}
t.Run("NoExec", func(t *testing.T) {
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
NoOwnerWorkspaceExec: true,
})
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
// Exec a random workspace
err := auth.Authorize(context.Background(), owner, policy.ActionSSH,
rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString()))
require.ErrorAsf(t, err, &rbac.UnauthorizedError{}, "expected unauthorized error")
})
t.Run("Exec", func(t *testing.T) {
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
NoOwnerWorkspaceExec: false,
})
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
// Exec a random workspace
err := auth.Authorize(context.Background(), owner, policy.ActionSSH,
rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString()))
require.NoError(t, err, "expected owner can")
})
}
// TestMemberRolesExcludeWorkspacePerms verifies that organization-member
// and organization-service-account grant no workspace permissions, and
// that the registered organization-workspace-access role is what carries
// them.
//
// Reads the global builtin role registry via RoleByName, which sibling
// tests reload, so it must run serially.
//
//nolint:paralleltest
func TestMemberRolesExcludeWorkspacePerms(t *testing.T) {
orgSettings := rbac.OrgSettings{
ShareableWorkspaceOwners: rbac.ShareableWorkspaceOwnersEveryone,
}
hasResource := func(perms []rbac.Permission, resource string) bool {
return slices.ContainsFunc(perms, func(p rbac.Permission) bool {
return p.ResourceType == resource && !p.Negate
})
}
member := rbac.OrgMemberPermissions(orgSettings).Member
require.False(t, hasResource(member, rbac.ResourceWorkspace.Type), "organization-member must not grant workspace permissions")
require.True(t, hasResource(member, rbac.ResourceOrganizationMember.Type), "organization-member should grant read-self")
sa := rbac.OrgServiceAccountPermissions(orgSettings).Member
require.False(t, hasResource(sa, rbac.ResourceWorkspace.Type), "organization-service-account must not grant workspace permissions")
require.True(t, hasResource(sa, rbac.ResourceOrganizationMember.Type), "organization-service-account should grant read-self")
// The registered organization-workspace-access role is the grant
// path for workspace permissions.
orgID := uuid.New()
wsAccess, err := rbac.RoleByName(rbac.ScopedRoleOrgWorkspaceAccess(orgID))
require.NoError(t, err)
require.True(t, hasResource(wsAccess.ByOrgID[orgID.String()].Member, rbac.ResourceWorkspace.Type),
"organization-workspace-access should grant workspace permissions")
}
// These were "pared down" in https://github.com/coder/coder/pull/21359 to avoid
// using the now DB-backed organization-member role. As a result, they no longer
// model real-world org-scoped users (who also have organization-member).
//
// For example, `org_auditor` is now expected to be forbidden for
// `assign_org_role:read`, even though in production an org auditor can read
// available org roles via the org-member baseline.
//
// The tests are still useful for unit-testing the built-in roles in isolation.
//
// TODO(geokat): Add an integration test that includes organization-member to
// recover the old test coverage.
//
// nolint:tparallel,paralleltest // subtests share a map, just run sequentially.
func TestRolePermissions(t *testing.T) {
t.Parallel()
crud := []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
auth := rbac.NewStrictAuthorizer(prometheus.NewRegistry())
// currentUser is anything that references "me", "mine", or "my".
currentUser := uuid.New()
adminID := uuid.New()
templateAdminID := uuid.New()
userAdminID := uuid.New()
auditorID := uuid.New()
orgID := uuid.New()
otherOrg := uuid.New()
workspaceID := uuid.New()
templateID := uuid.New()
fileID := uuid.New()
groupID := uuid.New()
apiKeyID := uuid.New()
// Subjects to user
memberMe := authSubject{Name: "member_me", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember()}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
owner := authSubject{Name: "owner", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleOwner()}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
templateAdmin := authSubject{Name: "template-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleTemplateAdmin()}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
userAdmin := authSubject{Name: "user-admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleUserAdmin()}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
auditor := authSubject{Name: "auditor", Actor: rbac.Subject{ID: auditorID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleAuditor()}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
orgAdmin := authSubject{Name: "org_admin", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
orgAuditor := authSubject{Name: "org_auditor", Actor: rbac.Subject{ID: auditorID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAuditor(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
orgUserAdmin := authSubject{Name: "org_user_admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgUserAdmin(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
orgTemplateAdmin := authSubject{Name: "org_template_admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgTemplateAdmin(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
orgAdminBanWorkspace := authSubject{Name: "org_admin_workspace_ban", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(orgID), rbac.ScopedRoleOrgWorkspaceCreationBan(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
agentsAccessUser := func() authSubject {
memberRole, err := rbac.RoleByName(rbac.RoleMember())
require.NoError(t, err)
agentsRole, err := rbac.RoleByName(rbac.ScopedRoleAgentsAccess(orgID))
require.NoError(t, err)
return authSubject{
Name: "agents_access",
Actor: rbac.Subject{
ID: currentUser.String(),
Roles: rbac.Roles{memberRole, agentsRole},
Scope: rbac.ScopeAll,
}.WithCachedASTValue(),
}
}()
orgWorkspaceAccessUser := func() authSubject {
memberRole, err := rbac.RoleByName(rbac.RoleMember())
require.NoError(t, err)
orgWorkspaceAccessRole, err := rbac.RoleByName(rbac.ScopedRoleOrgWorkspaceAccess(orgID))
require.NoError(t, err)
return authSubject{
Name: "org_workspace_access",
Actor: rbac.Subject{
ID: currentUser.String(),
Roles: rbac.Roles{memberRole, orgWorkspaceAccessRole},
Scope: rbac.ScopeAll,
}.WithCachedASTValue(),
}
}()
orgMemberMe := func() authSubject {
memberRole, err := rbac.RoleByName(rbac.RoleMember())
require.NoError(t, err)
perms := rbac.OrgMemberPermissions(rbac.OrgSettings{
ShareableWorkspaceOwners: rbac.ShareableWorkspaceOwnersEveryone,
})
return authSubject{
Name: "org_member_me",
Actor: rbac.Subject{
ID: currentUser.String(),
Roles: rbac.Roles{
memberRole,
{
Identifier: rbac.ScopedRoleOrgMember(orgID),
Site: []rbac.Permission{},
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{
orgID.String(): {
Org: perms.Org,
Member: perms.Member,
},
},
},
},
Scope: rbac.ScopeAll,
}.WithCachedASTValue(),
}
}()
setOrgNotMe := authSubjectSet{orgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin}
otherOrgAdmin := authSubject{Name: "org_admin_other", Actor: rbac.Subject{ID: uuid.NewString(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(otherOrg)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
otherOrgAuditor := authSubject{Name: "org_auditor_other", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAuditor(otherOrg)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
otherOrgUserAdmin := authSubject{Name: "org_user_admin_other", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgUserAdmin(otherOrg)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
otherOrgTemplateAdmin := authSubject{Name: "org_template_admin_other", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgTemplateAdmin(otherOrg)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
setOtherOrg := authSubjectSet{otherOrgAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin}
// requiredSubjects are required to be asserted in each test case. This is
// to make sure one is not forgotten.
requiredSubjects := []authSubject{
memberMe, owner, agentsAccessUser, orgWorkspaceAccessUser,
orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin,
templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
}
testCases := []struct {
// Name the test case to better locate the failing test case.
Name string
Resource rbac.Object
Actions []policy.Action
// AuthorizeMap must cover all subjects in 'requiredSubjects'.
// This map will run an Authorize() check with the resource, action,
// and subjects. The subjects are split into 2 categories, "true" and
// "false".
// true: Subjects who Authorize should return no error
// false: Subjects who Authorize should return forbidden.
AuthorizeMap map[bool][]hasAuthSubjects
}{
{
Name: "MyUser",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceUserObject(currentUser),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, otherOrgAdmin, otherOrgUserAdmin, orgAdmin, orgWorkspaceAccessUser},
false: {
orgTemplateAdmin, orgAuditor,
otherOrgAuditor, otherOrgTemplateAdmin,
},
},
},
{
Name: "AUser",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceUser,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, userAdmin},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "ReadMyWorkspaceInOrg",
// When creating the WithID won't be set, but it does not change the result.
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser},
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin},
},
},
{
Name: "UpdateMyWorkspaceInOrg",
// When creating the WithID won't be set, but it does not change the result.
Actions: []policy.Action{policy.ActionUpdate},
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser},
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
},
},
{
Name: "CreateDeleteMyWorkspaceInOrg",
// When creating the WithID won't be set, but it does not change the result.
Actions: []policy.Action{policy.ActionCreate, policy.ActionDelete},
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgWorkspaceAccessUser},
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace},
},
},
{
Name: "CreateWorkspaceForMembers",
// When creating the WithID won't be set, but it does not change the result.
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceWorkspace.InOrg(orgID).WithOwner(policy.WildcardSymbol),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin},
false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "MyWorkspaceInOrgExecution",
// When creating the WithID won't be set, but it does not change the result.
Actions: []policy.Action{policy.ActionSSH},
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgWorkspaceAccessUser},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
},
},
{
Name: "MyWorkspaceInOrgAppConnect",
// When creating the WithID won't be set, but it does not change the result.
Actions: []policy.Action{policy.ActionApplicationConnect},
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgWorkspaceAccessUser},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
},
},
{
Name: "CreateDeleteWorkspaceAgent",
Actions: []policy.Action{policy.ActionCreateAgent, policy.ActionDeleteAgent},
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgWorkspaceAccessUser},
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace},
},
},
{
Name: "UpdateWorkspaceAgent",
Actions: []policy.Action{policy.ActionUpdateAgent},
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser},
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
},
},
{
Name: "ShareMyWorkspace",
Actions: []policy.Action{policy.ActionShare},
Resource: rbac.ResourceWorkspace.
WithID(workspaceID).
InOrg(orgID).
WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser},
false: {
memberMe, agentsAccessUser, setOtherOrg,
templateAdmin, userAdmin,
orgTemplateAdmin, orgUserAdmin, orgAuditor,
},
},
},
{
Name: "ShareWorkspaceDormant",
Actions: []policy.Action{policy.ActionShare},
Resource: rbac.ResourceWorkspaceDormant.
WithID(uuid.New()).
InOrg(orgID).
WithOwner(memberMe.Actor.ID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {},
false: {
orgAdmin, owner, setOtherOrg,
userAdmin, memberMe, agentsAccessUser,
templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor,
orgAdminBanWorkspace,
orgWorkspaceAccessUser,
},
},
},
{
Name: "Templates",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin},
false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "ReadTemplates",
Actions: []policy.Action{policy.ActionRead, policy.ActionViewInsights},
Resource: rbac.ResourceTemplate.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAuditor, orgAdmin, templateAdmin, orgTemplateAdmin},
false: {setOtherOrg, orgUserAdmin, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "UseTemplates",
Actions: []policy.Action{policy.ActionUse},
Resource: rbac.ResourceTemplate.InOrg(orgID).WithGroupACL(map[string][]policy.Action{
groupID.String(): {policy.ActionUse},
}),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin},
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "Files",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceFile.WithID(fileID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, templateAdmin},
// Org template admins can only read org scoped files.
// File scope is currently not org scoped :cry:
false: {setOtherOrg, orgTemplateAdmin, orgAdmin, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "MyFile",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead},
Resource: rbac.ResourceFile.WithID(fileID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser},
false: {setOtherOrg, setOrgNotMe, userAdmin},
},
},
{
Name: "CreateOrganizations",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceOrganization,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "Organizations",
Actions: []policy.Action{policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin},
false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "ReadOrganizations",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin, auditor, orgAuditor, userAdmin, orgUserAdmin},
false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser},
},
},
{
Name: "CreateUpdateDeleteCustomRole",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceAssignOrgRole,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, userAdmin, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "RoleAssignment",
Actions: []policy.Action{policy.ActionAssign, policy.ActionUnassign},
Resource: rbac.ResourceAssignRole,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, userAdmin},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "ReadRoleAssignment",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceAssignRole,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {setOtherOrg, setOrgNotMe, owner, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
false: {},
},
},
{
Name: "OrgRoleAssignment",
Actions: []policy.Action{policy.ActionAssign, policy.ActionUnassign},
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, userAdmin, orgUserAdmin},
false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor, orgWorkspaceAccessUser},
},
},
{
Name: "CreateOrgRoleAssignment",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate},
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin},
false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "ReadOrgRoleAssignment",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgUserAdmin, userAdmin, templateAdmin},
false: {setOtherOrg, memberMe, agentsAccessUser, orgAuditor, orgTemplateAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "APIKey",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete, policy.ActionUpdate},
Resource: rbac.ResourceApiKey.WithID(apiKeyID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, memberMe, agentsAccessUser, orgWorkspaceAccessUser},
false: {setOtherOrg, setOrgNotMe, templateAdmin, userAdmin},
},
},
{
Name: "InboxNotification",
Actions: []policy.Action{
policy.ActionCreate, policy.ActionRead, policy.ActionUpdate,
},
Resource: rbac.ResourceInboxNotification.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin},
false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, templateAdmin, userAdmin, memberMe, agentsAccessUser, orgWorkspaceAccessUser},
},
},
{
Name: "UserData",
Actions: []policy.Action{policy.ActionReadPersonal, policy.ActionUpdatePersonal},
Resource: rbac.ResourceUserObject(currentUser),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser},
false: {setOtherOrg, setOrgNotMe, templateAdmin},
},
},
{
Name: "ManageOrgMember",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, userAdmin, orgUserAdmin},
false: {setOtherOrg, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "ReadOrgMember",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin},
false: {memberMe, agentsAccessUser, setOtherOrg, orgWorkspaceAccessUser},
},
},
{
Name: "AllUsersGroupACL",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID).WithGroupACL(
map[string][]policy.Action{
orgID.String(): {policy.ActionRead},
}),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin, orgAuditor, agentsAccessUser, orgWorkspaceAccessUser},
false: {setOtherOrg, memberMe, userAdmin},
},
},
{
Name: "Groups",
Actions: []policy.Action{policy.ActionCreate, policy.ActionDelete, policy.ActionUpdate},
Resource: rbac.ResourceGroup.WithID(groupID).InOrg(orgID).WithGroupACL(map[string][]policy.Action{
groupID.String(): {
policy.ActionRead,
},
}),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, userAdmin, orgUserAdmin},
false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor, orgWorkspaceAccessUser},
},
},
{
Name: "GroupsRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceGroup.WithID(groupID).InOrg(orgID).WithGroupACL(map[string][]policy.Action{
groupID.String(): {
policy.ActionRead,
},
}),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser},
},
},
{
Name: "GroupMemberMeRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceGroupMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin},
false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser},
},
},
{
Name: "GroupMemberOtherRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceGroupMember.WithID(adminID).InOrg(orgID).WithOwner(adminID.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin},
false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser},
},
},
{
Name: "WorkspaceDormantRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {orgAdmin, owner, templateAdmin, orgTemplateAdmin, orgWorkspaceAccessUser},
false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, orgUserAdmin, orgAuditor},
},
},
{
Name: "WorkspaceDormant",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {orgAdmin, owner, orgWorkspaceAccessUser},
false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
},
},
{
Name: "WorkspaceDormantUse",
Actions: []policy.Action{policy.ActionWorkspaceStart, policy.ActionApplicationConnect, policy.ActionSSH},
Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, userAdmin, owner, templateAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "WorkspaceBuild",
Actions: []policy.Action{policy.ActionWorkspaceStart, policy.ActionWorkspaceStop},
Resource: rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgWorkspaceAccessUser},
false: {setOtherOrg, userAdmin, templateAdmin, memberMe, agentsAccessUser, orgTemplateAdmin, orgUserAdmin, orgAuditor},
},
},
{
Name: "PrebuiltWorkspace",
Actions: []policy.Action{policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourcePrebuiltWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(database.PrebuildsSystemUserID.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin},
false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser},
},
},
{
Name: "Task",
Actions: crud,
Resource: rbac.ResourceTask.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgWorkspaceAccessUser},
false: {setOtherOrg, userAdmin, templateAdmin, memberMe, agentsAccessUser, orgTemplateAdmin, orgUserAdmin, orgAuditor},
},
},
// Some admin style resources
{
Name: "Licenses",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
Resource: rbac.ResourceLicense,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "DeploymentStats",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceDeploymentStats,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "DeploymentConfig",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceDeploymentConfig,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "MCPServerConfigRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceMCPServerConfig.WithID(uuid.New()).InOrg(orgID).WithGroupACL(map[string][]policy.Action{
orgID.String(): {policy.ActionRead},
}),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgAuditor, orgMemberMe, agentsAccessUser, orgWorkspaceAccessUser, orgUserAdmin, orgTemplateAdmin},
false: {setOtherOrg, memberMe, templateAdmin, userAdmin, auditor},
},
},
{
Name: "MCPServerConfigWrite",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionShare},
Resource: rbac.ResourceMCPServerConfig.WithID(uuid.New()).InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin},
false: {setOtherOrg, orgAuditor, auditor, orgMemberMe, memberMe, agentsAccessUser, orgWorkspaceAccessUser, orgUserAdmin, orgTemplateAdmin, templateAdmin, userAdmin},
},
},
{
Name: "DebugInfo",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceDebugInfo,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "Replicas",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceReplicas,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "TailnetCoordinator",
Actions: crud,
Resource: rbac.ResourceTailnetCoordinator,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "AuditLogs",
Actions: []policy.Action{policy.ActionRead, policy.ActionCreate},
Resource: rbac.ResourceAuditLog,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "ProvisionerDaemons",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin},
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "ProvisionerDaemonsRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin},
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "UserProvisionerDaemonsCreate",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, templateAdmin, orgTemplateAdmin, orgAdmin, orgWorkspaceAccessUser},
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, orgAuditor},
},
},
{
Name: "UserProvisionerDaemonsUpdateDelete",
Actions: []policy.Action{policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, templateAdmin, orgTemplateAdmin, orgAdmin},
false: {orgWorkspaceAccessUser, setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, orgAuditor},
},
},
{
Name: "ProvisionerJobs",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionCreate},
Resource: rbac.ResourceProvisionerJobs.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgTemplateAdmin, orgAdmin},
false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser},
},
},
{
Name: "System",
Actions: crud,
Resource: rbac.ResourceSystem,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "Oauth2App",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceOauth2App,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "Oauth2AppRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceOauth2App,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
false: {},
},
},
{
Name: "Oauth2AppSecret",
Actions: crud,
Resource: rbac.ResourceOauth2AppSecret,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "Oauth2Token",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
Resource: rbac.ResourceOauth2AppCodeToken,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "WorkspaceProxy",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceWorkspaceProxy,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "WorkspaceProxyRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceWorkspaceProxy,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
false: {},
},
},
{
Name: "WorkspaceBuildOrchestration",
Actions: crud,
Resource: rbac.ResourceWorkspaceBuildOrchestration.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
// Any owner/admin across may access any users' preferences
// Members may not access other members' preferences
Name: "NotificationPreferencesOwn",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceNotificationPreference.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {orgWorkspaceAccessUser, memberMe, agentsAccessUser, owner},
false: {
userAdmin, orgUserAdmin, templateAdmin,
orgAuditor, orgTemplateAdmin,
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
orgAdmin, otherOrgAdmin,
},
},
},
{
// Any owner/admin may access notification templates
Name: "NotificationTemplates",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceNotificationTemplate,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {
orgWorkspaceAccessUser, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, templateAdmin,
orgAuditor, orgTemplateAdmin,
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
orgAdmin, otherOrgAdmin,
},
},
},
{
Name: "NotificationMessages",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceNotificationMessage,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {
memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
orgWorkspaceAccessUser,
},
},
},
{
// Notification preferences are currently not organization-scoped
// Any owner/admin may access any users' preferences
// Members may not access other members' preferences
Name: "NotificationPreferencesOtherUser",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceNotificationPreference.WithOwner(uuid.NewString()), // some other user
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {
orgWorkspaceAccessUser, memberMe, agentsAccessUser, templateAdmin, orgUserAdmin, userAdmin,
orgAdmin, orgAuditor, orgTemplateAdmin,
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
otherOrgAdmin,
},
},
},
// All users can create, read, and delete their own webpush notification subscriptions.
{
Name: "WebpushSubscription",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
Resource: rbac.ResourceWebpushSubscription.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, memberMe, agentsAccessUser, orgWorkspaceAccessUser},
false: {orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin},
},
},
// AnyOrganization tests
{
Name: "CreateOrgMember",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceOrganizationMember.AnyOrganization(),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, userAdmin, orgAdmin, otherOrgAdmin, orgUserAdmin, otherOrgUserAdmin},
false: {
memberMe, agentsAccessUser, templateAdmin,
orgTemplateAdmin, orgAuditor,
otherOrgAuditor, otherOrgTemplateAdmin,
orgWorkspaceAccessUser,
},
},
},
{
Name: "CreateTemplateAnyOrg",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceTemplate.AnyOrganization(),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, orgAdmin, otherOrgAdmin},
false: {
userAdmin, memberMe, agentsAccessUser,
orgAuditor, orgUserAdmin,
otherOrgAuditor, otherOrgUserAdmin,
orgWorkspaceAccessUser,
},
},
},
{
Name: "CreateWorkspaceAnyOrg",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceWorkspace.AnyOrganization().WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, otherOrgAdmin, orgWorkspaceAccessUser},
false: {
memberMe, agentsAccessUser, userAdmin, templateAdmin,
orgAuditor, orgUserAdmin, orgTemplateAdmin,
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
},
},
},
{
Name: "CryptoKeys",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionRead},
Resource: rbac.ResourceCryptoKey,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "IDPSyncSettings",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceIdpsyncSettings.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgUserAdmin, userAdmin},
false: {
otherOrgAdmin,
memberMe, agentsAccessUser, templateAdmin,
orgAuditor, orgTemplateAdmin,
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
orgWorkspaceAccessUser,
},
},
},
{
Name: "OrganizationIDPSyncSettings",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceIdpsyncSettings,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, userAdmin},
false: {
orgAdmin, orgUserAdmin,
otherOrgAdmin,
memberMe, agentsAccessUser, templateAdmin,
orgAuditor, orgTemplateAdmin,
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
orgWorkspaceAccessUser,
},
},
},
{
Name: "ResourceMonitor",
Actions: []policy.Action{policy.ActionRead, policy.ActionCreate, policy.ActionUpdate},
Resource: rbac.ResourceWorkspaceAgentResourceMonitor,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {
memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
orgWorkspaceAccessUser,
},
},
},
{
Name: "WorkspaceAgentDevcontainers",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceWorkspaceAgentDevcontainers,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {
memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
orgWorkspaceAccessUser,
},
},
},
{
Name: "ConnectionLogs",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceConnectionLog,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
// Only the user themselves can access their own secrets — no one else.
{
Name: "UserSecrets",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceUserSecret.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {memberMe, agentsAccessUser, orgWorkspaceAccessUser},
false: {
owner, orgAdmin,
otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin,
templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
},
},
},
// Skills are user-authored instructions, not secrets. Owners can inspect
// and delete them, but only the user can create or update them.
{
Name: "UserSkillsReadDelete",
Actions: []policy.Action{policy.ActionRead, policy.ActionDelete},
Resource: rbac.ResourceUserSkill.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, memberMe, agentsAccessUser, orgWorkspaceAccessUser},
false: {
orgAdmin,
otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin,
templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
},
},
},
{
Name: "UserSkillsCreateUpdate",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate},
Resource: rbac.ResourceUserSkill.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {memberMe, agentsAccessUser, orgWorkspaceAccessUser},
false: {
owner, orgAdmin,
otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin,
templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
},
},
},
{
Name: "UsageEvents",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceUsageEvent,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {},
false: {
owner,
memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
orgWorkspaceAccessUser,
},
},
},
{
// Members can create/update records but can't read them afterwards.
Name: "AIBridgeInterceptionsCreateUpdate",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate},
Resource: rbac.ResourceAibridgeInterception.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {orgWorkspaceAccessUser, owner, memberMe, agentsAccessUser},
false: {
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
// Only owners and site-wide auditors can view interceptions and their sub-resources.
Name: "AIBridgeInterceptionsRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceAibridgeInterception.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, auditor},
false: {
orgWorkspaceAccessUser, memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
// Only owners can manage AI providers. Provider
// configuration is deployment-wide and includes secret
// material (api_key, settings) so it is not exposed to
// org admins or auditors.
Name: "AIProviders",
Actions: crud,
Resource: rbac.ResourceAIProvider,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {
orgWorkspaceAccessUser, memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
// Only owners can manage AI Gateway keys. They hold
// a hashed bearer secret used to authenticate Gateway
// replicas to coderd. Keys are deployment-wide.
Name: "AIGatewayKey",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
Resource: rbac.ResourceAIGatewayKey,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {
orgWorkspaceAccessUser, memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
// Updating an AI Gateway key records last-used liveness when a
// Gateway replica authenticates. It is reserved for the system
// actor, so no user-facing role, including owner, is authorized.
Name: "AIGatewayKeyUpdate",
Actions: []policy.Action{policy.ActionUpdate},
Resource: rbac.ResourceAIGatewayKey,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {},
false: {
owner,
orgWorkspaceAccessUser, memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
Name: "BoundaryUsage",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceBoundaryUsage,
AuthorizeMap: map[bool][]hasAuthSubjects{
false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "AiSeat",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead},
Resource: rbac.ResourceAiSeat,
AuthorizeMap: map[bool][]hasAuthSubjects{
false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
Name: "AiModelPrice",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceAiModelPrice,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser},
},
},
{
// Boundary logs: members can create logs they own (user-scoped).
// memberMe and agentsAccessUser have ID == currentUser, so they
// match the resource owner. Other subjects have different IDs.
Name: "BoundaryLogCreate",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceBoundaryLog.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {orgWorkspaceAccessUser, memberMe, agentsAccessUser},
false: {
owner,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor, auditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
// Cross-user isolation: no subject can create boundary logs
// owned by a different user. The resource owner is a random
// UUID that does not match any test subject's ID.
Name: "BoundaryLogCreateOther",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceBoundaryLog.WithOwner(uuid.New().String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {},
false: {
orgWorkspaceAccessUser, owner, memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor, auditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
// Boundary logs: only DBPurge can delete. No human role
// has delete; DBPurge is a system subject outside this matrix.
Name: "BoundaryLogDelete",
Actions: []policy.Action{policy.ActionDelete},
Resource: rbac.ResourceBoundaryLog,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {},
false: {
orgWorkspaceAccessUser, owner, memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor, auditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
// Boundary logs: owner and auditor get read.
Name: "BoundaryLogRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceBoundaryLog,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, auditor},
false: {
orgWorkspaceAccessUser, memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
Name: "ChatUsageCRU",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, agentsAccessUser},
false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser},
},
},
{
Name: "ChatUsageShare",
Actions: []policy.Action{policy.ActionShare},
Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, agentsAccessUser},
false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser},
},
},
{
Name: "ChatUsageDelete",
Actions: []policy.Action{policy.ActionDelete},
Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin},
false: {setOtherOrg, memberMe, orgMemberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser},
},
},
{
Name: "ChatModelConfigRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceChatModelConfig.WithID(uuid.New()).InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, auditor, orgAuditor},
false: {
memberMe, orgMemberMe, agentsAccessUser, orgWorkspaceAccessUser,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
otherOrgAdmin, otherOrgAuditor,
},
},
},
{
Name: "ChatModelConfigCreateUpdateDeleteShare",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionShare},
Resource: rbac.ResourceChatModelConfig.WithID(uuid.New()).InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin},
false: {
memberMe, orgMemberMe, agentsAccessUser, orgWorkspaceAccessUser,
auditor, orgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
otherOrgAdmin, otherOrgAuditor,
},
},
},
}
// Build coverage set from test case definitions statically,
// so we don't need shared mutable state during execution.
// This allows subtests to run in parallel.
coveredPermissions := make(map[string]map[policy.Action]bool)
for _, c := range testCases {
for _, action := range c.Actions {
if coveredPermissions[c.Resource.Type] == nil {
coveredPermissions[c.Resource.Type] = make(map[policy.Action]bool)
}
coveredPermissions[c.Resource.Type][action] = true
}
}
// Check coverage: every permission in policy.RBACPermissions must
// be covered by at least one test case.
for rtype, perms := range policy.RBACPermissions {
t.Run(fmt.Sprintf("%s-AllActions", rtype), func(t *testing.T) {
t.Parallel()
for action := range perms.Actions {
assert.True(t, coveredPermissions[rtype][action],
"action %q on type %q is not tested", action, rtype)
}
})
}
for _, c := range testCases {
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
remainingSubjs := make(map[string]struct{})
for _, subj := range requiredSubjects {
remainingSubjs[subj.Name] = struct{}{}
}
for _, action := range c.Actions {
err := c.Resource.ValidAction(action)
if !assert.NoError(t, err, "%q is not a valid action for type %q", action, c.Resource.Type) {
continue
}
for result, sets := range c.AuthorizeMap {
subjs := make([]authSubject, 0)
for _, set := range sets {
subjs = append(subjs, set.Subjects()...)
}
used := make(map[string]bool)
for _, subj := range subjs {
if _, ok := used[subj.Name]; ok {
assert.False(t, true, "duplicate subject %q", subj.Name)
}
used[subj.Name] = true
delete(remainingSubjs, subj.Name)
msg := fmt.Sprintf("%s as %q doing %q on %q", c.Name, subj.Name, action, c.Resource.Type)
// TODO: scopey
actor := subj.Actor
// Actor is missing some fields
if actor.Scope == nil {
actor.Scope = rbac.ScopeAll
}
err := auth.Authorize(context.Background(), actor, action, c.Resource)
if result {
assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg))
} else {
assert.ErrorContains(t, err, "forbidden", fmt.Sprintf("Should fail: %s", msg))
}
}
}
}
require.Empty(t, remainingSubjs, "test should cover all subjects")
})
}
}
func TestIsOrgRole(t *testing.T) {
t.Parallel()
randomUUID, err := uuid.Parse("cad8c09d-c099-4ec7-9263-7d52b1a3997a")
require.NoError(t, err)
testCases := []struct {
Identifier rbac.RoleIdentifier
OrgRole bool
OrgID uuid.UUID
}{
// Not org roles
{Identifier: rbac.RoleOwner()},
{Identifier: rbac.RoleMember()},
{Identifier: rbac.RoleAuditor()},
{
Identifier: rbac.RoleIdentifier{},
OrgRole: false,
},
// Org roles
{
Identifier: rbac.ScopedRoleOrgAdmin(randomUUID),
OrgRole: true,
OrgID: randomUUID,
},
{
Identifier: rbac.ScopedRoleOrgMember(randomUUID),
OrgRole: true,
OrgID: randomUUID,
},
}
// nolint:paralleltest
for _, c := range testCases {
t.Run(c.Identifier.String(), func(t *testing.T) {
t.Parallel()
ok := c.Identifier.IsOrgRole()
require.Equal(t, c.OrgRole, ok, "match expected org role")
require.Equal(t, c.OrgID, c.Identifier.OrganizationID, "match expected org id")
})
}
}
func TestListRoles(t *testing.T) {
t.Parallel()
siteRoles := rbac.SiteBuiltInRoles()
siteRoleNames := make([]string, 0, len(siteRoles))
for _, role := range siteRoles {
siteRoleNames = append(siteRoleNames, role.Identifier.Name)
}
// If this test is ever failing, just update the list to the roles
// expected from the builtin set.
// Always use constant strings, as if the names change, we need to write
// a SQL migration to change the name on the backend.
require.ElementsMatch(t, []string{
"owner",
"member",
"auditor",
"template-admin",
"user-admin",
},
siteRoleNames)
orgID := uuid.New()
orgRoles := rbac.OrganizationRoles(orgID)
orgRoleNames := make([]string, 0, len(orgRoles))
for _, role := range orgRoles {
orgRoleNames = append(orgRoleNames, role.Identifier.String())
}
require.ElementsMatch(t, []string{
fmt.Sprintf("organization-admin:%s", orgID.String()),
fmt.Sprintf("organization-auditor:%s", orgID.String()),
fmt.Sprintf("organization-user-admin:%s", orgID.String()),
fmt.Sprintf("organization-template-admin:%s", orgID.String()),
fmt.Sprintf("organization-workspace-creation-ban:%s", orgID.String()),
fmt.Sprintf("organization-workspace-access:%s", orgID.String()),
fmt.Sprintf("agents-access:%s", orgID.String()),
},
orgRoleNames)
}
func TestChangeSet(t *testing.T) {
t.Parallel()
testCases := []struct {
Name string
From []string
To []string
ExpAdd []string
ExpRemove []string
}{
{
Name: "Empty",
},
{
Name: "Same",
From: []string{"a", "b", "c"},
To: []string{"a", "b", "c"},
ExpAdd: []string{},
ExpRemove: []string{},
},
{
Name: "AllRemoved",
From: []string{"a", "b", "c"},
ExpRemove: []string{"a", "b", "c"},
},
{
Name: "AllAdded",
To: []string{"a", "b", "c"},
ExpAdd: []string{"a", "b", "c"},
},
{
Name: "AddAndRemove",
From: []string{"a", "b", "c"},
To: []string{"a", "b", "d", "e"},
ExpAdd: []string{"d", "e"},
ExpRemove: []string{"c"},
},
}
convert := func(s []string) rbac.RoleIdentifiers {
tmp := make([]rbac.RoleIdentifier, 0, len(s))
for _, e := range s {
tmp = append(tmp, rbac.RoleIdentifier{Name: e})
}
return tmp
}
for _, c := range testCases {
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
add, remove := rbac.ChangeRoleSet(convert(c.From), convert(c.To))
require.ElementsMatch(t, convert(c.ExpAdd), add, "expect added")
require.ElementsMatch(t, convert(c.ExpRemove), remove, "expect removed")
})
}
}
// TestWorkspaceAgentScopeBoundaryLog verifies that a real workspace agent
// scope (not ScopeAll) can create boundary logs for its own owner but
// cannot create them for other users, and cannot read or delete them.
func TestWorkspaceAgentScopeBoundaryLog(t *testing.T) {
t.Parallel()
auth := rbac.NewStrictAuthorizer(prometheus.NewRegistry())
ownerID := uuid.New()
otherOwnerID := uuid.New()
workspaceID := uuid.New()
templateID := uuid.New()
versionID := uuid.New()
agentScope := rbac.WorkspaceAgentScope(rbac.WorkspaceAgentScopeParams{
WorkspaceID: workspaceID,
OwnerID: ownerID,
TemplateID: templateID,
VersionID: versionID,
})
memberRole, err := rbac.RoleByName(rbac.RoleMember())
require.NoError(t, err)
agent := rbac.Subject{
ID: ownerID.String(),
Roles: rbac.Roles{memberRole},
Scope: agentScope,
}.WithCachedASTValue()
// Agent can create boundary logs for its own owner.
err = auth.Authorize(context.Background(), agent, policy.ActionCreate,
rbac.ResourceBoundaryLog.WithOwner(ownerID.String()))
require.NoError(t, err, "agent should create boundary logs for own owner")
// Agent cannot create boundary logs for a different owner.
err = auth.Authorize(context.Background(), agent, policy.ActionCreate,
rbac.ResourceBoundaryLog.WithOwner(otherOwnerID.String()))
require.Error(t, err, "agent must not create boundary logs for other owner")
// Agent cannot read boundary logs (even its own owner's).
err = auth.Authorize(context.Background(), agent, policy.ActionRead,
rbac.ResourceBoundaryLog.WithOwner(ownerID.String()))
require.Error(t, err, "agent must not read boundary logs")
// Agent cannot delete boundary logs (even its own owner's).
err = auth.Authorize(context.Background(), agent, policy.ActionDelete,
rbac.ResourceBoundaryLog.WithOwner(ownerID.String()))
require.Error(t, err, "agent must not delete boundary logs")
// When the workspace owner is a site admin, the agent scope
// wildcard for boundary_log combined with the owner role's site-level
// read grant means the agent CAN read all boundary logs. This is an
// accepted consequence of the wildcard scope needed for creation.
ownerRole, err := rbac.RoleByName(rbac.RoleOwner())
require.NoError(t, err)
adminAgent := rbac.Subject{
ID: ownerID.String(),
Roles: rbac.Roles{memberRole, ownerRole},
Scope: agentScope,
}.WithCachedASTValue()
// Admin-owned agent CAN read boundary logs due to site-level owner
// role + wildcard scope.
err = auth.Authorize(context.Background(), adminAgent, policy.ActionRead,
rbac.ResourceBoundaryLog.WithOwner(otherOwnerID.String()))
require.NoError(t, err, "admin agent inherits site-level read via owner role")
// Admin-owned agent still cannot create boundary logs for another owner
// because member-level create is user-scoped (subject.id must match owner).
err = auth.Authorize(context.Background(), adminAgent, policy.ActionCreate,
rbac.ResourceBoundaryLog.WithOwner(otherOwnerID.String()))
require.Error(t, err, "admin agent must not create boundary logs for other owner")
}
// TestDBPurgeBoundaryLogDelete verifies that the DBPurge system subject
// can delete boundary logs but cannot create or read them.
func TestDBPurgeBoundaryLogDelete(t *testing.T) {
t.Parallel()
auth := rbac.NewStrictAuthorizer(prometheus.NewRegistry())
// Build the DBPurge subject the same way dbauthz does.
dbPurge := rbac.Subject{
Type: rbac.SubjectTypeDBPurge,
FriendlyName: "DB Purge",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "dbpurge"},
DisplayName: "DB Purge Daemon",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceBoundaryLog.Type: {policy.ActionDelete},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
// DBPurge can delete boundary logs.
err := auth.Authorize(context.Background(), dbPurge, policy.ActionDelete,
rbac.ResourceBoundaryLog)
require.NoError(t, err, "DBPurge should delete boundary logs")
// DBPurge cannot create boundary logs.
err = auth.Authorize(context.Background(), dbPurge, policy.ActionCreate,
rbac.ResourceBoundaryLog.WithOwner(uuid.New().String()))
require.Error(t, err, "DBPurge must not create boundary logs")
// DBPurge cannot read boundary logs.
err = auth.Authorize(context.Background(), dbPurge, policy.ActionRead,
rbac.ResourceBoundaryLog)
require.Error(t, err, "DBPurge must not read boundary logs")
}