Skip to content

feat(authz): expose the caller's delegable scope — the read half of the delegated-admin gate (ADR-0090 D12 / ADR-0105 D8) - #3674

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

feat(authz): expose the caller's delegable scope — the read half of the delegated-admin gate (ADR-0090 D12 / ADR-0105 D8)#3674
os-zhuang merged 3 commits into
mainfrom
claude/adr-0105-group-tenancy-posture-5womi2

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Prerequisite for the D8 scoped-invitation UX (cloud#874), found while scoping it. Follows #3663 (the D8 engine). Tracking: #3541.

The gap

adminScope decided writes but could not be read: assignablePermissionSets lived only inside delegated-admin-gate.ts and resolveHeldScopes is private. So a form offering "place this person in a unit, with these positions" had no way to narrow its pickers — it would list the whole BU tree and every position, and the user would discover the boundary only by being refused. That turns an authorization gate into a validator and makes the boundary invisible until it bites.

What

ISecurityService.describeDelegableScope(callerContext) — exposed as GET /api/v1/security/my-delegable-scope and client.security.describeDelegableScope():

field meaning
placeableBusinessUnitIds union of subtrees where the caller may place people (scopes granting manageAssignments)
assignablePositions positions whose every distributed permission set the caller may hand out — containment check included
scopes held adminScopes with subtrees resolved, for attribution in a UI
isTenantAdmin unconstrained, with everything enumerated so a consumer renders one uniform picker instead of special-casing

Computed by the same resolveHeldScopes / setsBoundToPosition / containment helpers the write path enforces with, so an option this reports is one assert() accepts — a test asserts that agreement directly by replaying every offered (unit, position) pair through the gate, and then checks that a withheld position really is refused, so the narrowing is not cosmetic. It NARROWS; the gate still decides.

Why it's safe to expose

Strictly self-scoped — no target-user parameter, by design. The caller's own resolved sets are the only input, so it discloses nothing beyond the authority they already hold, and there's no "describe someone else's authority" surface to authorize (unlike explain, which has one and gates it on manage_users/covering adminScope).

Fail closed throughout: unresolvable scopes contribute nothing, a caller with no delegated authority gets empty lists, a degraded start with no gate returns empty, and a deployment without @objectstack/plugin-security gets 501. Audience anchors (everyone/guest) never appear — they're implicit and unassignable (ADR-0090 D9).

Verification

Next

The cloud D8 issuance UX consumes this: BU/position pickers narrowed to the issuer's actual authority, over the gated seam from #3663.

🤖 Generated with Claude Code

https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL


Generated by Claude Code

…he delegated-admin gate (ADR-0090 D12 / ADR-0105 D8)

adminScope decided writes but could not be READ: assignablePermissionSets
lived only inside delegated-admin-gate.ts, so a UI offering 'place this
person in a unit, with these positions' (the D8 scoped-invitation form) had
no way to narrow its pickers — it would list the whole tree and let the user
discover the boundary by being refused, turning an authorization gate into a
validator.

ISecurityService.describeDelegableScope(callerContext), exposed as
GET /api/v1/security/my-delegable-scope and
client.security.describeDelegableScope():

- placeableBusinessUnitIds — subtrees where the caller may place people
- assignablePositions — positions whose every distributed set is allowlisted
  (containment check included)
- scopes — held adminScopes with subtrees resolved, for attribution
- isTenantAdmin — unconstrained, everything enumerated so consumers render
  ONE uniform picker

Computed by the same helpers the write path enforces with, so an option this
reports is one assert() accepts; a test asserts that agreement directly by
replaying every offered (unit, position) pair through the gate. It NARROWS —
the gate still decides.

Strictly self-scoped (no target-user parameter), so it discloses nothing
beyond the authority the caller already holds. Fail closed: unresolvable
scopes contribute nothing, no delegated authority yields empty lists, and a
deployment without plugin-security gets 501.

Verified: plugin-security 606, rest 401, client 175, spec 6677 — all green;
route-ledger conformance entry added (#3587); five grep guards + spec
check:docs 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 2:06pm

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/client, @objectstack/plugin-security, @objectstack/rest, @objectstack/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/client, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/client)
  • content/docs/api/environment-routing.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/client, @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/client)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/client, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx (via @objectstack/client)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/client, @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/client)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/client, @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

claude added 2 commits July 27, 2026 13:58
…elegated-administration page

The page described what the write gate enforces but had no answer to 'what
may I delegate?' — the question an admin surface must ask to narrow its
pickers. Adds the endpoint, the SDK call, the response shape, and the three
properties that matter: it narrows but does not decide, a position appears
only if every set it distributes is allowlisted, and it is strictly
self-scoped.

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

DelegableScope + DelegableAdminScope are additive (0 breaking, 2 added) —
the drift guard just needs the refreshed snapshot committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants