Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: promote MinimumImplicitMember experiment to GA - #27472

Merged
Emyrk merged 4 commits into
mainfrom
stevenmasley/delete_minimum_experiment
Aug 3, 2026
Merged

feat: promote MinimumImplicitMember experiment to GA#27472
Emyrk merged 4 commits into
mainfrom
stevenmasley/delete_minimum_experiment

Conversation

@Emyrk

@Emyrk Emyrk commented Jul 23, 2026

Copy link
Copy Markdown
Member

Promotes the minimum-implicit-member experiment to GA and removes it.

What changes

  • The minimum-implicit-member experiment constant, its RoleOptions.MinimumImplicitMember toggle, and the global rbac.MinimumImplicitMember() accessor are deleted. The minimal-member behavior is now the only behavior: organization-member and organization-service-account carry only the floor (read-self records, notifications, and similar) and grant no workspace permissions. Workspace access lives exclusively on the organization-workspace-access role.
  • The experiment gate on customizing default_org_member_roles (PATCH /organizations/{org}) is removed; the built-in-roles-only validation remains.
  • The dashboard's Default Roles section and the implied-roles display on the members page are no longer experiment-gated.
  • Admin docs: new "Default member roles" section in docs/admin/users/organizations.md, cross-linked from groups-roles.md.

Why this is safe for existing deployments

Migration 000516 (shipped earlier) backfilled default_org_member_roles with ['organization-workspace-access'] on every organization. Members therefore keep exactly the effective permissions they had with the experiment off; the workspace elevation flows through the default role instead of being baked into organization-member.

Rollback caveat: rolling back past this release restores the bundled elevation, silently re-granting workspace access to members of organizations that cleared their default roles.

Review

Deep-review R1 findings are addressed in chore: address deep-review findings (copy fixes, read-only Default Roles for viewers, removable overlapping explicit grants, RBAC prose restoration, test de-tautologizing, docs). Point-by-point disposition is in the PR comments.


Generated by Coder Agents on behalf of @Emyrk.

@coderagents

coderagents Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

This PR promotes the minimum-implicit-member experiment to GA: the experiment constant is removed, the elevation now lives on organization-workspace-access (attached via DefaultOrgMemberRoles), and the experiment gate on customizing default_org_member_roles is dropped. Later commits also removed the experiment gate on the Organization Settings roles UI, so this is now GA in the dashboard as well.

Updates Needed

  • docs/install/releases/feature-stages.md - Regenerated; the experiments table now reads "Currently no experimental features are available."
  • docs/reference/api/schemas.md - Regenerated; minimum-implicit-member removed from the Experiment enum.

New Documentation Needed

  • docs/admin/users/organizations.md - Added a "Default member roles" section documenting default_org_member_roles, the organization-workspace-access elevation, the Premium requirement, dashboard/API usage, and limitations.
  • docs/admin/users/groups-roles.md - Added a cross-link to the new "Default member roles" section.

All previously identified documentation gaps are now addressed.


Automated review via Coder Agents

Emyrk added a commit that referenced this pull request Jul 27, 2026
<!-- Authored with Coder Agents on behalf of @Emyrk -->
Finishes the experiment removal on top of #27472:

- Deletes `DeviationRejectedWithoutExperiment` (it asserted the removed
403 gate and would fail), renames the surviving subtests, and drops the
now-pointless `dv.Experiments` setup.
- Adds `TestMemberWorkspaceElevation` as a witness that the
workspace-ops elevation stays off
`organization-member`/`organization-service-account` and lives on
`organization-workspace-access`.
- Frontend: removes both
`experiments.includes("minimum-implicit-member")` sites and the whole
`defaultRolesEnabled` prop plumbing (pages, view, stories) — required
once codegen drops the value from the `Experiment` union.
- `make gen`: apidoc, `typesGenerated.ts`, feature-stages/schemas docs.

Verified: `go build ./...`, `TestPatchOrganizationsByUser` +
`TestMemberWorkspaceElevation` pass, `tsc --noEmit` clean.

<sub>Coder Agents on behalf of @Emyrk.</sub>
@Emyrk
Emyrk requested a review from a team as a code owner July 27, 2026 14:36
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

@nickvigilante nickvigilante left a comment

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.

Docs LGTM

Emyrk added a commit that referenced this pull request Aug 3, 2026
<!-- Authored with Coder Agents on behalf of @Emyrk -->
Finishes the experiment removal on top of #27472:

- Deletes `DeviationRejectedWithoutExperiment` (it asserted the removed
403 gate and would fail), renames the surviving subtests, and drops the
now-pointless `dv.Experiments` setup.
- Adds `TestMemberWorkspaceElevation` as a witness that the
workspace-ops elevation stays off
`organization-member`/`organization-service-account` and lives on
`organization-workspace-access`.
- Frontend: removes both
`experiments.includes("minimum-implicit-member")` sites and the whole
`defaultRolesEnabled` prop plumbing (pages, view, stories) — required
once codegen drops the value from the `Experiment` union.
- `make gen`: apidoc, `typesGenerated.ts`, feature-stages/schemas docs.

Verified: `go build ./...`, `TestPatchOrganizationsByUser` +
`TestMemberWorkspaceElevation` pass, `tsc --noEmit` clean.

<sub>Coder Agents on behalf of @Emyrk.</sub>
@Emyrk
Emyrk force-pushed the stevenmasley/delete_minimum_experiment branch from bc95327 to 406bb94 Compare August 3, 2026 18:23
@Emyrk

Emyrk commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main and cleaned up for merge:

  • Conflict resolution: kept main's experiment list (workspace-capable-licensing, ai-gateway-seat-exclusion, agent-lifecycle-hooks) minus minimum-implicit-member; regenerated coderd/apidoc, typesGenerated.ts, and docs/reference/api/schemas.md.
  • New tests from main: enterprise/coderd/license/usercount_test.go and usercount_bench_test.go used the removed rbac.RoleOptions.MinimumImplicitMember toggle; updated them for the now-unconditional minimal member behavior (all pass).
  • Pre-existing CI failures fixed:
    • coderd/rbac authz tests: subjects that expect workspace access now also carry organization-workspace-access, matching the GA default_org_member_roles.
    • coderd/workspaceconnwatcher: same fix for its member subject.
    • fmt: biome formatting in CustomRolesPageView.tsx.
    • The Storybook failure was a CloudFront infra error, should pass on rerun.

Verified locally: go build ./..., go vet, go test ./coderd/rbac ./coderd/workspaceconnwatcher ./enterprise/coderd/license, tsc --noEmit, and the affected vitest/storybook files all pass. No minimum-implicit-member references remain in the tree.

Generated by Coder Agents on behalf of @Emyrk.

@Emyrk

Emyrk commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-08-03 18:29 UTC by @Emyrk
Spend: $95.82 / $100.00

Review history
  • R1 (2026-08-03): 23 reviewers, 8 Nit, 5 Note, 1 P1, 3 P2, 14 P3, 1 P4, REQUEST_CHANGES. Review

deep-review v0.9.0 | Round 1 | 6e5f02b..406bb94

Last posted: Round 1, 32 findings (1 P1, 3 P2, 14 P3, 1 P4, 8 Nit, 5 Note), REQUEST_CHANGES. Review

Finding inventory

Finding inventory: PR #27472

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Open enterprise/coderd/license/usercount_test.go:32 Six no-op ReloadBuiltinRoles(nil) pairs keep six Postgres subtests serial and the header comment stale R1 Netero Yes
CRF-2 P3 Open codersdk/deployment.go:5429 Empty ExperimentsSafe silently skips/vacuates wildcard-expansion and experiments-gauge coverage; gauge and --experiments='*' also go inert R1 Netero P3, Knov Note, Luffy Note, Hisoka Note Yes
CRF-3 P3 Open docs/install/releases/feature-stages.md:67 GA promotion leaves default_org_member_roles / Gateway Accounts documented nowhere in prose R1 Netero P3, Luffy P3, Mafuuu P3 Yes
CRF-4 Nit Open enterprise/coderd/license/usercount_bench_test.go:42 Orphaned comment survived deletion of the call it explained R1 Netero Nit, Razor Note, Gon Note Yes
CRF-5 Nit Open coderd/rbac/roles.go:1175 Comment says "in the floor" after floor was renamed to memberPerms R1 Netero Yes
CRF-6 Nit Open coderd/rbac/roles_test.go:212 //nolint:tparallel is dead; test no longer has subtests R1 Netero Nit, Takumi Note Yes
CRF-7 Nit Open enterprise/coderd/license/usercount_test.go:111 Subtest name MinimumMember keeps removed experiment's vocabulary R1 Netero Yes
CRF-8 Nit Open site/src/pages/.../CustomRolesPageView.stories.tsx:112 Story DefaultRolesEnabled named after deleted prop R1 Netero Yes
CRF-9 Note Open coderd/rbac/authz_internal_test.go:310 Floor-only member coverage survives the fixture updates; no gap R1 Netero Note, Melody Note Yes
CRF-10 Note Open coderd/rbac/roles.go:1176 Upgrade path covered by migration 000516 backfill + ReconcileSystemRoles R1 Netero Note, Razor Note, Melody Note, Knov Note Yes
CRF-11 P1 Open site/src/pages/.../CustomRolesPageView.tsx:154 Default Roles copy says "new members" while clearing the list revokes workspace access from every existing member on their next request R1 Kite P1, Leorio P1, Pariston P2, Razor P2, Chopper P2, Luffy P2 Yes
CRF-12 P2 Open site/src/pages/.../CustomRolesPageView.tsx:154 Three GA-visible strings explain the feature with the internal term "the floor", defined nowhere a user can reach R1 Gon P2, Leorio P2, Mafuuu P3, Luffy P3, Zoro P3 Yes
CRF-13 P2 Open site/src/pages/.../OrganizationMembersPage.tsx:90 Implied default roles are filtered out of the checkbox list, so an explicit grant that overlaps a default cannot be removed from the members page R1 Nami P2, Bisky P4 Yes
CRF-14 P2 Open coderd/rbac/roles.go:1133 The PR deleted the only in-file explanation that organization-member grants no workspace access and put nothing back R1 Leorio P2, Gon P3 Yes
CRF-15 P2 Open (raised in review body) commit a31ad5d Verification claim covered a -run filter inside a package that was red at that commit R1 Mafu-san P1, Kite Note No
CRF-16 P2 Open (raised in review body) commit a31ad5d message body U+2014 emdash in the commit message, the one place make lint/emdash cannot see R1 Mafu-san No
CRF-17 P3 Open coderd/rbac/roles_test.go:247 New assertion checks OrgWorkspaceAccessMemberPerms against a witness derived from itself, never touching the role the comment names R1 Meruem P3, Leorio P3, Knov P3, Melody Nit, Gon P2 Yes
CRF-18 P3 Open coderd/rbac/roles.go:227 OrgWorkspaceAccessMemberPerms stays exported with only in-package callers, so the elevation can be re-bundled into any role R1 Knov Yes
CRF-19 P3 Open coderd/workspaceconnwatcher/watcher_test.go:494 The implicit member role set has five hand-maintained implementations and no Go owner R1 Ryosuke P3, Zoro P3, Kite Nit Yes
CRF-20 P3 Open coderd/coderd.go:387 ReloadBuiltinRoles guard hand-mirrors the RoleOptions field set; a new field can be silently ignored R1 Meruem Yes
CRF-21 P3 Open codersdk/organizations.go:107 CreateOrganizationRequest cannot set default_org_member_roles, so a gateway org exists with the elevation between create and PATCH R1 Mafuuu Yes
CRF-22 P3 Open enterprise/coderd/organizations.go:65 Nothing narrows which built-ins may be defaults, and the members API reports only explicit roles, so an org-wide organization-admin grant is invisible to API and audit consumers R1 Kurapika P3, Luffy P3, Nami Note Yes
CRF-23 P3 Open enterprise/coderd/organizations.go:65 An emptied default_org_member_roles plus a lapsed license leaves no org-wide path back; every future member joins floor-only R1 Ryosuke P3, Knov Note Yes
CRF-24 P3 Open site/src/pages/.../CustomRolesPageView.tsx:63 showDefaultRoles' second conjunct cannot be false in production, so half a permission gate is unfalsifiable R1 Bisky P3, Zoro Nit Yes
CRF-25 P3 Open site/src/pages/.../CustomRolesPageView.tsx:63 A viewer with viewOrgRoles but not editSettings cannot see the org default roles at all, now that they decide member workspace access R1 Nami Yes
CRF-26 P3 Open site/src/pages/.../CustomRolesPageView.tsx:161 Edit button is live before the org roles query resolves, producing a dialog with a title, a Confirm button and nothing between R1 Nami Yes
CRF-27 P3 Open coderd/rbac/roles.go:344 The diff deletes the only definition of "floor" while nine sites keep using it as a term of art R1 Gon P3, Zoro Nit, Leorio Nit Yes
CRF-28 P2 Open (raised in review body) PR #27472 description Empty PR body for a change that alters the default permission set of every deployment R1 Leorio No
CRF-29 Nit Open coderd/rbac/roles_test.go:206 Test doc and two inline comments restate the assertions beside them; the test name states the opposite of what it proves R1 Gon P2 (orchestrator Nit) Yes
CRF-30 Nit Open coderd/rbac/roles_test.go:218 Hand-rolled hasResource loop where slices.ContainsFunc fits and the file already imports slices R1 Ging-Go Yes
CRF-31 P4 Open coderd/rbac/roles.go:1066 sort.Slice in Permissions, the function both role builders now route through directly R1 Ging-Go Yes
CRF-32 Nit Open coderd/rbac/roles_test.go:235 Seventh no-op ReloadBuiltinRoles(nil) pair, created by this diff; same class as CRF-1 and CRF-6 R1 Bisky Nit, Hisoka Nit, Takumi Note Yes
CRF-33 Note Open coderd/rbac/roles.go:1173 Rolling back past this release silently restores workspace access to gateway-account members R1 Mafuuu Yes
CRF-34 Note Open coderd/rbac/rolestore/rolestore.go:71 Expand silently drops unresolvable role names, and that string is now the only grant path R1 Meruem Yes
CRF-35 Note Open coderd/rbac/roles.go:221 DefaultOrgMemberRoles() applies only at org creation, so deployment default and per-org data diverge permanently R1 Pariston Yes

Contested and acknowledged

None.

Severity decisions (drop/downgrade gate)

Every downgrade below has its keep-argument written first, per the gate.

  • CRF-15, Mafu-san P1 to P2. Keep at P1: the change deletes permissions from the most widely held role in the system, the claimed verification did not cover the one package that catches it, and if the panel had trusted the claim a permission regression could have shipped on it. Downgrade reason: the defect the missing check would have caught is fixed at head (six reviewers independently ran go test ./coderd/rbac/ green), so the actionable remainder is the commit history and the verification standard, not the code. P2, and it stays a merge-blocking item for the commit body.
  • CRF-12, Gon/Leorio P2 held against Mafuuu/Luffy/Zoro P3. Higher severity wins on disagreement, and the P2 arguments are the more specific: both name all three strings, quote them, and show the PR deleted the only definition of the term. P2.
  • CRF-17, Meruem/Leorio/Knov P3 held against Melody Nit and three Notes. Keep at P3: the assertion is the load-bearing line of the test whose stated job is proving the elevation is reachable, and it cannot fail. Not downgraded to Nit because the consequence is a line that reads like coverage and is not; the real coverage lives in TestRolePermissions:316 and httpmw/authorize_test.go:71, which is mitigation, not defensibility.
  • CRF-23, Ryosuke P3 held. Knov empirically corrected Ryosuke's stated remedy: PUT /organizations/{org}/members/{user}/roles is AGPL-registered, so per-member repair exists and "direct SQL or rollback" is wrong. That argues for P4. Keep-argument that wins: with the license lapsed the org-wide default is unwritable, so every future member joins floor-only forever and must be fixed by hand. That is a durable trap, not an annoyance. P3, with the correction stated in the comment.
  • CRF-29, Gon P2 to Nit. Keep at P2: five of the eight comments this diff touches are defective, and after GA these comments are the only remaining explanation of a permission model that just changed. Downgrade reason: severity is set by consequence, and the consequence of a comment restating the require message next to it is reader time, not behavior. Netero rated the same class Nit (CRF-4). The comments that do carry a behavioral consequence are filed separately at their real severity: CRF-14 (missing explanation), CRF-17 (comment claims what the assertion does not check), CRF-27 (undefined vocabulary). Nit.
  • CRF-2 held at P3, not raised to P2. Keep-argument for P2: an operator's coderd_experiments dashboard silently loses every series and --experiments='*' becomes a documented no-op, with no release note. Reason it stays P3: both are the correct consequence of a deployment having zero experiments and both self-restore when the next experiment lands. The durable defect is the invisible coverage loss, where a skip reads as a pass. The two production-side consequences travel in the same comment.
  • CRF-22 held at P3, not raised to P2. Keep-argument for P2: an operator enumerating org admins through GET /organizations/{org}/members gets a false negative on an authorization-relevant state, and GA makes that the default surface. Reason it stays P3: two reviewers independently rated it P3, the write is authorized through canAssignRoles and audited via enterprise/audit/table.go:361, and the per-member dialog does render the implied roles. P3, with the false-negative consequence stated.

Cross-check: contradictions and interactions resolved

  • Bisky vs Takumi on parallelizing TestMemberWorkspaceElevation. Bisky says nothing the test reads is global, so it can be parallel; Takumi warns that dropping paralleltest would race TestOwnerExec's reload. Both hold, in sequence. Verified in the orchestrator worktree: OrgMemberPermissions (roles.go:1176) and OrgServiceAccountPermissions are literal maps plus org.ShareableWorkspaceOwners branches with no atomic load, so the test reads no global; its only global interaction is the ReloadBuiltinRoles(nil) write at :235. Delete the write first, then t.Parallel() is safe. Keep paralleltest if the reload stays. Stated this way in CRF-32.
  • Knov vs Ryosuke on the license-lapse remedy. Resolved in the severity notes above; Knov's specific route citation corrects Ryosuke's claim without killing the finding.
  • Convergence traced, not merged at max severity. CRF-11, CRF-12, CRF-13, CRF-22, CRF-24, CRF-25 and CRF-26 all land on the same newly exposed surface: the Default Roles editor and the member-roles dialog, which the experiment gate previously kept away from operators. The chain is worse than any single entry: the copy misstates the blast radius (CRF-11) in a term nobody can resolve (CRF-12), the dialog offers organization-admin as an org-wide default (CRF-22), an overlapping explicit grant then becomes unremovable (CRF-13), and read-only viewers cannot see any of it (CRF-25). Filed separately because the fixes are independent; the interaction is stated in the review body.
  • Schema-level side effects. No migration in this diff. Migration 000516_org_default_member_roles.up.sql is pre-existing and was traced by six reviewers: unconditional backfill of ARRAY['organization-workspace-access'] over every existing org, then NOT NULL with no SQL default. No CASCADE, trigger, or defaulting side effect. The user-facing consequence is the one CRF-10 records: existing members keep workspace access across the upgrade.
  • Non-diff findings kept. CRF-11, CRF-12, CRF-13, CRF-14, CRF-22, CRF-23, CRF-25, CRF-27, CRF-31, CRF-33, CRF-34, CRF-35 sit outside the diff hunks. All are valid at their reviewer-reported coordinates; the exposure this PR creates is what makes them findings now.
  • Deferral-language sweep. Grepped reviewer output for "for v1", "first pass", "can defer", "not urgent", "future reference": Mafuuu's note on enterprise/coderd/organizations.go:63 quotes the code's own "A future change can extend this" comment, and Zoro's memberPerms duplication note says "worth a follow-up decision". Re-evaluated both without the deferral framing: the first is a shipped-at-GA limitation with an honest 400, which stays a Note; the second is a pre-existing duplication this PR does not create, which stays a Note. Neither is a smuggled follow-up assumption on new code.
  • Reachability sweep. No finding was downgraded for lack of a current caller. CRF-18 is the inverse case and is kept at P3 precisely because an exported perm-set helper is externally reachable.

Round log

Round 1

Netero first pass: 3 P3, 5 Nit, 2 Note, no P0-P2, so the panel ran in the same round. Law not spawned (effective additions 61, below the 1000 threshold). Panel: bisky, hisoka, mafu-san, mafuuu, pariston, komugi, kurapika, razor, kite, knov, ging-go, ging-ts, ging-react, gon, leorio, nami, ryosuke, chopper, meruem, melody, luffy, plus wildcards zoro and takumi. Totals after cross-check: 1 P1, 6 P2, 14 P3, 1 P4, 8 Nit, 5 Note. Three process findings raised in the review body. Reviewed against 6e5f02b..406bb94.

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
Komugi flake/determinism
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.

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

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.

This is a good deletion. Net -138 lines, the minimumImplicitMember atomic.Bool mirror is gone from object.go, and OrgMemberPermissions / OrgServiceAccountPermissions are now pure functions of OrgSettings instead of reading process-global state on every call. RoleOptions is left describing only deploy-time deny toggles, and the startup ordering dependency between ReloadBuiltinRoles and ReconcileSystemRoles disappeared with the global.

The panel independently traced the two things that could have broken, and neither did. Cross-role negation: the workspace:share deny stays on organization-member while the positive workspace grant moved to organization-workspace-access, and check_all_org_permissions in policy.rego:120-133 votes across all of a subject's roles at a level, so the deny still overrides the allow in the other role. Upgrade path: migration 000516 unconditionally backfills every existing org, the column is NOT NULL with no SQL default so no insert path can silently omit it, ReconcileSystemRoles runs after ReloadBuiltinRoles in the same New(), and both role-union queries include the column for service accounts too. Deployments still passing --experiments=minimum-implicit-member warn and boot. Repo-wide greps for the removed identifier return nothing.

Round 1 count: 1 P1, 6 P2, 14 P3, 1 P4, 8 Nit, 5 Note. Three of the P2s are process findings and are below rather than inline.

The P1 and most of the P2s are not about the RBAC edit. They are about the surface the experiment flag was hiding. Seven findings land on the Default Roles editor and the member-roles dialog, and the chain is worse than any single one: the copy misstates the blast radius (CRF-11) in a term no operator can resolve (CRF-12), the dialog offers organization-admin as an org-wide default (CRF-22), an overlapping explicit grant then becomes unremovable from the members page (CRF-13), and a viewer without editSettings cannot see any of it (CRF-25). They are filed separately because the fixes are independent, but they ship together.

Process, three items:

CRF-15 (P2, verification scope). Commit a31ad5de1f states "Verified: go build ./..., TestPatchOrganizationsByUser + TestMemberWorkspaceElevation pass, tsc --noEmit clean" while go test ./coderd/rbac/ was red at that commit. Mafu-san ran both directions: at a31ad5de1f, TestAuthorizeDomain/Member/Set0/Case2 fails with rbac: forbidden on ssh and stop, package FAIL in 1.09s; at the merge base 6e5f02bfbe the same run is ok. The named test lives in the same package as the failing one, so the claim reports the result of a -run filter inside a red package as verification of the change. The fixtures that fix it land one commit later, so head is green. The standard for the rest of this PR: when a change edits a coderd/rbac permission set, the minimum evidence is go test ./coderd/rbac/ with no -run filter. Related, Kite: taken alone, 0e36260b2c deletes TestMinimumImplicitMember and ships the unconditional elevation removal with nothing asserting it, and a31ad5de1f adds TestMemberWorkspaceElevation back. A squash merge makes that moot; a merge commit does not.

CRF-16 (P2, emdash in commit metadata). a31ad5de1f's message body contains a U+2014 in "...(pages, view, stories) — required once codegen drops the value...". AGENTS.md forbids U+2014 outright, and scripts/check_emdash.sh scans git ls-files, so it has no reach into commit messages: make lint/emdash is clean. Rewrite the body before merge. The rule applies to commit and PR text, not only tracked files.

CRF-28 (P2, empty PR description). The body is the empty string for a change that permanently alters the default permission set of every deployment on upgrade. The permanent record becomes one line naming a Go identifier: it does not say that organization-member stops granting workspace access, that the elevation moves to organization-workspace-access, that 000516 plus ReconcileSystemRoles covers the upgrade, or that org admins can now clear default_org_member_roles without a flag. Write it. While there, the release notes should name the removed experiment value: operators who set it keep logging "ignoring unknown experiment" on every restart with nothing telling them to remove it. And Leorio on the subject line: "promote MinimumImplicitMember experiment to GA" names an internal Go field and describes the process, not the effect.

Two things the panel tried hard to break and could not, recorded so they are not re-litigated: no production code path builds a member subject outside the SQL union (coderdtest.go:933-943 and idpsync/role.go:182-204 already mirror it explicitly), and the removed 403 left no ungated path, because PATCH /organizations/{organization} sits behind RequireFeatureMW(FeatureMultipleOrganizations), dbauthz.UpdateOrganization runs canAssignRoles over the delta, the handler still rejects non-built-in names, and enterprise/audit/table.go:361 tracks the column. Kite's praise on TestMemberWorkspaceElevation stands too: replacing a toggle test with an invariant test, and updating the fixtures to keep their effective permissions equal to what a real member gets rather than weakening the assertions, is the harder and correct direction.

Hisoka, on the whole thing: "I came looking for a fight. ♦ ... Rating: ★ curiosity only. I wanted a ★★★★. You didn't give me one."


site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx:154

P1 [CRF-11] The Default Roles copy tells the admin an empty selection affects "new members", but clearing the list revokes workspace access from every existing member of the org on their next request. (Kite P1, Leorio P1, Pariston P2, Razor P2, Chopper P2, Luffy P2)

Six reviewers reached this independently. The section contradicts itself in one paragraph, and the wrong half is the one an admin reads while deciding whether to confirm:

  • CustomRolesPageView.tsx:154: "Roles attached to every member of this organization. An empty selection limits new members to the floor permissions only."
  • CustomRolesPageView.tsx:169: "No default roles. New members receive only the floor."
  • DefaultRolesDialog.tsx:80: "These roles are attached to every member of this organization. Use an empty selection to grant new members only the floor."

The behavior is settled, not inferred. Pariston: "TestGetAuthorizationUserRolesUnionsDefaultOrgMemberRoles at coderd/database/querier_test.go:3301 shrinks an org's defaults to [] and asserts the scoped organization-workspace-access role disappears from an already-existing member's effective roles, and from an existing service account's." users.sql:637 unions the column at request time, and 000516_org_default_member_roles.up.sql states "changes propagate to all members on the next request".

Kite traced how far the loss reaches: "The site member role's user permissions are allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ...) (coderd/rbac/roles.go:437), so a user's access to their own workspaces comes only from the org elevation that organization-workspace-access now carries exclusively." Luffy states the outcome: "Their workspaces keep running and they can't touch them."

This PR is what makes the copy load-bearing. showDefaultRoles dropped defaultRolesEnabled (:63) and the 403 on deviation was deleted from enterprise/coderd/organizations.go, so every deployment entitled for multiple_organizations now has a one-click path from a settings page to revoking read, ssh, start, stop and delete on running workspaces for every member of an org.

P1 rather than P2, against the four reviewers who said P2: their reason for P2 was that an attentive admin may notice the contradiction, which is a mitigation that depends on the reader catching the page lying to them. The consequence does not: it is an org-wide authorization revocation triggered from a settings control that describes it as a forward-only default, with no confirmation of scope. Reversible by re-adding the role, but only for someone who already knows the roles are unioned per request.

Fix all three strings, not one. Leorio's draft for the dialog: "These roles are added to every member of this organization, including existing members, and take effect on their next request. Removing organization-workspace-access revokes workspace create, build, ssh, and delete for every member who is not granted it directly." Kite's stronger option, worth taking: have DefaultRolesDialog warn on confirm when the submitted set drops organization-workspace-access while the org has members, naming the count. The copy fix removes the false statement; the warning is what makes the blast radius visible at the moment of the click.

🤖

site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx:154

P2 [CRF-12] The same three strings explain the feature with "the floor", an rbac package term that is defined nowhere an operator can reach, and this PR deleted the last place in the tree that defined it. (Gon P2, Leorio P2, Mafuuu P3, Luffy P3, Zoro P3)

Separate from CRF-11: fixing the "new members" claim does not remove the jargon, and removing the jargon does not fix the claim.

Leorio: "Grep the whole repo: 'floor' in this sense is defined nowhere. This PR deleted the only two places that explained it, the RoleOptions.MinimumImplicitMember doc comment and the elevation block in OrgMemberPermissions. The UI now uses a term whose definition the same commit removed." Luffy grepped the docs for somewhere to send the reader: "the only hits are the generated API reference (docs/reference/api/*.md), and 'Gateway Account' appears nowhere in docs/."

The consequence is the question the copy fails to answer. Mafuuu: "an org admin about to strip workspace access from every member of an organization is told the outcome in a word that has no referent they can resolve."

P2 over the three P3s: the P2 arguments are the more specific ones, naming all three strings, quoting them, and showing that the definition left the tree in this commit. Note this is not CRF-3 with different words. CRF-3 is the missing prose page; this is the shipped dialog standing in for it, and Zoro is right that even with a doc, "the floor" is the wrong word in a settings page.

Zoro's concrete replacements, for :154: "Roles attached to every member of this organization. With an empty selection, new members can read the organization and manage their own account, but cannot create or use workspaces." And for :169: "No default roles. New members cannot create or use workspaces." Fold in CRF-11's correction about existing members and both findings close together.

🤖

coderd/rbac/roles.go:1133

P2 [CRF-14] The PR deleted the only in-file explanation of where member workspace permissions went, and put nothing in its place. (Leorio P2, Gon P3)

Leorio: "Before this PR, a reader who opened OrgMemberPermissions hit an eleven-line comment naming OrgWorkspaceAccessMemberPerms, organization-workspace-access, and default_org_member_roles. The PR removed it (-roles.go:1199-1211) and put nothing in its place." What remains says only that the permissions vary with workspace sharing settings.

The consequence is a comprehension trap in the most widely held role in the system: "The next person who gets a bug report saying 'members can't create workspaces' reads this function, sees no ResourceWorkspace grant, and has no idea whether that is the design or the bug."

This is the substantive half of the comment findings, distinct from CRF-5 (stale "in the floor" wording) and CRF-29 (comments restating their assertions). Leorio's draft:

// OrgMemberPermissions returns the permissions for the organization-member
// system role, which can vary based on the organization's workspace sharing
// settings.
//
// Workspace permissions are deliberately not included here. They live on the
// organization-workspace-access role (OrgWorkspaceAccessMemberPerms), which
// DefaultOrgMemberRoles attaches to every new organization. An org that clears
// default_org_member_roles leaves its members with this set only.

OrgServiceAccountPermissions has the same hole and needs the same sentence.

🤖

coderd/rbac/roles.go:227

P3 [CRF-18] OrgWorkspaceAccessMemberPerms stays exported although the only production caller left is the role definition in the same package, so the elevation this PR just unbundled can be re-bundled into any other role. (Knov)

Knov: "Before this PR the function had three production callers: the orgWorkspaceAccess role at coderd/rbac/roles.go:720, and the two slices.Concat(elevation, floor) sites in OrgMemberPermissions and OrgServiceAccountPermissions. After the diff, grep -rn 'OrgWorkspaceAccessMemberPerms' coderd enterprise cli --include='*.go' | grep -v _test.go returns only the definition and line 720, both inside package rbac."

The consequence is the misuse the promotion exists to remove: "a future caller writing Member: slices.Concat(rbac.OrgWorkspaceAccessMemberPerms(), somethingElse) restores implicit workspace access without touching organization-member, and nothing fails."

Ordered with CRF-17, because it is one change: fix the test to read the role through RoleByName first, then unexport the helper to orgWorkspaceAccessMemberPerms. The elevation becomes reachable only through the role identifier, and the package boundary states the invariant the PR title only asserts in prose. Kept at P3 rather than treated as dead-code cleanup: an exported perm-set helper is reachable by any package, and the severity is set by what happens when someone uses it, not by there being no caller today.

🤖

codersdk/organizations.go:107

P3 [CRF-21] GA ships no way to create a Gateway Accounts organization atomically: default_org_member_roles is settable only on PATCH, so every new org is born granting workspace access to all members. (Mafuuu)

Mafuuu: "CreateOrganizationRequest (codersdk/organizations.go:107-113) has no default_org_member_roles field, and enterprise/coderd/organizations.go:316 hardcodes DefaultOrgMemberRoles: rbac.DefaultOrgMemberRoles() on insert. An operator who wants an org whose members must never hold workspace permissions has to do create-then-PATCH, and the org exists with the elevation in between."

The scenario: an operator provisions a gateway org from Terraform or a script, POST returns, and before the follow-up PATCH lands an IdP-sync-eligible user logs in and is enrolled. That user can create and exec into workspaces in an org created specifically to deny it. The subsequent PATCH closes the door but does not revoke what was already reachable.

P3 because the window is short and needs a login inside it. It is a finding rather than a note because GA is the moment this API shape stops being provisional: CreateOrganizationRequest gains a *[]string now or the gap is permanent. The fix is the field plus threading it into the existing InsertOrganizationParams, and the validation loop at enterprise/coderd/organizations.go:68-81 is already factored to be reused.

Mafuuu states the limit of the evidence: derived from reading the request shapes, the insert call site and the request-time union at users.sql:625, not from constructing the race.

🤖

site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx:161

P3 [CRF-26] Opening "Edit default roles" before the org roles query resolves gives the admin a dialog with a title, a Confirm button, and nothing in between. (Nami)

Nami: "The Edit button is disabled only for isUpdatingDefaultRoles || !defaultRolesEntitled, so it is live while availableOrgRoles is still undefined. DefaultRolesDialog.tsx:39 coerces that to [], and RoleSelector.tsx:67 returns null when selectableRoles is empty, so the dialog body is empty. Confirm still submits [...selected], which equals the current roles, so nothing is destroyed; the admin just gets a headless dialog and no explanation."

Nothing is lost, which is why this is P3 and not higher, but the empty dialog is indistinguishable from Bisky's saturation case in CRF-13, so an admin who hits either reports the same symptom and the two have different causes.

RoleSelector already accepts loading and error, and CustomRolesPage already holds organizationRolesQuery. Either disable the button until availableOrgRoles is defined, or thread the query's loading and error state through to the selector. Threading it also covers the load failure, which currently renders the same empty dialog.

🤖

enterprise/coderd/license/usercount_test.go:32

P3 [CRF-1] The file still declares that subtests toggle global builtin roles and keeps them serial, but after this PR every call is ReloadBuiltinRoles(nil). (Netero)

Netero: "All six sites (:69/:70, :115/:116, :157/:158, :188/:189, :231/:232, :256/:257) now pass nil on both the set and the cleanup, and grep -rn ReloadBuiltinRoles enterprise/coderd/license/ shows no caller in the package that passes options any more (the bench file's call was deleted in this diff). So the reset is a no-op and nothing in the package mutates the globals."

Two consequences: "the header comment at :32-33 tells the next reader that role globals vary per subtest when they do not, and the //nolint:tparallel,paralleltest at :35 permanently serializes six subtests that each stand up a Postgres database."

Fix: delete the six reload/cleanup pairs, the :32-33 paragraph and the nolint, then add t.Parallel() to the parent and subtests. CRF-32 is the seventh pair, in roles_test.go, created by this same diff; treat them as one change. Razor checked the assertions do not go vacuous: every subtest that depends on floor-only members calls emptyDefaultRoles, so the no-op reloads leave nothing hanging.

🤖

coderd/rbac/roles_test.go:218

Nit [CRF-30] Hand-rolled hasResource loop where slices.ContainsFunc does the job, in a file that already uses that pattern. (Ging-Go)

Ging: "The PR rewrote this test body and kept the six-line closure. permissionGranted at coderd/rbac/roles_test.go:57 in the same file is the established pattern":

hasResource := func(perms []rbac.Permission, resource string) bool {
    return slices.ContainsFunc(perms, func(p rbac.Permission) bool {
        return p.ResourceType == resource && !p.Negate
    })
}

slices is already imported here. Keep the !p.Negate filter exactly as is: Chopper and Kite both verified it is load-bearing, because OrgMemberPermissions appends a negated ResourceWorkspace/ActionShare permission when ShareableWorkspaceOwners != Everyone, and without the filter the require.False would flip for any other org setting.

🤖

coderd/rbac/roles.go:1066

P4 [CRF-31] sort.Slice in Permissions, the function this PR now routes both OrgMemberPermissions and OrgServiceAccountPermissions through directly. (Ging-Go)

Ging: "slices.SortFunc (1.21) is the replacement; the sort package docs point at it ('more ergonomic and runs faster', no reflection-based swapper). Same instability semantics, so the sort result is unchanged. Notable only because this PR deleted \"slices\" from this file's imports (line 6 still imports sort), leaving the file on the older API for its one sort."

Outside the diff, so P4. Worth knowing the instability is deliberate downstream: Komugi notes ReconcileSystemRole compares with the set-based rbac.PermissionsEqual, precisely so unstable tie order among a resource's actions cannot make every replica rewrite the system-role row at startup.

🤖

coderd/rbac/rolestore/rolestore.go:71

Note [CRF-34] A stale name in default_org_member_roles degrades quietly rather than failing loudly, and that string is now the only grant path. (Meruem)

Meruem: "Expand silently drops roles that no longer resolve. If a future release renames or removes organization-workspace-access, orgs storing that name lose the grant with no error surfaced anywhere. That is the failure mode a string-keyed cross-version reference buys, and it is now the only mechanism granting workspace access."

Not this PR's to fix. The GA promotion is what makes it permanent, which is why it is on the record.

🤖

coderd/rbac/roles.go:221

Note [CRF-35] DefaultOrgMemberRoles() is applied only at org creation, so the deployment default and the per-org data diverge permanently. (Pariston)

Pariston: "enterprise/coderd/organizations.go:316 passes it on insert and migration 000516 backfilled it once. There is no reconciler that reapplies it. Any future addition to the deployment-wide default will need its own backfill migration to reach existing orgs."

This is the base design that shipped under the experiment, not something this PR introduces, and no change is requested. It is the durable cost of moving the elevation from code into per-org data, and the reason the next change to DefaultOrgMemberRoles() is a migration, not a constant edit.

🤖

🤖 This review was automatically generated with Coder Agents.

organization?.default_org_member_roles,
organizationRolesQuery.data,
]);
}, [organization?.default_org_member_roles, organizationRolesQuery.data]);

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.

P2 [CRF-13] Implied default roles are filtered out of the checkbox list, so any role that sits in default_org_member_roles and is also granted explicitly to a member becomes unremovable from the members page. (Nami P2, Bisky P4)

Dropping the experiment guard makes defaultMemberImpliedRoles resolve on every deployment, and 000516 backfills ['organization-workspace-access'] into every existing org, so the list is non-empty everywhere. I verified the mechanism in the orchestrator worktree. Nami's trace:

site/src/modules/roles/RoleSelector.tsx:59-63 filters implied role names out of availableRoles before grouping, so implied roles appear in neither selectableRoles nor advancedRoles. [...] RoleSelectorDialog.tsx:68 seeds selectedRoles from the member's explicit roles and line 101 submits [...selectedRoles] verbatim. A hidden explicit grant is therefore re-submitted on every save. [...] coderd/members.go:442 passes params.Roles straight to UpdateMemberRoles with no dedupe against the org defaults, so the grant persists silently.

The filter at :60 is unconditional on selectedRoles, so an explicitly held role loses its checkbox purely because the org also grants it by default. The reachable sequence is all UI: drop the role from the defaults, grant it explicitly to one member, re-add it to the defaults. That member's row shows the role, their dialog has no way to clear it, and every save re-sends it. Same shape for organization-admin: while it sits in the default list, an explicitly granted org admin cannot be demoted from the members page.

Bisky reports the coverage half at P4: WithAdditionalImpliedRoles (RoleSelector.stories.tsx:92-100) renders the path with no play, and passes implied roles that are not in selectedRoles, so the overlap is untested and "a regression that puts organization-workspace-access back in the checkbox list changes pixels, not test results". Bisky also names a second reachable state on the same filter: when the defaults cover every assignable org role, selectableRoles is empty and RoleSelector returns null at :67, which an admin reports as "the role dialog is empty".

Nami's fix, in the page rather than the shared selector: pass only the implied roles the member does not already hold explicitly, so an explicit grant keeps its checkbox. If you prefer the selector, keep a role in selectableRoles when selectedRoles.has(role.name) and caption it. Either way add the play assertion for the overlap, because without it the fix has no witness.

Nami read the four files and did not execute a probe (the browser-mode run never connected), so treat the mechanism as traced, not reproduced. I confirmed RoleSelector.tsx:58-69 matches the description.

🤖

Comment thread coderd/rbac/roles_test.go Outdated
require.True(t, hasResource(sa, floorWitness), "organization-service-account should still include the floor")
})
// The elevation is available via organization-workspace-access.
require.True(t, hasResource(rbac.OrgWorkspaceAccessMemberPerms(), elevationWitness), "organization-workspace-access should carry the 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.

P3 [CRF-17] The new assertion checks OrgWorkspaceAccessMemberPerms against a witness defined as "whatever that helper grants", so it cannot fail and never touches the role the comment names. (Meruem P3, Leorio P3, Knov P3, Melody Nit, Gon P2)

Five reviewers landed on this line from different directions. Meruem: "elevationWitness is ResourceWorkspace.Type, chosen at line 230 because OrgWorkspaceAccessMemberPerms grants it. Line 247 then asserts OrgWorkspaceAccessMemberPerms() contains it. The assertion cannot fail unless someone changes both the helper and the witness, and it never touches the organization-workspace-access role."

The part left unverified is exactly the contract the test's own doc states. Meruem: "if the orgWorkspaceAccess builder at coderd/rbac/roles.go:712 stopped wiring OrgWorkspaceAccessMemberPerms() into ByOrgID[org].Member, this test would still pass while every member in every org lost workspace access." Gon separately flags the comment above it as pure restatement; it is worse than that, because it promises something the line does not check.

Assert through the role, which is what default_org_member_roles actually names:

orgID := uuid.New()
wsAccess := must(rbac.RoleByName(rbac.ScopedRoleOrgWorkspaceAccess(orgID)))
require.True(t, hasResource(wsAccess.ByOrgID[orgID.String()].Member, elevationWitness))

Melody adds the second half worth asserting here: require.Contains(t, rbac.DefaultOrgMemberRoles(), "organization-workspace-access").

Held at P3 against Melody's Nit and three Notes. Coverage does exist elsewhere, and the reviewers found it rather than assuming: TestRolePermissions drives orgWorkspaceAccessUser through the authorize matrix (roles_test.go:316), httpmw/authorize_test.go:58,71 seeds an org with rbac.DefaultOrgMemberRoles() and asserts the extracted actor holds the scoped role, and usercount_test.go "DefaultRolesParity" counts a role-less member as workspace-capable purely through the request-time union. That is mitigation, not defensibility. Bisky put it best: "there is no coverage hole; there is a line that reads like coverage and isn't", in the one test whose stated job is proving the elevation is reachable.

🤖

}
// organization-workspace-access carries the workspace perms; the
// organization-member role alone is only the floor.
wsAccess, err := rbac.RoleByName(rbac.ScopedRoleOrgWorkspaceAccess(orgID))

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-19] "What roles does an org member implicitly hold" now has five hand-maintained implementations and no Go owner; this PR is the bill for that. (Ryosuke P3, Zoro P3, Kite Nit)

Ryosuke enumerated them: GetAuthorizationUserRoles and GetActiveUsersAuthorizationRoles (parity enforced by TestGetActiveUsersAuthorizationRolesParity), coderd/idpsync/role.go:192-196, coderdtest.go:932-944, and now this helper. "The SQL pair has an owner. The three Go copies do not. [...] the elevation moved, and the fix was to hand-patch one more copy plus seven fixtures in authz_internal_test.go."

The failure mode is asymmetric, which is what makes it worth more than a style note: "A hand-rolled subject that is too weak turns a permit-assertion red, which CI catches. A deny-assertion keeps passing for the wrong reason, which CI does not catch, and the test then proves nothing about the role model it claims to test. watcher_test.go is a watcher permission test, so it holds both kinds."

Zoro found that the codebase already states the obligation these copies violate, at coderdtest.go:933-943: "The implicit role set (organization-member plus the org's default_org_member_roles) is unioned at request time by GetAuthorizationUserRoles. Subjects built directly here bypass that SQL union, so mirror it explicitly." That call site mirrors it generically by iterating org.DefaultOrgMemberRoles; the eight new fixtures name the role literally.

Two scopes, and Zoro argues the tradeoff honestly. Minimum, in this fixture, which is the one claiming to model "a basic org member" (Kite and Zoro converge on the same loop):

roles := rbac.Roles{memberRole, orgMember}
for _, name := range rbac.DefaultOrgMemberRoles() {
    r, err := rbac.RoleByName(rbac.RoleIdentifier{Name: name, OrganizationID: orgID})
    if err != nil {
        panic(err)
    }
    roles = append(roles, r)
}

Leave authz_internal_test.go explicit: Zoro notes that :802 and :1306 deliberately keep floor-only subjects, and a helper there would hide which subjects hold the elevation. Ryosuke's structural alternative, preserved at his severity rather than trimmed to the fixture fix: name the set once as rbac.ImpliedOrgMemberRoles(org) and have idpsync, coderdtest.AuthzUserSubject and this helper call it, so the next promotion like this one touches one function instead of four files.

🤖

Comment thread coderd/coderd.go Outdated
)

if bool(options.DeploymentValues.DisableOwnerWorkspaceExec) || bool(options.DeploymentValues.DisableWorkspaceSharing) || bool(options.DeploymentValues.DisableChatSharing) || experiments.Enabled(codersdk.ExperimentMinimumImplicitMember) {
if bool(options.DeploymentValues.DisableOwnerWorkspaceExec) || bool(options.DeploymentValues.DisableWorkspaceSharing) || bool(options.DeploymentValues.DisableChatSharing) {

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-20] The guard deciding whether to call ReloadBuiltinRoles is a hand-maintained mirror of the RoleOptions field set, so the two can drift silently. (Meruem)

Meruem: "Nothing ties the enumeration to the struct: adding a fourth option and populating it in the literal below, while forgetting the || clause, means the option is silently ignored on any deployment that sets it alone. Permissions are then wrong with no error and no log line. This diff is itself evidence that the drift surface is exercised, since removing the experiment required editing the condition and the literal in lockstep."

RoleOptions is a comparable all-bool struct, so the guard can be derived:

roleOpts := rbac.RoleOptions{
    NoOwnerWorkspaceExec: bool(options.DeploymentValues.DisableOwnerWorkspaceExec),
    NoWorkspaceSharing:   bool(options.DeploymentValues.DisableWorkspaceSharing),
    NoChatSharing:        bool(options.DeploymentValues.DisableChatSharing),
}
if roleOpts != (rbac.RoleOptions{}) {
    rbac.ReloadBuiltinRoles(&roleOpts)
}

Meruem checked that this preserves the guard's actual purpose: builtInRoles is an atomic.Pointer (roles.go:324), so the guard is not there for memory safety, only to avoid republishing the global from every coderd.New. Takumi independently notes that narrowing this trigger is a real improvement in a process hosting more than one coderd instance, where one instance's startup rewrites roles under another's live traffic. Deriving it keeps that benefit and makes a dropped field impossible.

🤖

return
}

// Deviations from rbac.DefaultOrgMemberRoles require the

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-22] Nothing narrows which built-in roles may become org-wide defaults, and the members API reports only explicit roles, so an org where every member holds organization-admin looks like an org full of plain members. (Kurapika P3, Luffy P3, Nami Note)

Kurapika: "The validation that survives only rejects non-built-in names (enterprise/coderd/organizations.go:69-83, rbac.RoleByName). Every org-scoped built-in passes, including organization-admin, organization-user-admin, organization-auditor."

The write itself is fine, and both reviewers checked before concluding: dbauthz.UpdateOrganization diffs the sets and runs canAssignRoles, enterprise/audit/table.go:361 tracks the column, and site-scoped names cannot be smuggled in because RoleByName rejects an identifier whose organization ID does not match. The gap is the derived state. Kurapika: "convertOrganizationMembers builds Roles from organization_members.roles only (coderd/members.go:516-534). An operator or audit consumer enumerating GET /api/v2/organizations/{org}/members sees [] for a member who effectively holds org-admin. The web UI compensates (OrganizationMembersPage.tsx:82-90); API consumers get no such field." Nami adds that the members table itself shows only explicit roles, so "an org that revokes organization-workspace-access from its defaults looks identical to one that grants it".

Luffy frames the scope question the promotion left open: "The stated requirement is 'let a Gateway Accounts org drop its members to the floor.' That needs the ability to remove roles. It does not need to offer org-admin to everyone."

Before this PR that state required opting into the experiment; now one PATCH on any deployment licensed for FeatureMultipleOrganizations reaches it, so the gap becomes the default surface and ships with the promotion. Two candidate fixes, and they are not equivalent: return the org's implied default roles on the member payload as a separate field so the implied grant stays distinguishable from the explicit one, or restrict default_org_member_roles to an allow-list of non-administrative roles. Kurapika: the second "is the stronger vow and costs you the ability to express 'all members are org admins' through this column at all."

Held at P3, not raised. The upward case is that an operator auditing org admins through the canonical endpoint gets a false negative on an authorization-relevant state. Against it: two reviewers independently rated P3, the write is authorized and audited, and the per-member dialog does render the implied rows. The false negative in the API enumeration is the part worth fixing.

🤖

},
};

export const DefaultRolesEnabled: Story = {

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] Story DefaultRolesEnabled is named after the prop this diff deleted. (Netero)

Netero: "With defaultRolesEnabled gone, the story's only remaining distinguishing arg is defaultRolesEntitled: true, and its sibling is already DefaultRolesNotEntitled (:122). Rename to DefaultRolesEntitled."

🤖

Comment thread coderd/rbac/roles_test.go Outdated
// is off (default), both roles carry the workspace-ops elevation. When
// on, both roles carry only the floor and the elevation must be
// granted explicitly via organization-workspace-access.
// TestMemberWorkspaceElevation verifies that organization-member and

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-29] The test doc and two inline comments restate the assertions beside them, and the test name states the opposite of what the test proves. (Gon P2, orchestrator Nit)

Gon's three instances, all in the rewritten test:

  • :206 doc: "'carry only the floor' and 'the workspace-ops elevation is never bundled in' are one claim in two clauses." The part worth keeping is the default_org_member_roles link, because nothing in the body shows it.
  • :231: "'floor must always be present' is what require.True(t, hasResource(member, floorWitness), \"organization-member should include the floor\") asserts." The clause that earns its line is the contrast that makes the identifier a witness: "ResourceOrganizationMember is part of the floor, not the elevation."
  • :246: the comment and the failure message say the same thing. That comment has a second problem and CRF-17 owns it: it promises something the assertion does not check.

And the name: "The test asserts organization-member and organization-service-account do not carry the elevation. Read cold, the name says they do. TestMemberExcludesWorkspaceElevation."

Gon rated these P2. I moved them to Nit and the reasoning is on the record: severity is set by consequence, and the consequence of a comment restating the require message beside it is reader time, not behavior. The keep-argument for P2 is real, that five of eight comments this diff touches are defective and after GA these comments are the only explanation of a permission model that just changed, so the comments that do carry a behavioral consequence are filed at their own severity instead: CRF-14 (the missing explanation), CRF-17 (the comment that claims what the assertion never checks), CRF-27 (the undefined vocabulary). Gon's aggregate point stands: one editing pass over the diff's comments, not five point fixes.

🤖

