feat: configurable default org member roles#25994
Conversation
Docs preview📖 View docs preview for |
55edab1 to
1451f52
Compare
|
Design change: dropped the SQL
Rationale: the SQL Coder Agents on behalf of @Emyrk. |
| ALTER TABLE organizations | ||
| ADD COLUMN default_org_member_roles text[]; | ||
|
|
||
| UPDATE organizations | ||
| SET default_org_member_roles = ARRAY['organization-workspace-access']::text[]; | ||
|
|
||
| ALTER TABLE organizations | ||
| ALTER COLUMN default_org_member_roles SET NOT NULL; | ||
|
|
||
| COMMENT ON COLUMN organizations.default_org_member_roles IS | ||
| 'Roles granted to every member of this organization at request time. ' | ||
| 'The set is unioned into each member''s effective roles when ' | ||
| 'GetAuthorizationUserRoles runs, so changes propagate to all members ' | ||
| 'on the next request. Deployments can use this column to revoke ' | ||
| 'capabilities that would otherwise be considered normal organization ' | ||
| 'member permissions.'; |
There was a problem hiding this comment.
Existing orgs are backfilled this way
|
/coder-agents-review |
|
Chat: Review posted | View chat Review history
deep-review v0.6.1 | Round 1 | Last posted: Round 1, 12 findings (1 P1, 4 P3, 4 Nit, 3 Note), REQUEST_CHANGES. Review Finding inventoryFinding InventoryFindings
Round logRound 1Panel (12 reviewers: Bisky, Hisoka, Mafuuu, Pariston, Gon, Leorio, Knuckle, Kurapika, Meruem, Killua, Knov, Razor). Netero: 0 findings. 1 P1, 4 P3, 4 Nit, 3 Note. Reviewed against 6aef7c0..1451f52. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
Well-structured preparation PR. The resolve-at-read approach via GetAuthorizationUserRoles is a clean design: one SQL union point, no backfill needed, atomic propagation on next request. Test density is strong at 60%, and the three enterprise subtests cover the experiment gate thoroughly.
1 P1, 4 P3, 4 Nit, 3 Note.
The critical gap is the absence of any validation on role names written to default_org_member_roles. Nine of twelve reviewers converged on this independently. The P1 finding (CRF-1) traces two concrete attack paths: privilege escalation through built-in role injection, and DoS through malformed names that break RoleNameFromString and produce 500s for every org member. The experiment gate limits blast radius today but provides no structural protection.
Additional observations:
- PR description inaccuracy (CRF-11): The implementation notes state the migration adds a column with
DEFAULT ARRAY['organization-workspace-access']::text[]. The actual migration adds the column without a DEFAULT, backfills existing rows, then sets NOT NULL. The Go code supplies the value at every insertion point. Consider correcting the description to avoid misleading future readers. - PR title scope: The scope
coderddoes not contain all changed files (changes spancoderd/,codersdk/,enterprise/,site/). Per project rules, either use a broader path scope or omit the scope:feat: add organizations.default_org_member_roles for Gateway Accounts.
"The assumption 'callers will only shrink, not escalate' is invisible." (Meruem)
coderd/rbac/roles.go:1186
Note [CRF-12] Shrinking default_org_member_roles to empty is currently a no-op. organization-member (line 1181) and organization-service-account (line 1258) still bundle OrgWorkspaceAccessMemberPerms(). An operator who enables the experiment and sets default_org_member_roles = [] gets a successful API response, but effective permissions don't change because the member role still grants the same workspace-ops set. The PR acknowledges this as staged delivery. Noting it here so the follow-up is not forgotten. (Knov)
🤖
🤖 This review was automatically generated with Coder Agents.
1451f52 to
7511db4
Compare
7511db4 to
3efb4a4
Compare
|
Per follow-up review on the audit: rolled the
Full test sweep: Coder Agents on behalf of @Emyrk. |
3efb4a4 to
c5afc1c
Compare
|
Follow-up amend ( Why: Change in
Test: Coder Agents on behalf of @Emyrk. |
c5afc1c to
4d52fbe
Compare
|
Bot review response (
Coder Agents on behalf of @Emyrk. |
286774c to
e6dfeb2
Compare
fb33230 to
eafa7f5
Compare
85045f1 to
d15a3f0
Compare
eafa7f5 to
0d366d9
Compare
9edae41 to
ad60bf1
Compare
b96becd to
56ddb69
Compare
e9a3bd8 to
c746ace
Compare
56ddb69 to
dce59fb
Compare
c746ace to
eeadd81
Compare
| addedRoles := append(orgRoles, rbac.ScopedRoleOrgMember(arg.OrganizationID)) | ||
| addedRoles = append(addedRoles, defaultRoles...) | ||
| err = q.canAssignRoles(ctx, arg.OrganizationID, addedRoles, []rbac.RoleIdentifier{}) | ||
| if err != nil { | ||
| return database.OrganizationMember{}, err | ||
| } |
There was a problem hiding this comment.
As this is written, you need perms to assign the default role to change an org members perms. Right now role assign is very global, but this could be an issue later if we were change that.
I'm leaving it as is today.
| UPDATE organizations | ||
| SET default_org_member_roles = ARRAY['organization-workspace-access']::text[]; |
There was a problem hiding this comment.
Backfill all existing organizations
eeadd81 to
010e350
Compare
dce59fb to
662646e
Compare
Adds a new `default_org_member_roles` set attribute to the `coderd_organization` resource so callers can override the deployment-wide default member roles per organization. The attribute is set after `CreateOrganization` (the create endpoint doesn't accept it) and through `UpdateOrganization` thereafter. Also bumps coder/coder to a SHA that contains the gateway-accounts stack (coder/coder#25994) and reacts to coder/coder#24984, which migrated UpdateTemplateMeta fields from values to optional pointers.
662646e to
16724f9
Compare
| func DefaultOrgMemberRoles() []string { | ||
| return []string{orgWorkspaceAccess} | ||
| } |
There was a problem hiding this comment.
Do we need to catch up existing organizations as this evolves?
There was a problem hiding this comment.
There is a PR that backfills existing orgs. So this is covered
There was a problem hiding this comment.
I see the one-time migration backfill for introducing default_org_member_roles, but I don’t see anything that would catch up existing orgs if DefaultOrgMemberRoles() changes later. Was your reply referring just to the introduction backfill, or is there another reconcile path I’m missing?
There was a problem hiding this comment.
Ah. If we change DefaultOrgMemberRoles, you are right there is no resolution mechanism. I'll add a comment to this effect.
So it only catches up orgs to this state.
Changing it later is a bit tough to reconcile because after this feature goes live, it can be changed from the default set.
Adds a configurable per-org default member role set, unioned into each
member's effective roles at read time via GetAuthorizationUserRoles.
PATCH gated by the minimum-implicit-member experiment. Only built-in
role names accepted. Diff routed through canAssignRoles. Per-org,
not per-user. Default {organization-workspace-access} preserves
behavior.
Refs #25936.
- querier_test.go: the assertion only checks effective roles, so do not claim the test verifies that organization_members rows were untouched. - enterprise/coderd: replace the user-facing "Gateway Accounts are not enabled" message with a description tied to the actual API surface: changing default organization roles.
ed135cf to
0474e9f
Compare

Refs #25936. Stacks on #25929.
Adds a configurable per-org default member role set. Unioned into each member's effective roles at read time.
{organization-workspace-access}preserves behavior.minimum-implicit-memberexperiment.canAssignRoles. Caller must be permitted to assign each added or removed role.GetAuthorizationUserRoles. No backfill.Implementation notes
Gateway Accounts are not enabled on this deployment.ARRAY['organization-workspace-access'], then sets NOT NULL.DEFAULT.rbac.DefaultOrgMemberRoles()is the sole source; everyInsertOrganizationsite must supply it. NOT NULL is the fail-closed backstop.GetAuthorizationUserRolesjoinsorganizationsandarray_cats the column into the unnest. Applies to regular users and service accounts.rbac.DefaultOrgMemberRoles()is a function, not avar, so callers cannot mutate the shared default.UpdateOrganizationParams(sqlc-gen) preserves the existing value when the request omits the field.Coder Agents on behalf of @Emyrk.