Skip to content

feat(web): org policy page, identity picker, role-mappings UI - #18

Merged
marcorivm merged 1 commit into
open-edition/07-gateway-resource-scopefrom
open-edition/08-web-org-policy
Aug 8, 2026
Merged

feat(web): org policy page, identity picker, role-mappings UI#18
marcorivm merged 1 commit into
open-edition/07-gateway-resource-scopefrom
open-edition/08-web-org-policy

Conversation

@marcorivm

@marcorivm marcorivm commented Aug 6, 2026

Copy link
Copy Markdown
Member

The org policy page, identity picker, and role-mappings UI, plus a dead-code sweep. 23 files, +2,421/−384 — 1,342 test lines, but they are almost entirely one backend file (routes/org/policy.test.ts). There are no component tests for the identity picker, the role-mapping dialog, or reorder behaviour.

The web surface for the backend landed in #11#13. Last PR before the tail (#8, spend budgets).

Stack

Split out of the original 381-file #8. Upstream catch-up (v1.42.0 → v1.44.0) already landed as #10, so main is now v1.44.0 and everything below is our own code.

main (v1.44.0, after #10)
 └─ #14  01-tier1-ungating              23 files    +73/-442
     └─ #11  02-org-members-rbac        47 files  +5298/-36
         └─ #12  03-user-groups         16 files  +2963/-1
             └─ #13  04-project-access  33 files  +7701/-234
                 └─ #15  05-gateway-org-scope       6 files  +1080/-176
                     └─ #16  06-gateway-conditions  23 files  +2292/-211
                         └─ #17  07-gateway-resource-scope  13 files  +1551/-20
                             └─ #18  08-web-org-policy      23 files  +2421/-384
                                 └─ #8   spend budgets      20 files  +2523/-35
                                     └─ #9   upstream-sync tooling  8 files  +823/-0

Review and merge in order, top to bottom. Roughly half of each diff is tests.

@marcorivm

Copy link
Copy Markdown
Member Author

What it actually does

Two new admin surfaces plus a rewritten identity picker, wired to backend already built and tested earlier in the stack (#11-#13, #15-#17):

1. A "Policy" nav item and page (app/(dashboard)/policy/page.tsx) rendering the existing PolicyEditor in scope="organization" mode — the org-level guardrail set the gateway evaluates alongside each project's policy, taking the stricter verdict. No scope switcher, because project-level policy authoring was retired earlier in the stack (/v1/policy/* now 410s). The page has no client-side role check: a non-admin's request 403s server-side and the editor renders degraded. This needed a real backend route, added here: packages/api/src/routes/org/policy.ts mounts registerPolicyRoutes at /v1/org/policy with resolveScope: (auth) => ({ organizationId: auth.organizationId }).

2. Role mappings on the Groups pagegroups-content.tsx now renders RoleMappingsSection below GroupsTable. An admin maps a group to an org role (member/admin); anyone in that group is granted at least that role. Create, edit (role only — group is fixed once created), reorder via up/down buttons, delete. Ordering is load-bearing: mappings are first-match by priority, and the effect is monotonic — can only raise a role, never lower it. The create/edit dialog calls a live preview endpoint before submit, showing "N members would be raised to X" so blast radius is visible up front. The backend (role-mappings.ts, hooks, API client) already existed; this adds only the consuming UI.

3. A materially rewritten OrgIdentityPicker (lib/policy-editor/identity-picker.tsx) used inside policy rule editing. Offers user and group identities only — no agent, since the API 422s an agent identity on an org rule in every edition. Warns that person/group targeting matches any agent in a project that person/group can access, because proxied traffic carries no signed-in-user identity. Handles partial directory failures gracefully (a non-admin's groups-403 doesn't blank the picker if members loaded) and distinguishes a genuinely removed principal ("Unknown (removed)") from one whose directory merely failed to load.

Why it exists

The web surface for the org RBAC / groups / role-mapping backend and gateway enforcement from #11-#17. Until now those were API-only — no way for an admin to author org policy, assign roles via groups, or target identities in a rule from the console.

Reading order

New UI (real attention):

  1. lib/policy-editor/identity-picker.tsx — the load-bearing rewrite; trickiest state management (per-kind directory-load state, removed-vs-unresolvable chip logic).
  2. lib/policy-editor/_components/identity-picker-section.tsx — small presentational component the picker delegates to; quick.
  3. packages/api/src/routes/org/policy.ts — the new backend mount. Short, but read the doc comment: it states the actual security invariant (two-guard stack).
  4. app/(dashboard)/groups/_components/role-mappings-section.tsx — orchestrates dialog + row actions + reorder over the full list.
  5. .../role-mapping-dialog.tsx — create/edit with live preview; group select disabled in edit mode.
  6. .../role-mapping-row-actions.tsx — per-row reorder/edit/delete; reorder state deliberately lifted to the parent.
  7. app/(dashboard)/policy/page.tsx + loading.tsx — thin. The doc comment explains the "why no scope switcher" rationale; the component is trivial.
  8. packages/api/src/routes/org/policy.test.ts — skim the describe names rather than every test.

Dead-code sweep (skim only — verified inert): deletes hooks/use-domains.ts, use-scim-tokens.ts, use-sso-connections.ts, use-sso-enforcement.ts and their API clients lib/api/{domains,scim-tokens,sso-connections,sso-enforcement}.ts, plus exports in lib/api/index.ts, types.ts, keys.ts. These called /v1/org/sso/* and /v1/org/domains endpoints that never existed in this OSS backend. Confirmed: the hooks had zero callers even before this PR, no matching backend routes exist anywhere in packages/api, and grepping the post-diff tree for sso-connections, sso-enforcement, scim-tokens, use-domains, OrgSsoConnection, OrgSsoEnforcement, ScimToken returns nothing. Don't spend review time here.

What to scrutinise

routes/org/policy.ts's scope guard is the one thing worth re-deriving by hand. role: "admin" is scope-blind — a project-scoped agent key resolves to its owning user, and if that user is an org admin, the role check alone passes. The extra c.get("auth").scope === "project" rejection is what actually prevents a leaked agent credential from rewriting org-wide guardrails. Confirm the same pattern is applied to /v1/org/budgets (it is) and that no route reuses auth({ role: "admin" }) without it.

Is anything client-side-only enforcement? Checked specifically: no. The Policy nav item (nav-config.ts), the Budgets tab, and RoleMappingsSection are all rendered unconditionally and rely on the backend's 403 to degrade (AdminOnlyNotice / admin-only cards) — a deliberate pattern already used for the Activity nav item. Authorization is 100% server-side (verified in policy.ts, role-mappings.ts, budgets.ts). There is no control the UI hides that the backend would allow, nor one the UI shows that the backend doesn't gate.

Identity picker under partial failureallFailed vs per-kind loaded.group/loaded.user. Worth checking the failure paths exercise "groups 403s, members succeeds" and the reverse, not just "both fail."

Role-mapping monotonicity — the UI states "raise only, never lower" as a hard invariant, but that's a backend guarantee. This PR doesn't re-test it; confirm the earlier role-mappings.test.ts covers it.

Design decisions worth questioning

  • The org policy page has no scope switcher and no visible role gate — a bet that "backend 403 + inline notice" is sufficient UX for admin-only surfaces sitewide. Consistent with prior art, so not a new risk, but every future admin-only page must remember to omit a nav-level check.
  • OrgIdentityPicker excludes agent targeting at org scope because the server 422s it — reasonable, but the picker's shape is now coupled to validation rules living in a different file (assertIdentitiesValid). If that validation changes, the picker's options go stale silently rather than failing loudly.

Test coverage reality

Strong on the backend. routes/org/policy.test.ts (1,342 lines) covers scope fencing, authorization (non-admin 403, project-scoped-key-held-by-an-admin 403, unauthenticated 401), cross-org isolation, identity authoring (rejects agent identities, foreign-org principals, suspended members), target authoring, virtual "Default Rule" semantics, publish/generation retention, ordering/reorder conflicts, and audit-log content. Close to exhaustive for the route layer.

Nothing here tests the UI. No identity-picker.test.tsx, no component tests for role-mapping-dialog.tsx or role-mappings-section.tsx — nothing covering reorder, preview, or dialog state reset. Note the PR description's "~1,342 lines of tests" reads like heavy UI coverage; it is almost entirely one backend test file.


Reviewer orientation guide — produced by analysing this PR's diff and surrounding code, not the commit messages. Claims about line numbers and behaviour are worth spot-checking as you read; where it says something is untested or risky, that was verified against the tree rather than inferred.

…ode sweep

Reconciliation Stage E (final feature stage). Mounts the OSS org policy
route via the eeRoutes seam (the write path and org-capable editor already
existed upstream — only the mount was missing, and removed-routes pointed
at a dangling path); adds the org policy page at /policy. The identity
picker is real again — people and user-groups only, no agent-groups — and
safe now that the gateway (Stages F/G/H) enforces those principals, so it
never writes rules the gateway ignores. Adds the role-mappings management
section to /groups (create/edit/reorder/delete, raise-only copy, live
blast-radius preview) that the backend had shipped without a UI. Sweeps the
unused SSO/SCIM/domain clients, hooks, types, and query keys. No
orphan-neutralization pass (inert under grants), no agent-group, no
migration.
@marcorivm
marcorivm force-pushed the open-edition/08-web-org-policy branch from 5581e1e to 5136983 Compare August 8, 2026 18:30
@marcorivm
marcorivm merged commit 85a1511 into main Aug 8, 2026
@marcorivm
marcorivm deleted the open-edition/08-web-org-policy branch August 8, 2026 19:22
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