Skip to content

Org-scoped presentation customization authority: a tenant org admin authors tier-A overlays without platform-wide manage_metadata #12702

Description

@os-zhuang

Part of #12701 (hotcrm single-DB multi-org SaaS). Maintainer-decided direction (2026-08-27 live PM chat, direct-dispatch channel; quotes in the epic) — the implementation shape below is open, the contract is not.

The measured gap (all on origin/main @ 15bf9e8)

  • manage_metadata is the only metadata-authoring capability, and it is scope: 'platform' (packages/spec/src/security/capabilities.ts:43). Every /meta write door demands it (packages/runtime/src/domains/meta.ts, pinned by meta-save-capability-gate.test.ts).
  • Org-scoped writes are threaded only for the five registry allowOrgOverride: true types (view, dashboard, report, translation, email_template) via organizationIdForMetaWrite (packages/metadata-core/src/meta-write-org-scope.ts); every other type lands env-wide (org 作用域的 flow overlay 只在「本进程内发布后」绑定触发器,重启后静默失绑——冷启动两条读路径都把 organization_id 非空的行滤掉了 #6190 discipline).
  • Consequence in a walled posture (group/isolated): granting a tenant org admin the one key that unlocks per-org tier-A overlays also unlocks env-wide tier-B authoring (flows, objects — cross-tenant reach). So a SaaS operator cannot let tenants customize views/dashboards/reports/translations/email templates at all, even though the per-org overlay mechanism (ADR-0005) is complete under the door.

Contract to build

A tenant org admin in a walled posture can author org-scoped overlays for exactly the five tier-A types without holding platform manage_metadata:

  1. A new org-scoped authoring capability (suggested name manage_org_presentation; final name is the dev's call inside ADR-0066 conventions) declared in PLATFORM_CAPABILITIES, seeded like its siblings.
  2. The metadata write door accepts it only when ALL of: the type's registry entry declares allowOrgOverride: true; the write is org-scoped to the caller's own active organization (never env-wide, never another org); posture semantics unchanged otherwise. Platform manage_metadata keeps its exact current behavior — this capability is a subset key, not a re-keying.
  3. Refusals stay loud and name the sanctioned path (no caller-grant disclosure, The runtime dispatcher serialises a PermissionDeniedError's details to the client, so positions / permissionSets reach the browser on the /data transport #7450): a holder attempting a tier-B write or an env-wide write is refused with the existing envelopes (NOT_OVERRIDABLE / PERMISSION_DENIED family — reuse codes, do not mint).
  4. Read side: verify the org-overlay read path (organizationIdForMetaRead) needs no change for this caller class; state the conclusion in the PR body either way.

Walls (restated so the card cannot drift)

Acceptance

  • Capability declared + seeded + documented in the capability registry's own conventions.
  • Walled-posture pins in both directions: org admin holder CAN write an org-scoped view overlay; the same holder is refused (code + status asserted, ADR-0112 envelope) for a tier-B write, an env-wide write, and a foreign-org write. single-posture behavior byte-unchanged for existing callers.
  • Changeset (published packages).

Clause-②: yes (new capability = widened public surface) — dispatch and review at the contract-review tier.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions