mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
f22d4e2cbb
Adds table to store keys that AI Gateway standalone replicas will use to authenticate into Coderd. Also adds RBAC and audit boilerplate.
1679 lines
62 KiB
Go
1679 lines
62 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")
|
|
})
|
|
}
|
|
|
|
// 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(),
|
|
}
|
|
}()
|
|
|
|
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,
|
|
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},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
Name: "ReadRoleAssignment",
|
|
Actions: []policy.Action{policy.ActionRead},
|
|
Resource: rbac.ResourceAssignRole,
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {setOtherOrg, setOrgNotMe, owner, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
Name: "UserData",
|
|
Actions: []policy.Action{policy.ActionReadPersonal, policy.ActionUpdatePersonal},
|
|
Resource: rbac.ResourceUserObject(currentUser),
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner, memberMe, agentsAccessUser, userAdmin},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
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},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
Name: "Task",
|
|
Actions: crud,
|
|
Resource: rbac.ResourceTask.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner, orgAdmin},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
Name: "DeploymentStats",
|
|
Actions: []policy.Action{policy.ActionRead},
|
|
Resource: rbac.ResourceDeploymentStats,
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner},
|
|
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
Name: "DebugInfo",
|
|
Actions: []policy.Action{policy.ActionRead},
|
|
Resource: rbac.ResourceDebugInfo,
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner},
|
|
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "Replicas",
|
|
Actions: []policy.Action{policy.ActionRead},
|
|
Resource: rbac.ResourceReplicas,
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner},
|
|
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
Name: "TailnetCoordinator",
|
|
Actions: crud,
|
|
Resource: rbac.ResourceTailnetCoordinator,
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner},
|
|
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
Name: "UserProvisionerDaemons",
|
|
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
|
Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID),
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner, templateAdmin, orgTemplateAdmin, orgAdmin},
|
|
false: {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},
|
|
},
|
|
},
|
|
{
|
|
Name: "System",
|
|
Actions: crud,
|
|
Resource: rbac.ResourceSystem,
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner},
|
|
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
Name: "Oauth2AppRead",
|
|
Actions: []policy.Action{policy.ActionRead},
|
|
Resource: rbac.ResourceOauth2App,
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
|
false: {},
|
|
},
|
|
},
|
|
{
|
|
Name: "Oauth2AppSecret",
|
|
Actions: crud,
|
|
Resource: rbac.ResourceOauth2AppSecret,
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner},
|
|
false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
Name: "WorkspaceProxyRead",
|
|
Actions: []policy.Action{policy.ActionRead},
|
|
Resource: rbac.ResourceWorkspaceProxy,
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
|
false: {},
|
|
},
|
|
},
|
|
{
|
|
// 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: {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: {
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// 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: {
|
|
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},
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "CreateWorkspaceAnyOrg",
|
|
Actions: []policy.Action{policy.ActionCreate},
|
|
Resource: rbac.ResourceWorkspace.AnyOrganization().WithOwner(currentUser.String()),
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
true: {owner, orgAdmin, otherOrgAdmin},
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
// 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},
|
|
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},
|
|
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},
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// 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: {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: {
|
|
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: {
|
|
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: {
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
Name: "AiSeat",
|
|
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead},
|
|
Resource: rbac.ResourceAiSeat,
|
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
|
false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
// 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: {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: {
|
|
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: {
|
|
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: {
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
{
|
|
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},
|
|
},
|
|
},
|
|
}
|
|
// 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("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")
|
|
}
|