🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

Commit 85bcd5d

Browse files
fix!: require org membership for user ACLs (#26852) (#27884)
Backport of #26852 Original PR: #26852 — fix!: require org membership for user ACLs Merge commit: 6b3341a Requested by: @jdomeracki-coder Clean cherry-pick, no conflicts. --- _Opened by Coder Agents on behalf of @jdomeracki-coder._ Co-authored-by: Jon Ayers <jon@coder.com>
1 parent 350070e commit 85bcd5d

2 files changed

Lines changed: 108 additions & 3 deletions

File tree

coderd/rbac/authz_internal_test.go

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,19 +312,35 @@ func TestAuthorizeDomain(t *testing.T) {
312312

313313
testAuthorize(t, "UserACLList", user, []authTestCase{
314314
{
315-
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(unusedID).WithACLUserList(map[string][]policy.Action{
315+
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(defOrg).WithACLUserList(map[string][]policy.Action{
316316
user.ID: ResourceWorkspace.AvailableActions(),
317317
}),
318318
actions: ResourceWorkspace.AvailableActions(),
319319
allow: true,
320320
},
321321
{
322-
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(unusedID).WithACLUserList(map[string][]policy.Action{
322+
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(defOrg).WithACLUserList(map[string][]policy.Action{
323323
user.ID: {policy.WildcardSymbol},
324324
}),
325325
actions: ResourceWorkspace.AvailableActions(),
326326
allow: true,
327327
},
328+
{
329+
// User ACLs only grant permissions in organizations where the
330+
// subject is currently a member.
331+
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(unusedID).WithACLUserList(map[string][]policy.Action{
332+
user.ID: ResourceWorkspace.AvailableActions(),
333+
}),
334+
actions: ResourceWorkspace.AvailableActions(),
335+
allow: false,
336+
},
337+
{
338+
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(unusedID).WithACLUserList(map[string][]policy.Action{
339+
user.ID: {policy.WildcardSymbol},
340+
}),
341+
actions: ResourceWorkspace.AvailableActions(),
342+
allow: false,
343+
},
328344
{
329345
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(unusedID).WithACLUserList(map[string][]policy.Action{
330346
user.ID: {policy.ActionRead, policy.ActionUpdate},
@@ -714,6 +730,93 @@ func TestAuthorizeDomain(t *testing.T) {
714730
}))
715731
}
716732

733+
// TestAuthorizeUserACLOrgMembership verifies that user ACL grants require org
734+
// membership, while site-wide roles still authorize independent of ACLs.
735+
func TestAuthorizeUserACLOrgMembership(t *testing.T) {
736+
t.Parallel()
737+
738+
orgID := uuid.New()
739+
740+
// Site template-admin, not a member of orgID.
741+
siteTemplateAdmin := Subject{
742+
ID: "site-template-admin",
743+
Scope: must(ExpandScope(ScopeAll)),
744+
Roles: Roles{
745+
must(RoleByName(RoleMember())),
746+
must(RoleByName(RoleTemplateAdmin())),
747+
},
748+
}
749+
testAuthorize(t, "SiteTemplateAdminNotInOrg", siteTemplateAdmin, []authTestCase{
750+
{
751+
// Authorized by the site role, no ACL needed.
752+
resource: ResourceTemplate.InOrg(orgID),
753+
actions: []policy.Action{policy.ActionUpdate},
754+
allow: true,
755+
},
756+
{
757+
// Redundant ACL entry; still authorized by the site role.
758+
resource: ResourceTemplate.InOrg(orgID).WithACLUserList(map[string][]policy.Action{
759+
siteTemplateAdmin.ID: {policy.ActionUpdate},
760+
}),
761+
actions: []policy.Action{policy.ActionUpdate},
762+
allow: true,
763+
},
764+
})
765+
766+
// Site user-admin (no template perms), not a member of orgID.
767+
siteUserAdmin := Subject{
768+
ID: "site-user-admin",
769+
Scope: must(ExpandScope(ScopeAll)),
770+
Roles: Roles{
771+
must(RoleByName(RoleMember())),
772+
must(RoleByName(RoleUserAdmin())),
773+
},
774+
}
775+
testAuthorize(t, "SiteUserAdminNotInOrg", siteUserAdmin, []authTestCase{
776+
{
777+
// No template role and no ACL entry: denied.
778+
resource: ResourceTemplate.InOrg(orgID),
779+
actions: []policy.Action{policy.ActionUpdate},
780+
allow: false,
781+
},
782+
{
783+
// An ACL grant must not authorize a non-member.
784+
resource: ResourceTemplate.InOrg(orgID).WithACLUserList(map[string][]policy.Action{
785+
siteUserAdmin.ID: {policy.ActionUpdate},
786+
}),
787+
actions: []policy.Action{policy.ActionUpdate},
788+
allow: false,
789+
},
790+
})
791+
792+
// Same site user-admin, now also a member of orgID.
793+
siteUserAdminOrgMember := Subject{
794+
ID: "site-user-admin-org-member",
795+
Scope: must(ExpandScope(ScopeAll)),
796+
Roles: Roles{
797+
must(RoleByName(RoleMember())),
798+
must(RoleByName(RoleUserAdmin())),
799+
orgMemberRole(orgID),
800+
},
801+
}
802+
testAuthorize(t, "SiteUserAdminOrgMember", siteUserAdminOrgMember, []authTestCase{
803+
{
804+
// Org membership alone does not grant template update.
805+
resource: ResourceTemplate.InOrg(orgID),
806+
actions: []policy.Action{policy.ActionUpdate},
807+
allow: false,
808+
},
809+
{
810+
// As an org member, the ACL grant takes effect.
811+
resource: ResourceTemplate.InOrg(orgID).WithACLUserList(map[string][]policy.Action{
812+
siteUserAdminOrgMember.ID: {policy.ActionUpdate},
813+
}),
814+
actions: []policy.Action{policy.ActionUpdate},
815+
allow: true,
816+
},
817+
})
818+
}
819+
717820
// TestAuthorizeLevels ensures level overrides are acting appropriately
718821
func TestAuthorizeLevels(t *testing.T) {
719822
t.Parallel()

coderd/rbac/policy.rego

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,9 @@ object_is_included_in_scope_allow_list if {
330330

331331
# ACL for users
332332
acl_allow if {
333-
# TODO: Should you have to be a member of the org too?
333+
# The subject must be a member of the object's organization for a
334+
# user ACL grant to apply.
335+
is_org_member
334336
perms := input.object.acl_user_list[input.subject.id]
335337

336338
# Check if either the action or * is allowed

0 commit comments

Comments
 (0)