mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd/rbac): make organization-member a per-org system custom role (#21359)
Migrated the built-in organization-member role to DB storage so it can be customized per org. Closes https://github.com/coder/internal/issues/1073 (part 1)
This commit is contained in:
@@ -168,7 +168,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "Admin",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleIdentifiers{ScopedRoleOrgMember(orgIDs[0]), RoleAuditor(), RoleOwner(), RoleMember()},
|
||||
Roles: RoleIdentifiers{RoleAuditor(), RoleOwner(), RoleMember()},
|
||||
},
|
||||
ObjectType: ResourceWorkspace.Type,
|
||||
Action: policy.ActionRead,
|
||||
@@ -177,7 +177,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "OrgAdmin",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleIdentifiers{ScopedRoleOrgMember(orgIDs[0]), ScopedRoleOrgAdmin(orgIDs[0]), RoleMember()},
|
||||
Roles: RoleIdentifiers{ScopedRoleOrgAdmin(orgIDs[0]), RoleMember()},
|
||||
},
|
||||
ObjectType: ResourceWorkspace.Type,
|
||||
Action: policy.ActionRead,
|
||||
@@ -186,7 +186,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "OrgMember",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleIdentifiers{ScopedRoleOrgMember(orgIDs[0]), ScopedRoleOrgMember(orgIDs[1]), RoleMember()},
|
||||
Roles: RoleIdentifiers{RoleMember()},
|
||||
},
|
||||
ObjectType: ResourceWorkspace.Type,
|
||||
Action: policy.ActionRead,
|
||||
@@ -196,11 +196,9 @@ func TestFilter(t *testing.T) {
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleIdentifiers{
|
||||
ScopedRoleOrgMember(orgIDs[0]), ScopedRoleOrgAdmin(orgIDs[0]),
|
||||
ScopedRoleOrgMember(orgIDs[1]), ScopedRoleOrgAdmin(orgIDs[1]),
|
||||
ScopedRoleOrgMember(orgIDs[2]), ScopedRoleOrgAdmin(orgIDs[2]),
|
||||
ScopedRoleOrgMember(orgIDs[4]),
|
||||
ScopedRoleOrgMember(orgIDs[5]),
|
||||
ScopedRoleOrgAdmin(orgIDs[0]),
|
||||
ScopedRoleOrgAdmin(orgIDs[1]),
|
||||
ScopedRoleOrgAdmin(orgIDs[2]),
|
||||
RoleMember(),
|
||||
},
|
||||
},
|
||||
@@ -221,10 +219,6 @@ func TestFilter(t *testing.T) {
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleIdentifiers{
|
||||
ScopedRoleOrgMember(orgIDs[0]),
|
||||
ScopedRoleOrgMember(orgIDs[1]),
|
||||
ScopedRoleOrgMember(orgIDs[2]),
|
||||
ScopedRoleOrgMember(orgIDs[3]),
|
||||
RoleMember(),
|
||||
},
|
||||
},
|
||||
@@ -235,7 +229,7 @@ func TestFilter(t *testing.T) {
|
||||
Name: "ScopeApplicationConnect",
|
||||
Actor: Subject{
|
||||
ID: userIDs[0].String(),
|
||||
Roles: RoleIdentifiers{ScopedRoleOrgMember(orgIDs[0]), RoleAuditor(), RoleOwner(), RoleMember()},
|
||||
Roles: RoleIdentifiers{RoleAuditor(), RoleOwner(), RoleMember()},
|
||||
},
|
||||
ObjectType: ResourceWorkspace.Type,
|
||||
Action: policy.ActionRead,
|
||||
@@ -312,7 +306,7 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
Groups: []string{allUsersGroup},
|
||||
Roles: Roles{
|
||||
must(RoleByName(RoleMember())),
|
||||
must(RoleByName(ScopedRoleOrgMember(defOrg))),
|
||||
orgMemberRole(defOrg),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -456,7 +450,7 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
Scope: must(ExpandScope(ScopeAll)),
|
||||
Roles: Roles{
|
||||
must(RoleByName(ScopedRoleOrgAdmin(defOrg))),
|
||||
must(RoleByName(ScopedRoleOrgMember(defOrg))),
|
||||
orgMemberRole(defOrg),
|
||||
must(RoleByName(RoleMember())),
|
||||
},
|
||||
}
|
||||
@@ -502,39 +496,40 @@ func TestAuthorizeDomain(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
siteAdminWorkspaceActions := slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionShare)
|
||||
testAuthorize(t, "SiteAdmin", user, []authTestCase{
|
||||
// Similar to an orphaned user, but has site level perms
|
||||
{resource: ResourceTemplate.AnyOrganization(), actions: []policy.Action{policy.ActionCreate}, allow: true},
|
||||
|
||||
// Org + me
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: siteAdminWorkspaceActions, allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: siteAdminWorkspaceActions, allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: siteAdminWorkspaceActions, allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.All(), actions: siteAdminWorkspaceActions, allow: true},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unusedID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner(user.ID), actions: siteAdminWorkspaceActions, allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unusedID), actions: siteAdminWorkspaceActions, allow: true},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: siteAdminWorkspaceActions, allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: siteAdminWorkspaceActions, allow: true},
|
||||
|
||||
// Other org + other use
|
||||
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unusedID), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: siteAdminWorkspaceActions, allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unusedID), actions: siteAdminWorkspaceActions, allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: siteAdminWorkspaceActions, allow: true},
|
||||
})
|
||||
|
||||
user = Subject{
|
||||
ID: "me",
|
||||
Scope: must(ExpandScope(ScopeApplicationConnect)),
|
||||
Roles: Roles{
|
||||
must(RoleByName(ScopedRoleOrgMember(defOrg))),
|
||||
orgMemberRole(defOrg),
|
||||
must(RoleByName(RoleMember())),
|
||||
},
|
||||
}
|
||||
@@ -762,7 +757,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
||||
|
||||
testAuthorize(t, "AdminAlwaysAllow", user,
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.actions = ResourceWorkspace.AvailableActions()
|
||||
c.actions = slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionShare)
|
||||
c.allow = true
|
||||
return c
|
||||
}, []authTestCase{
|
||||
@@ -890,7 +885,7 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
ID: "me",
|
||||
Roles: Roles{
|
||||
must(RoleByName(RoleMember())),
|
||||
must(RoleByName(ScopedRoleOrgMember(defOrg))),
|
||||
orgMemberRole(defOrg),
|
||||
},
|
||||
Scope: must(ExpandScope(ScopeApplicationConnect)),
|
||||
}
|
||||
@@ -926,7 +921,7 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
ID: "me",
|
||||
Roles: Roles{
|
||||
must(RoleByName(RoleMember())),
|
||||
must(RoleByName(ScopedRoleOrgMember(defOrg))),
|
||||
orgMemberRole(defOrg),
|
||||
},
|
||||
Scope: Scope{
|
||||
Role: Role{
|
||||
@@ -1015,7 +1010,7 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
ID: "me",
|
||||
Roles: Roles{
|
||||
must(RoleByName(RoleMember())),
|
||||
must(RoleByName(ScopedRoleOrgMember(defOrg))),
|
||||
orgMemberRole(defOrg),
|
||||
},
|
||||
Scope: Scope{
|
||||
Role: Role{
|
||||
@@ -1070,7 +1065,7 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
ID: meID.String(),
|
||||
Roles: Roles{
|
||||
must(RoleByName(RoleMember())),
|
||||
must(RoleByName(ScopedRoleOrgMember(defOrg))),
|
||||
orgMemberRole(defOrg),
|
||||
},
|
||||
Scope: must(ScopeNoUserData.Expand()),
|
||||
}
|
||||
@@ -1138,7 +1133,7 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
// This is odd behavior, as without this membership role, the test for
|
||||
// the workspace fails. Maybe scopes should just assume the user
|
||||
// is a member.
|
||||
must(RoleByName(ScopedRoleOrgMember(defOrg))),
|
||||
orgMemberRole(defOrg),
|
||||
},
|
||||
Scope: Scope{
|
||||
Role: Role{
|
||||
@@ -1404,6 +1399,28 @@ func testAuthorize(t *testing.T, name string, subject Subject, sets ...[]authTes
|
||||
}
|
||||
}
|
||||
|
||||
// orgMemberRole returns an organization-member role for RBAC-only tests.
|
||||
//
|
||||
// organization-member is now a DB-backed system role (not a built-in role), so
|
||||
// RoleByName won't resolve it here. Assume the default behavior: workspace
|
||||
// sharing enabled.
|
||||
func orgMemberRole(orgID uuid.UUID) Role {
|
||||
workspaceSharingDisabled := false
|
||||
orgPerms, memberPerms := OrgMemberPermissions(workspaceSharingDisabled)
|
||||
return Role{
|
||||
Identifier: ScopedRoleOrgMember(orgID),
|
||||
DisplayName: "",
|
||||
Site: []Permission{},
|
||||
User: []Permission{},
|
||||
ByOrgID: map[string]OrgPermissions{
|
||||
orgID.String(): {
|
||||
Org: orgPerms,
|
||||
Member: memberPerms,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
Reference in New Issue
Block a user