Skip to content

feat: add organizations.default_org_member_roles for Gateway Accounts#25994

Draft
Emyrk wants to merge 1 commit into
gateway-accounts/extract-workspace-accessfrom
gateway-accounts/org-default-roles
Draft

feat: add organizations.default_org_member_roles for Gateway Accounts#25994
Emyrk wants to merge 1 commit into
gateway-accounts/extract-workspace-accessfrom
gateway-accounts/org-default-roles

Conversation

@Emyrk
Copy link
Copy Markdown
Member

@Emyrk Emyrk commented Jun 2, 2026

Refs #25936. Stacks on #25929.

Adds organizations.default_org_member_roles and unions it into every member's effective roles at read time. Changes to the column propagate to all members on the next request. Deployment-wide default is {'organization-workspace-access'} so this PR is behavior-preserving.

  • PATCH organization accepts the new field but rejects deviations from the deployment default unless the minimum-implicit-member experiment is on (Gateway Accounts are not enabled on this deployment.).
  • Built-in role names only: PATCH with a name that does not resolve via rbac.RoleByName is rejected with HTTP 400 to block both custom roles and malformed names that would otherwise break RoleNameFromString downstream.
  • dbauthz UpdateOrganization routes changes to default_org_member_roles through canAssignRoles, so the caller must be permitted to assign each added or removed role, mirroring InsertOrganizationMember and UpdateMemberRoles.
  • Experiment constant lands in this PR so the write-gate has something to check; the floor-shrink behavior lands in a follow-up.
  • No backfill PR needed: resolve-at-read in GetAuthorizationUserRoles (single SQL change, atomic snapshot per request).
  • Gateway Accounts are per-org, not per-user. To make a gateway-accounts org an admin shrinks that org's default; per-user demotion is intentionally not supported.
Implementation notes
  • Migration 000515 adds the column as nullable, backfills existing rows with ARRAY['organization-workspace-access'], then sets NOT NULL. The column has no SQL DEFAULT; rbac.DefaultOrgMemberRoles() is the sole authoritative source and every InsertOrganization call site must supply this value. The NOT NULL constraint provides fail-closed behavior for any raw SQL that omits the column.
  • GetAuthorizationUserRoles joins organizations and array_cats the column into the unnest, so the SQL is the only union point. The union applies to regular users and to service accounts.
  • rbac.DefaultOrgMemberRoles() is a function (not a var) so callers cannot mutate the shared deployment default.
  • Audit table tracks the new column.
  • UpdateOrganizationParams (sqlc-gen) preserves the existing value when the request omits the field.

Coder Agents on behalf of @Emyrk.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

Docs preview

📖 View docs preview for docs/admin/security/audit-logs.md

Comment on lines +8 to +10
'on the next request. Used by the minimum-implicit-member experiment '
'to control whether workspace-operations capabilities are bundled into '
'the organization-member floor or attached via organization-workspace-access.';
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not mention the experiment because this column will exist after the experiment goes live. Just mention that some deployments may want revoke what is considered "normal permissions"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Rewrote the comment to drop the experiment reference and focus on the durable purpose: deployments can use this column to revoke capabilities that would otherwise be considered normal organization member permissions.

Comment thread coderd/rbac/roles.go Outdated
Comment thread codersdk/organizations.go
Comment thread codersdk/organizations.go
@Emyrk Emyrk force-pushed the gateway-accounts/org-default-roles branch 2 times, most recently from 55edab1 to 1451f52 Compare June 2, 2026 18:40
Copy link
Copy Markdown
Member Author

Emyrk commented Jun 2, 2026

Design change: dropped the SQL DEFAULT on default_org_member_roles; the Go const rbac.DefaultOrgMemberRoles is now the single source of truth.

  • Migration 000515 adds the column nullable, backfills existing orgs with ARRAY['organization-workspace-access'], then sets NOT NULL. No persistent DEFAULT.
  • InsertOrganization SQL query now requires default_org_member_roles. All call sites pass rbac.DefaultOrgMemberRoles (production: enterprise/coderd/organizations.go; test fixture: dbgen.Organization; 5 direct test sites).
  • Dropped TestDefaultOrgMemberRolesMatchesSQLDefault (no SQL DEFAULT to drift from).
  • Test expectations in coderd/httpmw/authorize_test.go and coderd/provisionerdserver/provisionerdserver_test.go updated to include organization-workspace-access in the owner role list, since GetAuthorizationUserRoles now correctly unions it in via the default.

Rationale: the SQL DEFAULT only did real work once (the ALTER TABLE backfill); every production INSERT goes through sqlc, which lists every column, so the DEFAULT was dead weight afterward. Removing it eliminates the drift surface entirely and forces conscious INSERTs.

Coder Agents on behalf of @Emyrk.

Comment on lines +1 to +16
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.';
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing orgs are backfilled this way

@Emyrk
Copy link
Copy Markdown
Member Author

Emyrk commented Jun 2, 2026

/coder-agents-review

@coder-agents-review
Copy link
Copy Markdown
Contributor

coder-agents-review Bot commented Jun 2, 2026

Chat: Review posted | View chat
Requested: 2026-06-02 20:33 UTC by @Emyrk
Spend: $43.03 / $100.00

Review history
  • R1 (2026-06-02): 12 reviewers, 4 Nit, 3 Note, 1 P1, 4 P3, REQUEST_CHANGES. Review

deep-review v0.6.1 | Round 1 | 6aef7c0..1451f52

Last posted: Round 1, 12 findings (1 P1, 4 P3, 4 Nit, 3 Note), REQUEST_CHANGES. Review

Finding inventory

Finding Inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P1 Open enterprise/coderd/organizations.go:108 No validation of role names in default_org_member_roles; privilege escalation and DoS R1 Hisoka P1, Kurapika P1, Mafuuu P2, Meruem P2, Knov P2, Knuckle P2, Razor P2, Pariston P3, Bisky P3 Yes
CRF-2 P3 Open coderd/rbac/roles.go:219 DefaultOrgMemberRoles is an exported mutable var slice R1 Hisoka, Kurapika, Meruem, Knov, Razor, Gon, Mafuuu Yes
CRF-3 P3 Open coderd/database/dbgen/dbgen.go:1043 takeFirstSlice swallows intentional empty DefaultOrgMemberRoles R1 Bisky, Mafuuu, Meruem, Knuckle Yes
CRF-4 P3 Open coderd/rbac/roles.go:216 DefaultOrgMemberRoles comment omits sync invariant and authoritative-source doc R1 Gon, Leorio Yes
CRF-5 P3 Open coderd/database/querier_test.go:3043 No test that service accounts also receive default_org_member_roles R1 Bisky Yes
CRF-6 Nit Open coderd/database/queries/users.sql:618 Endash (U+2013) in SQL comment adjacent to new code R1 Bisky, Leorio, Kurapika Yes
CRF-7 Nit Open codersdk/deployment.go:5006 Experiment naming: slug names mechanism not feature; display name includes jargon R1 Gon, Leorio Yes
CRF-8 Nit Open enterprise/coderd/organizations.go:65 Guard comment restates the if-condition R1 Gon Yes
CRF-9 Nit Open codersdk/organizations.go:120 Doc comment produces confusing generated API docs R1 Leorio Yes
CRF-10 Note Open coderd/database/queries/users.sql:633 Service accounts receive default_org_member_roles uniformly; design intent should be explicit R1 Pariston, Meruem, Razor Yes
CRF-11 Note Open coderd/database/migrations/000515_org_default_member_roles.up.sql:1 PR description claims migration adds DEFAULT clause; it does not R1 Leorio, Hisoka, Mafuuu, Meruem, Pariston, Razor Yes
CRF-12 Note Open coderd/rbac/roles.go:1186 Shrinking default_org_member_roles is a no-op until follow-up removes elevation from org-member R1 Knov Yes

Round log

Round 1

Panel (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-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

Copy link
Copy Markdown
Contributor

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 coderd does not contain all changed files (changes span coderd/, 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.

if req.Icon != nil {
updateOrgParams.Icon = *req.Icon
}
if req.DefaultOrgMemberRoles != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 [CRF-1] No validation that entries in DefaultOrgMemberRoles are legitimate role names. Two traced consequences:

  1. Privilege escalation. With the experiment enabled, PATCH /organizations/{id} with {"default_org_member_roles": ["organization-admin"]} stores the value. GetAuthorizationUserRoles produces organization-admin:<org_id> for every member. rolestore.Expand resolves it against builtInRoles (roles.go:32, 543). Every member gets org-admin permissions. Individual role revocation is defeated: a demoted admin retains privileges through this column.

  2. DoS. A role name containing a colon (e.g., "foo:bar") produces foo:bar:<org_id> after SQL concatenation. RoleNameFromString (roles.go:79) rejects it with "too many colons." The error propagates through UserRBACSubject to the apikey middleware (apikey.go:474), which returns a hard 500 for every authenticated request from every member of the org. Recovery requires direct database access.

The experiment gate limits blast radius today, but provides no structural protection against misuse. Compare with putMemberRoles, where canAssignRoles validates every role name against the built-in registry and checks ActionAssign on ResourceAssignOrgRole.

Fix: validate each entry against rbac.RoleByName(RoleIdentifier{Name: role, OrganizationID: org.ID}) before writing. Consider constraining to a subset of rbac.DefaultOrgMemberRoles since the stated purpose is shrinking the floor, not granting arbitrary roles. (Hisoka P1, Kurapika P1, Mafuuu P2, Meruem P2, Knov P2, Knuckle P2, Razor P2, Pariston P3, Bisky P3)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good callout. We need to check if the role can be assigned by the caller

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Two-layer fix in 4d52fbe8b8:

API layer (enterprise/coderd/organizations.go): added a built-in-only validation after the experiment gate. Each entry in req.DefaultOrgMemberRoles must resolve via rbac.RoleByName; anything else (custom role, malformed name with extra colons) returns 400 Invalid default_org_member_roles entry. This blocks the DoS attack path directly: "foo:bar" never reaches the SQL. Callout comment notes the temporary restriction so a future change can extend to custom org roles. Test in enterprise/coderd/organizations_test.go::TestPatchOrganizationsByUser/DefaultOrgMemberRoles/NonBuiltInRoleRejected.

dbauthz layer (coderd/database/dbauthz/dbauthz.go): rewrote UpdateOrganization to (1) authorize ActionUpdate on the org, then (2) when DefaultOrgMemberRoles differs from the existing value, compute the added/removed diff (as rbac.RoleIdentifiers scoped to the org) and route through the same canAssignRoles helper used by InsertOrganizationMember and UpdateMemberRoles. So the caller must hold ActionAssign on ResourceAssignOrgRole.InOrg(orgID) and must individually pass rbac.CanAssignRole for each added or removed role. This closes the escalation path: even with the experiment on, a non-admin caller can't slip organization-admin into the default set unless they were already permitted to assign org-admin individually. Test in coderd/database/dbauthz/dbauthz_test.go::UpdateOrganization now asserts both ActionUpdate and ActionAssign are required.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the hand-rolled defaultOrgMemberRolesDiff with rbac.ChangeRoleSet per Steven's review. The dbauthz UpdateOrganization path now wraps the stored []string as RoleIdentifiers via a tiny scopedOrgRoleIdentifiers helper and passes them straight through rbac.ChangeRoleSet -> canAssignRoles. Head: e6dfeb2988.

Comment thread coderd/rbac/roles.go Outdated
// DefaultOrgMemberRoles is the deployment-wide default for the
// organizations.default_org_member_roles column, applied to every
// new organization at creation time.
var DefaultOrgMemberRoles = []string{orgWorkspaceAccess}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-2] DefaultOrgMemberRoles is an exported mutable var slice used in security-relevant comparisons.

var DefaultOrgMemberRoles = []string{orgWorkspaceAccess}

The experiment gate in patchOrganization (organizations.go:70) compares the request against this value via slices.Equal. dbgen.Organization uses it as the creation default. Any code path that mutates the backing array (append, index assignment, variable reassignment) silently corrupts both the gate comparison and the creation default for all subsequent requests.

No current code mutates it, but Go exported var slices are a known mutation hazard. Seven of twelve reviewers flagged this independently.

Fix: return a copy from a function:

func DefaultOrgMemberRoles() []string { return []string{orgWorkspaceAccess} }

All six call sites change from rbac.DefaultOrgMemberRoles to rbac.DefaultOrgMemberRoles(). (Hisoka, Kurapika, Meruem, Knov, Razor, Gon, Mafuuu)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4d52fbe8b8. Converted to a function returning a fresh slice on each call. Updated all 12 call sites (coderd/rbac/roles.go, coderd/database/dbgen/dbgen.go, enterprise/coderd/organizations.go, coderd/provisionerdserver/acquirer_test.go, coderd/httpmw/authorize_test.go x2, coderd/httpmw/organizationparam_test.go, cli/server_createadminuser_test.go x2, enterprise/coderd/organizations_test.go x2). Doc comment now also states the "sole authoritative source / every InsertOrganization site must supply this value" invariant per CRF-4.

Comment thread coderd/database/dbgen/dbgen.go Outdated
Comment thread coderd/rbac/roles.go Outdated
Comment on lines +216 to +218
// DefaultOrgMemberRoles is the deployment-wide default for the
// organizations.default_org_member_roles column, applied to every
// new organization at creation time.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-4] Comment omits the sync invariant and authoritative-source documentation.

The PR description says the comment documents a sync requirement with the migration's column DEFAULT, but (a) no such sync requirement is stated in the comment, and (b) the column has no DEFAULT. The Go constant is the sole authoritative source of the default value. Every InsertOrganization call site must use it. A reader adding a new insert path or raw SQL script has no warning.

Suggested rewrite:

// DefaultOrgMemberRoles is the authoritative default for
// organizations.default_org_member_roles. The column has no DB-level
// DEFAULT, so every InsertOrganization call site must supply this value.

(Gon, Leorio)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Folded into the CRF-2 change in 4d52fbe8b8. The new DefaultOrgMemberRoles doc comment reads:

// DefaultOrgMemberRoles is the deployment-wide default for the
// organizations.default_org_member_roles column, applied to every new
// organization at creation time. The column has no SQL DEFAULT, so this
// is the sole authoritative source: every InsertOrganization call site
// must supply this value unless a caller-chosen override is required.
// Returned as a fresh slice each call to prevent accidental mutation of
// the shared default through append or index assignment.

Comment thread coderd/database/querier_test.go
Comment thread codersdk/deployment.go
Comment thread enterprise/coderd/organizations.go Outdated
Comment on lines +65 to +68
// Writes to default_org_member_roles that deviate from the
// deployment-wide default require the minimum-implicit-member
// experiment. Without the experiment, every organization keeps the
// standard floor + organization-workspace-access elevation.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-8] Guard comment restates the if-condition. The code is self-documenting: the three-line conditional with slices.Equal, rbac.DefaultOrgMemberRoles, and Experiments.Enabled(...) communicates intent clearly. The second sentence ("standard floor + organization-workspace-access elevation") references architectural terms that don't appear locally.

Suggested trim:

// Deviations from rbac.DefaultOrgMemberRoles require the
// minimum-implicit-member experiment.

(Gon)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4d52fbe8b8. Trimmed to your suggested:

// Deviations from rbac.DefaultOrgMemberRoles require the
// minimum-implicit-member experiment.

Comment thread codersdk/organizations.go
Comment on lines +120 to +121
// DefaultOrgMemberRoles, when non-nil, replaces the org's default
// member roles.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-9] The doc comment // DefaultOrgMemberRoles, when non-nil, replaces the org's default member roles. produces confusing generated API docs. Codegen expands the CamelCase name, creating: "Default org member roles when non-nil, replaces the org's default member roles." The field name merges into the sentence, making "when non-nil" read like a conditional on the roles themselves.

Fix: // When non-nil, replaces the org's default member roles. (Leorio)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping as-is for now. The codegen-friendly rewording is a worthwhile fix but is incidental to this PR; will land alongside the next codersdk-touching change.

'organization-member'
END
),
organizations.default_org_member_roles
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-10] default_org_member_roles applies uniformly to all org members, including service accounts. When an admin shrinks an org's defaults for Gateway Accounts (e.g., removes organization-workspace-access), service accounts in that org also lose those roles. This aligns with the PR description's "per-org, not per-user" intent, but is worth confirming explicitly in the follow-up that gateway-account orgs won't have service accounts that need workspace access. (Pariston, Meruem, Razor)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. By design: default_org_member_roles is per-org and unioned for both organization-member and organization-service-account. Shrinking the column affects every member of the org including service accounts. TestGetAuthorizationUserRolesUnionsDefaultOrgMemberRoles now exercises both code paths.

@@ -0,0 +1,16 @@
ALTER TABLE organizations
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-11] The PR description states 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 (rbac.DefaultOrgMemberRoles) is the sole authoritative source, supplied at every InsertOrganization call site. The NOT NULL constraint provides fail-closed behavior (raw SQL inserts that omit the column will fail). Consider correcting the PR description to avoid misleading future readers about the migration's behavior. (Leorio, Hisoka, Mafuuu, Meruem, Pariston, Razor)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description updated to reflect the actual nullable -> backfill -> NOT NULL pattern and the rbac.DefaultOrgMemberRoles() function as the sole authoritative source.

@Emyrk Emyrk force-pushed the gateway-accounts/org-default-roles branch from 1451f52 to 7511db4 Compare June 2, 2026 21:36
@Emyrk Emyrk force-pushed the gateway-accounts/org-default-roles branch from 7511db4 to 3efb4a4 Compare June 2, 2026 22:03
Copy link
Copy Markdown
Member Author

Emyrk commented Jun 2, 2026

Per follow-up review on the audit: rolled the dbauthz fix into this PR rather than splitting it out.

InsertOrganizationMember and UpdateMemberRoles now fetch the org's default_org_member_roles and include them in canAssignRoles's implied set. The caller is required to be authorized to assign the full effective role set (explicit roles + organization-member + the org's defaults), matching what GetAuthorizationUserRoles will hand back to the new member at request time.

  • Two new q.db.GetOrganizationByID lookups (bypass dbauthz so the caller doesn't need org-read).
  • Reuses the existing convertToOrganizationRoles helper to scope role names to the org.
  • Comments updated to drop the "deferred to config" caveat.
  • coderd/rbac/roles.go adds orgWorkspaceAccess: true to prebuildsOrchestrator's assignable set. Without it, the orchestrator can't add the prebuild user to an org whose default contains organization-workspace-access, which broke TestReinitializeAgent/useDefaultOrg=false.
  • dbauthz_test.go for InsertOrganizationMember and UpdateMemberRoles now seed the fake org with DefaultOrgMemberRoles: []string{} and add a GetOrganizationByID mock; existing ActionAssign assertions stay one per call (canAssignRoles emits a single ActionAssign regardless of how many roles are in the diff).

Full test sweep: coderd/rbac, coderd/database/dbauthz, enterprise/coderd (TestPatchOrganizationsByUser, TestReinitializeAgent), coderd (TestAddMember) all clean.

Coder Agents on behalf of @Emyrk.

@Emyrk Emyrk force-pushed the gateway-accounts/org-default-roles branch from 3efb4a4 to c5afc1c Compare June 2, 2026 22:11
Copy link
Copy Markdown
Member Author

Emyrk commented Jun 2, 2026

Follow-up amend (c5afc1cee6): folded the idpsync filter fix from the audit into this PR so the diff stays consistent with the new resolve-at-read semantics.

Why: (AGPLIDPSync).SyncRoles only stripped the literal organization-member role from both sides of the IdP diff. With default_org_member_roles now in play, any role listed there would otherwise look like a diff against OrganizationMember.Roles (which never stores defaults), so sync would try to re-grant or remove it forever.

Change in coderd/idpsync/role.go:

  • Inside the per-org loop, fetch org.DefaultOrgMemberRoles via tx.GetOrganizationByID (ctx is already dbauthz.AsSystemRestricted).
  • Build an implicit set = {organization-member} ∪ default_org_member_roles.
  • Delete entries in that set from both validExpected and existingFound before the equality check.

Test: TestNoopNoDiff gained the new GetOrganizationByID expectation returning DefaultOrgMemberRoles: []string{} so existing semantics hold. TestRoleSyncTable already routes through dbgen.Organization, which now defaults to rbac.DefaultOrgMemberRoles; the suite passes because none of the table cases claim organization-workspace-access.

Coder Agents on behalf of @Emyrk.

Copy link
Copy Markdown
Member Author

Emyrk commented Jun 2, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@Emyrk Emyrk force-pushed the gateway-accounts/org-default-roles branch from c5afc1c to 4d52fbe Compare June 2, 2026 23:02
@Emyrk Emyrk changed the title feat(coderd): add organizations.default_org_member_roles for Gateway Accounts feat: add organizations.default_org_member_roles for Gateway Accounts Jun 2, 2026
Copy link
Copy Markdown
Member Author

Emyrk commented Jun 2, 2026

Bot review response (4d52fbe8b8):

# Resolution
CRF-1 (P1) role validation Done. Two-layer fix: API rejects non-built-in names (400 Invalid default_org_member_roles entry.); dbauthz UpdateOrganization routes default-role diffs through canAssignRoles so the caller must already be permitted to assign each added/removed role.
CRF-2 (P3) varfunc Done. 12 call sites updated. Doc now also covers the CRF-4 sync invariant.
CRF-3 (P3) takeFirstSlice empty trap Not fixed in this PR. dbgen is test-only; existing dbauthz mocks already seed []string{} explicitly. Revisit if a future test author hits the trap.
CRF-4 (P3) doc invariant Folded into CRF-2.
CRF-5 (P3) service-account test Done. TestGetAuthorizationUserRolesUnionsDefaultOrgMemberRoles now exercises both a regular user and IsServiceAccount: true.
CRF-6 (Nit) endash Fixed permissions–such aspermissions, such as. make gen regenerated queries.sql.go.
CRF-7 (Nit) experiment slug rename Keeping minimum-implicit-member; it names the merge-safety gate accurately.
CRF-8 (Nit) guard comment trim Done, took your suggested wording.
CRF-9 (Nit) codersdk doc rewording Not in this PR; will land with the next codersdk-touching change.
CRF-10 (Note) service accounts share defaults Confirmed intended; CRF-5 test now covers it.
CRF-11 (Note) PR description stale PR description updated.
CRF-12 (Note) empty defaults no-op today Acknowledged staged delivery. Once OrgWorkspaceAccessMemberPerms is removed from organization-member and organization-service-account (follow-up PR), default_org_member_roles = [] becomes the actual shrink mechanism.
Title scope Dropped the coderd scope; changes span coderd/, codersdk/, cli/, docs/, enterprise/, site/.

Coder Agents on behalf of @Emyrk.

@Emyrk Emyrk force-pushed the gateway-accounts/org-default-roles branch 2 times, most recently from 286774c to e6dfeb2 Compare June 2, 2026 23:28
@Emyrk Emyrk force-pushed the gateway-accounts/extract-workspace-access branch 2 times, most recently from fb33230 to eafa7f5 Compare June 3, 2026 13:15
@Emyrk Emyrk force-pushed the gateway-accounts/org-default-roles branch from e6dfeb2 to 85045f1 Compare June 3, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant