Skip to content

feat(server): scope admin RBAC to organizations + per-org 2FA (PR2/4)#70

Merged
Isonimus merged 2 commits into
mainfrom
feat/organizations-layer-pr2
Jun 21, 2026
Merged

feat(server): scope admin RBAC to organizations + per-org 2FA (PR2/4)#70
Isonimus merged 2 commits into
mainfrom
feat/organizations-layer-pr2

Conversation

@Isonimus

Copy link
Copy Markdown
Contributor

Why

PR2 of 4 for the organizations tenant layer. PR1 added the organizations entity + nullable FKs + backfill (inert). This PR is where isolation is actually enforced: the admin owner role stops being a global superuser and becomes a superuser within its own org only — so on a multi-customer box, Company A's owner can never see Company B's data.

What

  • authz (admin/authz.ts): canViewProject / canManageProject gate owner access on the project belonging to the user's org (new projectInOrg helper). Members are same-org by construction.
  • admin routes (routes/admin.ts): every owner-scoped query filters by organization_id — project list/create, user + invite listings, membership grants, and the per-org last-active-owner guard. Cross-org project/user ids return 404, not 403 (no cross-tenant existence leak).
  • session data-view path (middleware/project-access.ts): org-scoped too (404 on a cross-org X-Project-Id).
  • per-org 2FA (admin/settings.ts): policy moves from the install-wide admin_settings.require_2fa to organizations.require_2fa; isTwoFactorRequired/setTwoFactorRequired take an orgId, threaded through login, /me, invite-accept, enforce-2fa, and the settings route. One tenant's toggle no longer affects another.
  • invites (admin/invite.ts): carry the inviter's org so an accepted account joins that company.

Tests

  • New org-isolation integration suite (two orgs A/B): cross-tenant 404s on view/rotate/delete, org-scoped user/invite listings, invite org-inheritance, per-org last-owner guard.
  • Existing admin/RBAC test helpers made org-aware via a shared test-support/org helper.
  • 135 server tests pass (was 128). type-check clean; lint clean (one pre-existing warning unrelated to this PR).

Note

organization_id stays nullable; the NOT NULL backstop lands in PR3 once the bootstrap scripts (create-admin/create-project) and remaining data-only test helpers are org-aware.

Isonimus added 2 commits June 20, 2026 11:40
Re-scope the admin layer to the organization tenant (migration 013): an
owner is now a superuser WITHIN its own org, never across the box.

- authz: canViewProject/canManageProject gate owner access on the project
  belonging to the user's org (new projectInOrg helper).
- admin routes: every owner-scoped query filters by organization_id —
  project list/create, user + invite listings, membership grants, and the
  per-org last-active-owner guard. Cross-org project/user ids return 404
  (not 403) so a tenant can't even confirm another's resources exist.
- project-access middleware: the session data-view path is org-scoped too
  (404 on cross-org X-Project-Id).
- 2FA policy is now per-org (organizations.require_2fa); isTwoFactorRequired
  / setTwoFactorRequired take an orgId, threaded through login, /me, invite
  accept, enforce-2fa, and the settings route.
- invites carry the inviter's org so an accepted account joins that company.

Adds an org-isolation integration suite (two orgs, cross-tenant 404s, scoped
user/invite listings, invite org-inheritance, per-org last-owner guard) and
makes the existing admin/RBAC test helpers org-aware via a shared
test-support/org helper. 135 server tests pass.
A newly-published high-severity advisory (GHSA-vxpw-j846-p89q, undici DoS
via WebSocket fragment-count bypass, fixed in 6.27.0) broke the CI audit
step. undici 6.24.0 is a dev-only transitive dependency of @redocly/cli
(the OpenAPI linter) and never ships in the prod Docker image. Pin it via
pnpm.overrides, mirroring the earlier ws/form-data/protobufjs overrides.
@Isonimus Isonimus merged commit 392b923 into main Jun 21, 2026
4 checks passed
@Isonimus Isonimus deleted the feat/organizations-layer-pr2 branch June 21, 2026 09:41
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