Comment thread coderd/rbac/roles.go
// Chat access requires the agents-access role and is intentionally
// not granted in the floor.
floor := Permissions(map[string][]policy.Action{
memberPerms := Permissions(map[string][]policy.Action{

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] The upgrade path for existing deployments is covered, contrary to what the deletion suggests. (Netero, Razor, Melody, Knov, Mafuuu, Hisoka, Pariston, Ryosuke)

Eight reviewers traced this independently, so it is worth stating once as settled. Netero: "Removing the gate means organization-member never carries the elevation, but migration 000516_org_default_member_roles.up.sql backfills every existing org with ARRAY['organization-workspace-access'], and coderd.go:659 calls rolestore.ReconcileSystemRoles after ReloadBuiltinRoles at coderd.go:384, so DB-stored system roles are rewritten at startup. Existing members keep workspace access."

The supporting facts, each checked by someone: the column is NOT NULL with no SQL default, so no insert path can silently omit it (Hisoka, Komugi); 000198_ensure_default_org runs before 000516, so a fresh install's default org is backfilled too (Hisoka); the only production InsertOrganization caller passes rbac.DefaultOrgMemberRoles() (Razor, Knov, Mafuuu); the union fires for service accounts because the array_cat sits outside the is_service_account CASE (Hisoka, Meruem, Knov); the single production writer of UpdateOrganizationParams carries the existing value forward, so no unrelated org update blanks the column (Melody); and organization-workspace-access is assignable by orgAdmin and orgUserAdmin, so a gateway org is operable without a site owner (Hisoka, Pariston).

No action. Recorded so the next round does not re-derive it.

🤖

Roles: Roles{
must(RoleByName(RoleMember())),
orgMemberRole(defOrg),
must(RoleByName(ScopedRoleOrgWorkspaceAccess(defOrg))),

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-9] Floor-only member coverage survives the fixture updates. (Netero, Melody)

Netero: "Six member subjects gained ScopedRoleOrgWorkspaceAccess, but siteUserAdminOrgMember (:802) and the subject at :1306 still carry orgMemberRole alone, so the new default (member without the workspace elevation) is still exercised in the authz tables. No gap."

Melody checked the two survivors are correct rather than overlooked: "authz_internal_test.go:802 asserts template ACL behavior and makes no workspace claim, and :1306 carries RoleOwner, whose site permissions cover workspaces."

No action. This is the answer to "did the fixture edits weaken the assertions", and it is no.

🤖

Comment thread coderd/rbac/roles.go
@@ -1184,7 +1173,7 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions {

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-33] Rolling back to a pre-GA binary silently restores workspace access to gateway-account members. (Mafuuu)

Mafuuu: "An org with default_org_member_roles = [] relies entirely on OrgMemberPermissions not bundling the elevation. Roll the binary back to a build where MinimumImplicitMember exists and defaults off, and organization-member carries OrgWorkspaceAccessMemberPerms() again: every member of that org regains workspace create, exec, and ssh, with no configuration change and no log line."

This was equally true under the experiment and the PR does not make it worse, but GA means more orgs will be in the configuration where it bites. Runbook item, not a code change: "do not roll back past this release with emptied default roles", stated rather than discovered.

Takumi checked the adjacent rolling-upgrade window and closed it: from v2.35.x an old replica's own GetAuthorizationUserRoles already unions the column, and from v2.34.x or earlier the old replica is already broken by 000516 itself, because its organization queries are SELECT * into a fixed 9-column struct. This PR creates no new mixed-version window. Established from git show v2.34.7:coderd/database/queries/organizations.sql, not from a live two-version fleet.

🤖

Emyrk and others added 3 commits August 3, 2026 20:30
<!-- Authored with Coder Agents on behalf of @Emyrk -->
Finishes the experiment removal on top of #27472:

- Deletes `DeviationRejectedWithoutExperiment` (it asserted the removed
403 gate and would fail), renames the surviving subtests, and drops the
now-pointless `dv.Experiments` setup.
- Adds a test witnessing that workspace permissions stay off
`organization-member`/`organization-service-account` and live on
`organization-workspace-access`.
- Frontend: removes both
`experiments.includes("minimum-implicit-member")` sites and the whole
`defaultRolesEnabled` prop plumbing (pages, view, stories), required
once codegen drops the value from the `Experiment` union.
- `make gen`: apidoc, `typesGenerated.ts`, feature-stages/schemas docs.

Verified on the pre-rebase base at authoring time; the full-tree
re-verification after the rebase onto main lives in the follow-up
cleanup commit.

<sub>Coder Agents on behalf of @Emyrk.</sub>
- Regenerate apidoc, typesGenerated.ts, and API reference docs so the
  minimum-implicit-member experiment is removed from generated output.
- Update license usercount tests/bench added on main: the RoleOptions
  MinimumImplicitMember toggle is gone now that minimal member is the
  only behavior.
- Give rbac authz test subjects and the workspaceconnwatcher member
  subject the organization-workspace-access role, matching the GA
  default_org_member_roles, since organization-member alone is now
  floor-only.
- Biome formatting fix in CustomRolesPageView.tsx.
Frontend (Default Roles surface):
- Correct the Default Roles copy: changes apply to existing members as
  well as new ones, and the internal term 'floor' is gone from all
  user-facing strings (CRF-11, CRF-12).
- Show the Default Roles section read-only to viewers without
  editSettings; only the Edit button is permission-gated. Also disables
  Edit until the org roles query resolves (CRF-24, CRF-25, CRF-26).
- Roles a member holds explicitly stay selectable in the member role
  dialog even when implied by org defaults, so overlapping explicit
  grants can be removed (CRF-13).
- Rename stories named after the deleted prop (CRF-8).

RBAC:
- Restore prose explaining that organization-member grants no workspace
  access and define the floor/workspace-access split where it lives
  (CRF-14, CRF-27, CRF-5).
- Unexport OrgWorkspaceAccessMemberPerms (CRF-18).
- Rework the member-roles test to assert against the registered
  organization-workspace-access role instead of a witness derived from
  itself; use slices.ContainsFunc; drop the no-op reload pair
  (CRF-17, CRF-29, CRF-30, CRF-32, CRF-6).
- coderd.go builds the full RoleOptions struct and compares to the zero
  value, so new fields cannot be silently ignored (CRF-20).

Tests:
- Remove the no-op ReloadBuiltinRoles(nil) pairs from the license
  usercount tests and parallelize them; rename the MinimumMember
  subtest; drop the orphaned bench comment (CRF-1, CRF-7, CRF-4).

Docs:
- Add a 'Default member roles' section to the organizations admin page
  and cross-link it from groups-roles, adapted for GA from the closed
  docs PR #26702 (CRF-3).
@Emyrk
Emyrk force-pushed the stevenmasley/delete_minimum_experiment branch from 406bb94 to 65cef72 Compare August 3, 2026 20:31
@Emyrk

Emyrk commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Deep-review R1 disposition (fixes in chore: address deep-review findings, plus a reworded commit message and a filled-in PR body):

Fixed

Finding Fix
CRF-11 (P1), CRF-12 (P2) Default Roles copy rewritten: states changes apply to existing members as well as new ones; "the floor" removed from all three user-facing strings.
CRF-13 (P2) Roles a member holds explicitly stay selectable in the member-role dialog even when implied by org defaults (filtered against the member's initial roles so rows don't vanish mid-edit).
CRF-14 (P2), CRF-27 (P3), CRF-5 (Nit) OrgMemberPermissions doc comment now defines the floor, states that organization-member grants no workspace access, and points at the workspace-access role as the grant path.
CRF-28 (P2) PR body written.
CRF-16 (P2) Commit message reworded, em dash removed. CRF-15's verification claim was also narrowed to what was actually run at authoring time.
CRF-17 (P3) The test now asserts against the registered organization-workspace-access role via RoleByName, not a witness derived from the helper itself.
CRF-18 (P3) OrgWorkspaceAccessMemberPerms unexported.
CRF-20 (P3) coderd.go builds the full RoleOptions struct and compares against the zero value; a new field can no longer be silently ignored by the guard.
CRF-24, CRF-25, CRF-26 (P3) Default Roles section now renders read-only for viewers without editSettings; only the Edit button is permission-gated, and it is disabled until the org roles query resolves. The unfalsifiable conjunct is gone.
CRF-3 (P3) "Default member roles" admin docs added to organizations.md (adapted for GA from closed #26702) and cross-linked from groups-roles.md.
CRF-1, CRF-32, CRF-6 (P3/Nit) All no-op ReloadBuiltinRoles(nil) pairs removed; the license usercount tests are parallelized; dead //nolint directives removed or re-justified.
CRF-4, CRF-7, CRF-8, CRF-29, CRF-30 (Nit) Orphaned bench comment deleted; subtest renamed EmptyDefaultRoles; stories renamed; test renamed TestMemberRolesExcludeWorkspacePerms with comments that no longer restate assertions; slices.ContainsFunc.
CRF-31 (P4) Permissions uses slices.SortFunc.

Acknowledged, not in this PR

  • CRF-2: empty ExperimentsSafe is the correct end state; the gauge and --experiments='*' self-restore when the next safe experiment lands.
  • CRF-19: consolidating the five hand-maintained member-subject constructions is a test-infra refactor; follow-up material.
  • CRF-21: default_org_member_roles on POST /organizations is an API addition; follow-up. The create-then-PATCH window matches the pre-GA behavior.
  • CRF-22: implied-role visibility in the members list API is a design question shared with the pre-existing endpoint shape; the write is audited and the per-member dialog shows implied roles.
  • CRF-23: with a lapsed license the org-wide default is unwritable, but per-member repair via PUT /organizations/{org}/members/{user}/roles is AGPL-registered, per the review's own correction.
  • CRF-33 (rollback caveat) is now stated in the PR body. CRF-9, CRF-10, CRF-34, CRF-35 are notes with no action requested.

Verified: go build ./...; go test ./coderd/rbac ./coderd/workspaceconnwatcher ./enterprise/coderd/license; golangci-lint on the touched packages; tsc --noEmit; the CustomRolesPageView stories (12) and OrganizationMembersPage tests (3); make fmt, lint/markdown, lint/emdash all clean.

Generated by Coder Agents on behalf of @Emyrk.

@Emyrk
Emyrk merged commit 52423eb into main Aug 3, 2026
36 checks passed
@Emyrk
Emyrk deleted the stevenmasley/delete_minimum_experiment branch August 3, 2026 20:56
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants