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.

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