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