Skip to content

feat(authz): scoped invitations — placement intent gated by the issuer's adminScope, applied on acceptance (ADR-0105 D8) - #3663

Merged
os-zhuang merged 1 commit into
mainfrom
claude/adr-0105-group-tenancy-posture-5womi2
Jul 27, 2026
Merged

feat(authz): scoped invitations — placement intent gated by the issuer's adminScope, applied on acceptance (ADR-0105 D8)#3663
os-zhuang merged 1 commit into
mainfrom
claude/adr-0105-group-tenancy-posture-5womi2

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

ADR-0105 D8 engine (the open half of "scoped invitations — engine seam open; UX commercial"). Builds on #3645. Tracking: #3541.

Why the gate and the carrier ship in one PR

I started this as a carrier-only change (fields + whitelist) with placement application planned cloud-side, and found that split is unsafe:

organization_admin is deliberately read-only on the RBAC tables (auto-org-admin-grant.ts: "so a freshly-granted org admin cannot rebind themselves to admin_full_access"). If the platform applied an invitation's placement payload under system context without checking the issuer, an org admin could assign any position to anyone by routing it through an invitation — recovering exactly the authority that read-only stance denies them. And a framework that declares placement fields nothing applies is the declared ≠ enforced trap AGENTS.md #10 exists to prevent. So: carrier + gate + application, together, open.

What

  • Issuance is authorized against the ISSUER's adminScope (ADR-0090 D12) — judged at issuance, while the actor is the inviter, not at acceptance when the actor is the invitee. The check dry-runs the existing DelegatedAdminGate against the very sys_user_position rows the acceptance would write, which is ADR-0105 D8's "the existing anti-escalation gate, reused verbatim" taken literally: one copy of the subtree/allowlist/containment logic, so a future tightening of the gate tightens invitations in the same commit. A refusal rejects the whole invitation (no row created).
  • Acceptance applies it under system context — idempotent (a replayed acceptance converges instead of duplicating assignments) and failure-isolated (a placement miss never undoes a valid membership; the onInvitationAccepted host seam still fires).
  • Fail closed: an invitation requesting placement in a deployment without the delegated-administration runtime is refused, never silently placed unchecked.

Surface

Package Change
platform-objects sys_invitation.business_unit_id + .positions
plugin-auth ADR-0092 whitelist entry (D7 collision guard covers it automatically); not in the editable map — placement is settable only at issuance, through the gate. better-auth schema.invitation.additionalFields so the columns are real and the invite endpoint carries the request; beforeCreateInvitation / afterAcceptInvitation wiring; lazy invitation-placement resolver (plugin-security starts after plugin-auth)
plugin-security new invitation-placement service — assertIssuable / apply

An invitation without placement intent never consults the gate and behaves exactly as before.

Verification

  • plugin-security 602, plugin-auth 528, platform-objects 215, spec 6659, core 386, runtime 654 — all green.
  • New coverage: intent parsing (camel/snake/JSON-round-tripped/absent), the gate receives the right operation shape and its refusal propagates, apply is idempotent + system-context + stamps granted_by, and the four hook paths (authorized, refused, no-runtime-⇒-refused, ordinary invitation untouched, placement-failure-keeps-membership).
  • Five repo grep guards clean; changeset included (minor ×3).

Follow-up (cloud, commercial)

The issuance UX on cloud#874: BU/position pickers constrained to the issuer's scope, over this now-gated seam. End-to-end HTTP verification belongs with that PR — ee-group-showcase is the natural home, since it already boots the enterprise runtime that registers this service.

🤖 Generated with Claude Code

https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL


Generated by Claude Code

…r's adminScope, applied on acceptance (ADR-0105 D8)

An invitation may now carry PLACEMENT INTENT (target business unit +
positions), so a delegated plant admin's invitee arrives already in the
right unit and role instead of waiting on a platform admin. Closes the
structural gap D8 names for single-posture deployments; the natural
admission path under group.

The two halves ship together, deliberately — a carrier without a gate
would be an escalation hole. organization_admin is deliberately READ-ONLY
on the RBAC tables (auto-org-admin-grant.ts) so a fresh org admin cannot
rebind themselves; applying an unchecked invitation payload under system
context would hand that authority straight back through the invitation
surface.

- Issuance authorized against the ISSUER's adminScope (ADR-0090 D12) by
  dry-running the existing DelegatedAdminGate against the very
  sys_user_position rows the acceptance would write. Reused verbatim: one
  copy of the subtree/allowlist logic, so the two can never drift.
- Acceptance applies it under system context — idempotent (a replayed
  acceptance converges) and failure-isolated (a placement miss never
  undoes a valid membership; the host seam still fires).

Surface: sys_invitation gains business_unit_id + positions (ADR-0092
extension fields, in the D7 collision-guarded whitelist, NOT generically
editable); plugin-security registers the invitation-placement service;
plugin-auth wires beforeCreateInvitation/afterAcceptInvitation to it and
FAILS CLOSED when the runtime is absent.

Verified: plugin-security 602, plugin-auth 528, platform-objects 215,
spec 6659, core 386, runtime 654 — all green; five repo grep guards clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 1:20pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/plugin-security.

17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via @objectstack/plugin-auth, @objectstack/plugin-security)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-auth, @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx (via @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/plugin-security)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-auth, @objectstack/plugin-security)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 13:29
@os-zhuang
os-zhuang merged commit aa8b847 into main Jul 27, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/adr-0105-group-tenancy-posture-5womi2 branch July 27, 2026 13:30
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…e channel closed by ADR-0108 (#4028)

The status line still read 'Phase 0/1 implemented' while both Phase 2 product decisions landed on 2026-07-28: D8 end-to-end (#3645/#3663/#3674/#3695/#3722/#3767) and D9 (#3824/#3873). Cross-references ADR-0108. Docs-only, empty changeset.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants