From 22d1682c20f42d2adac1fb43824071e9c2b9ddcd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 03:33:05 +0000 Subject: [PATCH] feat(profiles): tenant_admin gains the org-scoped presentation authority The SaaS composition's `tenant_admin` shipped with no metadata-authoring key, because the platform's only one was `manage_metadata` (`scope: 'platform'`). The installed `@objectstack/spec` 17.6.0 declares the org-scoped subset `manage_org_presentation` (`scope: 'org'`), so the profile now grants it. The docstring's "deliberately DROPPED" paragraph shrinks to that grant, as it said it would, and its `Blocked-by:` line goes. The three app keys it named (`customize_application`, `manage_profiles`, `manage_roles`) stay ungranted: on 17.6.0 the types they author (object, app, page, permission, position) are all `allowOrgOverride: false`, so they still need the platform-scoped key. `test/saas-composition.test.ts` adds the positive assertion, with the scope read from the installed `PLATFORM_CAPABILITIES`. The existing pin that `tenant_admin` holds no platform-scoped capability is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT --- .../1369-tenant-admin-org-presentation.md | 19 ++++++++++ src/sales/profiles/tenant-admin.profile.ts | 35 +++++++++++-------- test/saas-composition.test.ts | 11 ++++++ 3 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 .changeset/1369-tenant-admin-org-presentation.md diff --git a/.changeset/1369-tenant-admin-org-presentation.md b/.changeset/1369-tenant-admin-org-presentation.md new file mode 100644 index 00000000..12d7ba3a --- /dev/null +++ b/.changeset/1369-tenant-admin-org-presentation.md @@ -0,0 +1,19 @@ +--- +'hotcrm': minor +--- + +The SaaS tenant administrator may now author their own organization's views, dashboards and reports + +In the SaaS / multi-org composition (`HOTCRM_COMPOSITION=saas`), the +**Tenant Administrator** permission set now holds the platform's org-scoped +`manage_org_presentation` capability. The platform admits a metadata save from +this capability only for the types it lets each organization override (views, +dashboards, reports, translations and email templates). The save becomes an +overlay for the caller's own active organization, and other organizations see +none of it. + +Everything else stays as it was. Objects, flows, apps, pages, permission sets +and positions still require `manage_metadata`, which reaches every +organization in a deployment, so a tenant admin still cannot author them. The +community app's **System Administrator** and the default composition are +unchanged. diff --git a/src/sales/profiles/tenant-admin.profile.ts b/src/sales/profiles/tenant-admin.profile.ts index d11fc1e5..ac15ca5b 100644 --- a/src/sales/profiles/tenant-admin.profile.ts +++ b/src/sales/profiles/tenant-admin.profile.ts @@ -91,20 +91,24 @@ import { SystemAdminProfile } from './system-admin.profile'; * and no session produces. Keeping the two grants therefore says the true * thing about a tenant admin: inside their org, they see and edit everything. * - * ### What is deliberately DROPPED, and why it is not an oversight - * - * `customize_application`, `manage_profiles` and `manage_roles` are not - * granted. All three describe METADATA authoring, and under a walled posture - * the only metadata-authoring capability the platform has is `manage_metadata` - * — `scope: 'platform'`, which unlocks env-wide tier-B authoring (objects, - * flows) with cross-tenant reach. There is currently no key that lets a tenant - * admin author even their own org's overlays without also handing them that - * reach, so granting these three would describe an authority the deployment - * cannot safely give. - * - * Blocked-by: objectstack-ai/objectstack#12702 — org-scoped presentation - * customization authority. When that capability ships, the tenant admin gains - * it here and this paragraph shrinks to a grant. + * ### What a tenant admin may AUTHOR: their own org's presentation, no more + * + * `manage_org_presentation` IS granted. The platform declares it `scope: 'org'` + * (objectstack-ai/objectstack#12702): it admits `/meta` item writes only for + * the types whose registry entry declares `allowOrgOverride: true` — on the + * installed 17.6.0 that is view, dashboard, report, translation and + * email_template — and only as overlays in the caller's own active + * organization. Measured on 17.6.0 against the platform's own write verdict + * (`metaWriteCapabilityVerdict`): with this grant those saves are allowed when + * the session carries an active organization, and refused when it does not. + * + * `customize_application`, `manage_profiles` and `manage_roles` stay + * ungranted. They describe authoring the app's structure and security model — + * objects, apps, pages, permission sets, positions — and the registry declares + * every one of those types `allowOrgOverride: false`, so the only key that + * admits those writes is still `manage_metadata`: `scope: 'platform'`, reach + * across every organization in the deployment. Granting the three would + * describe an authority the deployment cannot safely give. * * `manage_sharing` IS granted: the platform declares it `scope: 'org'` * ("Administer record sharing … beyond one's own records"), which is precisely @@ -129,5 +133,8 @@ export const TenantAdminProfile = { // Org-bounded by the driver's tenant predicate — see the audit above. 'view_all_data', 'modify_all_data', 'manage_sharing', + // Org-scoped `/meta` writes, only for `allowOrgOverride: true` types and + // only in the caller's own active org — see the AUTHOR note above. + 'manage_org_presentation', ], }; diff --git a/test/saas-composition.test.ts b/test/saas-composition.test.ts index d17c603a..9ecfc8ff 100644 --- a/test/saas-composition.test.ts +++ b/test/saas-composition.test.ts @@ -200,6 +200,17 @@ describe('tenant_admin is an ORG admin, judged by the platform capability regist ).toBe('org'); }); + it('holds the org-scoped presentation-authoring capability (#1369)', () => { + // The org-bounded subset of metadata authoring. Its scope is read off the + // installed registry, never copied here: if the platform ever widened it, + // this and the platform-scoped pin below would both go red. + expect(TenantAdminProfile.systemPermissions).toContain('manage_org_presentation'); + expect( + scopeOf.get('manage_org_presentation'), + 'manage_org_presentation is not org-scoped on the installed platform line — a tenant profile must not hold it', + ).toBe('org'); + }); + it('grants NO platform-scoped capability', () => { // Read off the platform's own registry rather than a hand-listed denylist, // so a capability that becomes platform-scoped upstream — or a new one