diff --git a/.changeset/adr-0029-k2-security-ownership.md b/.changeset/adr-0029-k2-security-ownership.md new file mode 100644 index 000000000..3e5c5224a --- /dev/null +++ b/.changeset/adr-0029-k2-security-ownership.md @@ -0,0 +1,32 @@ +--- +"@objectstack/platform-objects": minor +"@objectstack/plugin-security": minor +"@objectstack/plugin-sharing": minor +--- + +ADR-0029 K2 — security domain ownership (RBAC + sharing) + Setup nav contributions. + +Moves the security objects out of the `@objectstack/platform-objects` monolith +into the two capability plugins that already register and operate them, split by +concern (the two are orthogonal — sharing objects never reference RBAC objects): + +- **`@objectstack/plugin-security`** (RBAC) gains `sys_role`, + `sys_permission_set`, `sys_user_permission_set`, `sys_role_permission_set`, + and the `defaultPermissionSets` seed (which its `bootstrap-platform-admin` + already consumes). The RBAC + default-permission-set tests move with them. +- **`@objectstack/plugin-sharing`** gains `sys_record_share`, + `sys_sharing_rule`, `sys_share_link`. +- `@objectstack/platform-objects` no longer defines/exports any security + objects; the `/security` subpath is now an empty barrel. Runtime is unchanged + (both plugins already registered these objects at runtime). + +**D7 navigation** — the Setup app's `group_access_control` is now assembled from +three sources: `plugin-security` contributes Roles / Permission Sets (priority +100), `plugin-sharing` contributes Sharing Rules / Record Shares (priority 200), +and `platform-objects` keeps only API Keys (`sys_api_key`, an identity object, +priority 300) — preserving the original menu order. + +**i18n (D8)** — the objects are removed from the `platform-objects` i18n extract +config; existing generated bundles keep working at runtime (object-name keyed). +Migrating the i18n extraction to the owning plugins remains the tracked +follow-up. diff --git a/packages/platform-objects/scripts/i18n-extract.config.ts b/packages/platform-objects/scripts/i18n-extract.config.ts index 574d27623..1da213c79 100644 --- a/packages/platform-objects/scripts/i18n-extract.config.ts +++ b/packages/platform-objects/scripts/i18n-extract.config.ts @@ -51,15 +51,10 @@ import { } from '../src/identity/index.js'; // ── Security ────────────────────────────────────────────────────────────── -import { - SysRole, - SysPermissionSet, - SysUserPermissionSet, - SysRolePermissionSet, - SysRecordShare, - SysSharingRule, - SysShareLink, -} from '../src/security/index.js'; +// RBAC objects moved to @objectstack/plugin-security and sharing objects to +// @objectstack/plugin-sharing (ADR-0029 K2 / D8). Their i18n extraction must +// move to those plugins before the next regeneration; existing generated +// bundles keep working until then. // ── Audit ───────────────────────────────────────────────────────────────── import { @@ -146,14 +141,8 @@ export default defineStack({ SysOauthConsent, SysJwks, - // Security - SysRole, - SysPermissionSet, - SysUserPermissionSet, - SysRolePermissionSet, - SysRecordShare, - SysSharingRule, - SysShareLink, + // Security: RBAC moved to @objectstack/plugin-security, sharing to + // @objectstack/plugin-sharing (ADR-0029 K2 / D8). // Audit SysAuditLog, diff --git a/packages/platform-objects/src/apps/setup-nav.contributions.ts b/packages/platform-objects/src/apps/setup-nav.contributions.ts index 8693ab87f..85f3d4130 100644 --- a/packages/platform-objects/src/apps/setup-nav.contributions.ts +++ b/packages/platform-objects/src/apps/setup-nav.contributions.ts @@ -9,10 +9,12 @@ * in `@objectstack/platform-objects`. They are registered alongside * `SETUP_APP` (via `plugin-auth`'s `manifest.register`). * - * Some groups are intentionally **absent** here because a capability plugin - * owns them and contributes their entries: - * - `group_integrations` → `@objectstack/plugin-webhooks` (ADR-0029 K2.a) - * - `group_approvals` → `@objectstack/plugin-approvals` (ADR-0029 K2.b) + * Some entries/groups are intentionally contributed by the capability plugin + * that owns the underlying objects rather than living here (ADR-0029 K2): + * - `group_integrations` → `@objectstack/plugin-webhooks` (K2.a) + * - `group_approvals` → `@objectstack/plugin-approvals` (K2.b) + * - `group_access_control` Roles / Permission Sets → `@objectstack/plugin-security` + * - `group_access_control` Sharing Rules / Record Shares → `@objectstack/plugin-sharing` * As each remaining domain moves to its capability plugin, its entries move out * of this file into that plugin the same way. * @@ -57,12 +59,15 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [ { app: 'setup', group: 'group_access_control', - priority: BASE_PRIORITY, + // Priority 300 keeps API Keys after plugin-security's Roles / Permission + // Sets (100) and plugin-sharing's Sharing Rules / Record Shares (200), + // preserving the original menu order. + priority: 300, items: [ - { id: 'nav_roles', type: 'object', label: 'Roles', objectName: 'sys_role', icon: 'shield-check' }, - { id: 'nav_permission_sets', type: 'object', label: 'Permission Sets', objectName: 'sys_permission_set', icon: 'lock' }, - { id: 'nav_sharing_rules', type: 'object', label: 'Sharing Rules', objectName: 'sys_sharing_rule', icon: 'share-2', requiresObject: 'sys_sharing_rule', requiredPermissions: ['manage_platform_settings'] }, - { id: 'nav_record_shares', type: 'object', label: 'Record Shares', objectName: 'sys_record_share', icon: 'link', requiresObject: 'sys_record_share', requiredPermissions: ['manage_platform_settings'] }, + // Roles / Permission Sets are contributed by @objectstack/plugin-security + // and Sharing Rules / Record Shares by @objectstack/plugin-sharing + // (ADR-0029 K2). Only API Keys (sys_api_key, an identity object owned by + // plugin-auth) remains a platform-objects base entry here. { id: 'nav_api_keys', type: 'object', label: 'API Keys', objectName: 'sys_api_key', icon: 'key', requiredPermissions: ['manage_platform_settings'] }, ], }, diff --git a/packages/platform-objects/src/index.ts b/packages/platform-objects/src/index.ts index b91f6b982..1f487f4c6 100644 --- a/packages/platform-objects/src/index.ts +++ b/packages/platform-objects/src/index.ts @@ -8,7 +8,7 @@ * * Subpath imports available: * @objectstack/platform-objects/identity — user, session, org, team, api-key, ... - * @objectstack/platform-objects/security — role, permission-set + * @objectstack/platform-objects/security — (empty; RBAC moved to @objectstack/plugin-security, sharing to @objectstack/plugin-sharing per ADR-0029) * @objectstack/platform-objects/audit — audit-log, presence * @objectstack/platform-objects/integration — (empty; sys_webhook moved to @objectstack/plugin-webhooks per ADR-0029) * @objectstack/platform-objects/metadata — sys_metadata, sys_metadata_history diff --git a/packages/platform-objects/src/platform-objects.test.ts b/packages/platform-objects/src/platform-objects.test.ts index 1a440d3a9..afb96fd32 100644 --- a/packages/platform-objects/src/platform-objects.test.ts +++ b/packages/platform-objects/src/platform-objects.test.ts @@ -16,13 +16,9 @@ import { SysUserPreference, SysVerification, } from './identity/index.js'; -import { - SysPermissionSet, - SysRole, - SysUserPermissionSet, - SysRolePermissionSet, - defaultPermissionSets, -} from './security/index.js'; +// RBAC objects (SysRole/SysPermissionSet/… + defaultPermissionSets) moved to +// @objectstack/plugin-security and the sharing objects to +// @objectstack/plugin-sharing per ADR-0029 K2 — see their packages' tests. import { SysAuditLog, SysPresence } from './audit/index.js'; // sys_webhook moved to @objectstack/plugin-webhooks per ADR-0029 (K2.a). import { @@ -46,10 +42,6 @@ const systemObjects = [ ['SysApiKey', SysApiKey, 'sys_api_key'], ['SysTwoFactor', SysTwoFactor, 'sys_two_factor'], ['SysUserPreference', SysUserPreference, 'sys_user_preference'], - ['SysRole', SysRole, 'sys_role'], - ['SysPermissionSet', SysPermissionSet, 'sys_permission_set'], - ['SysUserPermissionSet', SysUserPermissionSet, 'sys_user_permission_set'], - ['SysRolePermissionSet', SysRolePermissionSet, 'sys_role_permission_set'], ['SysAuditLog', SysAuditLog, 'sys_audit_log'], ['SysPresence', SysPresence, 'sys_presence'], ['SysMetadata', SysMetadata, 'sys_metadata'], @@ -71,117 +63,11 @@ describe('@objectstack/platform-objects', () => { expect((object as any).tableName).toBeUndefined(); }); - describe('default permission sets', () => { - it('exposes the four canonical platform permission sets', () => { - const names = defaultPermissionSets.map((p) => p.name).sort(); - expect(names).toEqual([ - 'admin_full_access', - 'member_default', - 'organization_admin', - 'viewer_readonly', - ]); - }); - - it('organization_admin has setup.access but not studio.access / manage_metadata / manage_platform_settings', () => { - const orgAdmin = defaultPermissionSets.find((p) => p.name === 'organization_admin')!; - const sys = orgAdmin.systemPermissions ?? []; - expect(sys).toContain('setup.access'); - expect(sys).toContain('manage_org_users'); - expect(sys).not.toContain('studio.access'); - expect(sys).not.toContain('manage_metadata'); - expect(sys).not.toContain('manage_platform_settings'); - }); - - it('organization_admin is read-only on global RBAC tables to prevent privilege escalation', () => { - const orgAdmin = defaultPermissionSets.find((p) => p.name === 'organization_admin')!; - for (const obj of [ - 'sys_role', - 'sys_permission_set', - 'sys_role_permission_set', - 'sys_user_permission_set', - 'sys_user_role', - ]) { - const perms = (orgAdmin.objects as any)[obj]; - expect(perms, `${obj} explicit perms missing`).toBeDefined(); - expect(perms.allowRead).toBe(true); - expect(perms.allowCreate).toBe(false); - expect(perms.allowEdit).toBe(false); - expect(perms.allowDelete).toBe(false); - } - }); - - it('admin_full_access grants wildcard CRUD with viewAll/modifyAll', () => { - const admin = defaultPermissionSets.find((p) => p.name === 'admin_full_access')!; - const wildcard = admin.objects['*']; - expect(wildcard).toBeDefined(); - expect(wildcard.allowRead).toBe(true); - expect(wildcard.allowCreate).toBe(true); - expect(wildcard.allowEdit).toBe(true); - expect(wildcard.allowDelete).toBe(true); - expect(wildcard.viewAllRecords).toBe(true); - expect(wildcard.modifyAllRecords).toBe(true); - }); - - it('member_default ships tenant + owner RLS policies plus better-auth system table guards', () => { - const member = defaultPermissionSets.find((p) => p.name === 'member_default')!; - const policyNames = (member.rowLevelSecurity ?? []).map((p) => p.name).sort(); - expect(policyNames).toEqual([ - 'owner_only_deletes', - 'owner_only_writes', - 'sys_account_self', - 'sys_api_key_self', - 'sys_device_code_self', - 'sys_oauth_access_token_self', - 'sys_oauth_application_self', - 'sys_oauth_consent_self', - 'sys_oauth_refresh_token_self', - 'sys_organization_self', - 'sys_session_self', - 'sys_team_member_self', - 'sys_two_factor_self', - 'sys_user_org_members', - 'sys_user_preference_self', - 'sys_user_self', - 'tenant_isolation', - ]); - const tenantPolicy = (member.rowLevelSecurity ?? []).find((p) => p.name === 'tenant_isolation')!; - expect(tenantPolicy.using).toBe('organization_id = current_user.organization_id'); - const orgSelf = (member.rowLevelSecurity ?? []).find((p) => p.name === 'sys_organization_self')!; - expect(orgSelf.object).toBe('sys_organization'); - expect(orgSelf.using).toBe('id = current_user.organization_id'); - // The user_id-keyed better-auth tables (sys_session etc.) get - // per-object carve-outs because the wildcard tenant_isolation - // policy would otherwise DENY them (they lack organization_id). - const sessionSelf = (member.rowLevelSecurity ?? []).find((p) => p.name === 'sys_session_self')!; - expect(sessionSelf.object).toBe('sys_session'); - expect(sessionSelf.using).toBe('user_id = current_user.id'); - }); - - it('viewer_readonly denies writes', () => { - const viewer = defaultPermissionSets.find((p) => p.name === 'viewer_readonly')!; - const wildcard = viewer.objects['*']; - expect(wildcard.allowRead).toBe(true); - expect(wildcard.allowCreate).toBe(false); - expect(wildcard.allowEdit).toBe(false); - expect(wildcard.allowDelete).toBe(false); - }); - }); - describe('sysadmin row actions', () => { // Setup-App admins must be able to drive the access-control lifecycle // without dropping to SQL. These assertions lock in the high-traffic // affordances (activate/deactivate/clone for RBAC objects; unlink // for identity links) so they cannot silently regress. - it('SysRole exposes activate/deactivate/clone/set-default row actions', () => { - const names = (SysRole.actions ?? []).map((a) => a.name).sort(); - expect(names).toEqual(['activate_role', 'clone_role', 'deactivate_role', 'set_default_role']); - }); - - it('SysPermissionSet exposes activate/deactivate/clone row actions', () => { - const names = (SysPermissionSet.actions ?? []).map((a) => a.name).sort(); - expect(names).toEqual(['activate_permission_set', 'clone_permission_set', 'deactivate_permission_set']); - }); - it('SysAccount exposes an unlink-account row action wired to better-auth', () => { const unlink = (SysAccount.actions ?? []).find((a) => a.name === 'unlink_account'); expect(unlink).toBeDefined(); diff --git a/packages/platform-objects/src/security/index.ts b/packages/platform-objects/src/security/index.ts index 25e889f03..a36f7ed88 100644 --- a/packages/platform-objects/src/security/index.ts +++ b/packages/platform-objects/src/security/index.ts @@ -2,13 +2,17 @@ /** * platform-objects/security — Security & Permission Platform Objects + * + * **Empty since ADR-0029 (K2).** The RBAC objects (role / permission-set / + * user-permission-set / role-permission-set + default permission sets) moved + * to `@objectstack/plugin-security`, and the sharing objects (record-share / + * sharing-rule / share-link) moved to `@objectstack/plugin-sharing`, so each + * plugin owns its data model and behavior as one unit. Import them from the + * owning plugin instead. + * + * The subpath (`@objectstack/platform-objects/security`) is retained as an + * empty barrel to avoid churning the package `exports` map / tsup entries + * during the incremental decomposition; it can be removed at ADR-0029 K4. */ -export { SysRole } from './sys-role.object.js'; -export { SysPermissionSet } from './sys-permission-set.object.js'; -export { SysUserPermissionSet } from './sys-user-permission-set.object.js'; -export { SysRolePermissionSet } from './sys-role-permission-set.object.js'; -export { SysRecordShare } from './sys-record-share.object.js'; -export { SysSharingRule } from './sys-sharing-rule.object.js'; -export { SysShareLink } from './sys-share-link.object.js'; -export { defaultPermissionSets } from './default-permission-sets.js'; +export {}; diff --git a/packages/plugins/plugin-security/src/manifest.ts b/packages/plugins/plugin-security/src/manifest.ts index bd37b5e7f..8ce953d96 100644 --- a/packages/plugins/plugin-security/src/manifest.ts +++ b/packages/plugins/plugin-security/src/manifest.ts @@ -14,7 +14,7 @@ import { SysUserPermissionSet, SysRolePermissionSet, defaultPermissionSets, -} from '@objectstack/platform-objects/security'; +} from './objects/index.js'; export const SECURITY_PLUGIN_ID = 'com.objectstack.plugin-security'; export const SECURITY_PLUGIN_VERSION = '1.0.0'; diff --git a/packages/platform-objects/src/security/default-permission-sets.ts b/packages/plugins/plugin-security/src/objects/default-permission-sets.ts similarity index 100% rename from packages/platform-objects/src/security/default-permission-sets.ts rename to packages/plugins/plugin-security/src/objects/default-permission-sets.ts diff --git a/packages/plugins/plugin-security/src/objects/index.ts b/packages/plugins/plugin-security/src/objects/index.ts new file mode 100644 index 000000000..59683e83d --- /dev/null +++ b/packages/plugins/plugin-security/src/objects/index.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * RBAC objects owned by `@objectstack/plugin-security` (ADR-0029 K2). + * + * Moved here from the `@objectstack/platform-objects` monolith so the plugin + * owns its data model, behavior (bootstrap-platform-admin), and admin menu as + * one unit. The sharing objects (record-share / sharing-rule / share-link) + * live in `@objectstack/plugin-sharing`. + */ + +export { SysRole } from './sys-role.object.js'; +export { SysPermissionSet } from './sys-permission-set.object.js'; +export { SysUserPermissionSet } from './sys-user-permission-set.object.js'; +export { SysRolePermissionSet } from './sys-role-permission-set.object.js'; +export { defaultPermissionSets } from './default-permission-sets.js'; diff --git a/packages/plugins/plugin-security/src/objects/rbac-objects.test.ts b/packages/plugins/plugin-security/src/objects/rbac-objects.test.ts new file mode 100644 index 000000000..feaf0dca2 --- /dev/null +++ b/packages/plugins/plugin-security/src/objects/rbac-objects.test.ts @@ -0,0 +1,121 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { SysRole, SysPermissionSet, defaultPermissionSets } from './index.js'; + +/** + * RBAC object + default-permission-set assertions. Moved here with the objects + * from `@objectstack/platform-objects` (ADR-0029 K2) — the plugin that owns the + * data owns its tests. + */ +describe('default permission sets', () => { + it('exposes the four canonical platform permission sets', () => { + const names = defaultPermissionSets.map((p) => p.name).sort(); + expect(names).toEqual([ + 'admin_full_access', + 'member_default', + 'organization_admin', + 'viewer_readonly', + ]); + }); + + it('organization_admin has setup.access but not studio.access / manage_metadata / manage_platform_settings', () => { + const orgAdmin = defaultPermissionSets.find((p) => p.name === 'organization_admin')!; + const sys = orgAdmin.systemPermissions ?? []; + expect(sys).toContain('setup.access'); + expect(sys).toContain('manage_org_users'); + expect(sys).not.toContain('studio.access'); + expect(sys).not.toContain('manage_metadata'); + expect(sys).not.toContain('manage_platform_settings'); + }); + + it('organization_admin is read-only on global RBAC tables to prevent privilege escalation', () => { + const orgAdmin = defaultPermissionSets.find((p) => p.name === 'organization_admin')!; + for (const obj of [ + 'sys_role', + 'sys_permission_set', + 'sys_role_permission_set', + 'sys_user_permission_set', + 'sys_user_role', + ]) { + const perms = (orgAdmin.objects as any)[obj]; + expect(perms, `${obj} explicit perms missing`).toBeDefined(); + expect(perms.allowRead).toBe(true); + expect(perms.allowCreate).toBe(false); + expect(perms.allowEdit).toBe(false); + expect(perms.allowDelete).toBe(false); + } + }); + + it('admin_full_access grants wildcard CRUD with viewAll/modifyAll', () => { + const admin = defaultPermissionSets.find((p) => p.name === 'admin_full_access')!; + const wildcard = admin.objects['*']; + expect(wildcard).toBeDefined(); + expect(wildcard.allowRead).toBe(true); + expect(wildcard.allowCreate).toBe(true); + expect(wildcard.allowEdit).toBe(true); + expect(wildcard.allowDelete).toBe(true); + expect(wildcard.viewAllRecords).toBe(true); + expect(wildcard.modifyAllRecords).toBe(true); + }); + + it('member_default ships tenant + owner RLS policies plus better-auth system table guards', () => { + const member = defaultPermissionSets.find((p) => p.name === 'member_default')!; + const policyNames = (member.rowLevelSecurity ?? []).map((p) => p.name).sort(); + expect(policyNames).toEqual([ + 'owner_only_deletes', + 'owner_only_writes', + 'sys_account_self', + 'sys_api_key_self', + 'sys_device_code_self', + 'sys_oauth_access_token_self', + 'sys_oauth_application_self', + 'sys_oauth_consent_self', + 'sys_oauth_refresh_token_self', + 'sys_organization_self', + 'sys_session_self', + 'sys_team_member_self', + 'sys_two_factor_self', + 'sys_user_org_members', + 'sys_user_preference_self', + 'sys_user_self', + 'tenant_isolation', + ]); + const tenantPolicy = (member.rowLevelSecurity ?? []).find((p) => p.name === 'tenant_isolation')!; + expect(tenantPolicy.using).toBe('organization_id = current_user.organization_id'); + const orgSelf = (member.rowLevelSecurity ?? []).find((p) => p.name === 'sys_organization_self')!; + expect(orgSelf.object).toBe('sys_organization'); + expect(orgSelf.using).toBe('id = current_user.organization_id'); + const sessionSelf = (member.rowLevelSecurity ?? []).find((p) => p.name === 'sys_session_self')!; + expect(sessionSelf.object).toBe('sys_session'); + expect(sessionSelf.using).toBe('user_id = current_user.id'); + }); + + it('viewer_readonly denies writes', () => { + const viewer = defaultPermissionSets.find((p) => p.name === 'viewer_readonly')!; + const wildcard = viewer.objects['*']; + expect(wildcard.allowRead).toBe(true); + expect(wildcard.allowCreate).toBe(false); + expect(wildcard.allowEdit).toBe(false); + expect(wildcard.allowDelete).toBe(false); + }); +}); + +describe('RBAC object canonical names + row actions', () => { + it('SysRole / SysPermissionSet use their canonical sys_ short names and are system objects', () => { + expect(SysRole.name).toBe('sys_role'); + expect(SysPermissionSet.name).toBe('sys_permission_set'); + expect(SysRole.isSystem).toBe(true); + expect(SysPermissionSet.isSystem).toBe(true); + }); + + it('SysRole exposes activate/deactivate/clone/set-default row actions', () => { + const names = (SysRole.actions ?? []).map((a) => a.name).sort(); + expect(names).toEqual(['activate_role', 'clone_role', 'deactivate_role', 'set_default_role']); + }); + + it('SysPermissionSet exposes activate/deactivate/clone row actions', () => { + const names = (SysPermissionSet.actions ?? []).map((a) => a.name).sort(); + expect(names).toEqual(['activate_permission_set', 'clone_permission_set', 'deactivate_permission_set']); + }); +}); diff --git a/packages/platform-objects/src/security/sys-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts similarity index 100% rename from packages/platform-objects/src/security/sys-permission-set.object.ts rename to packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts diff --git a/packages/platform-objects/src/security/sys-role-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-role-permission-set.object.ts similarity index 100% rename from packages/platform-objects/src/security/sys-role-permission-set.object.ts rename to packages/plugins/plugin-security/src/objects/sys-role-permission-set.object.ts diff --git a/packages/platform-objects/src/security/sys-role.object.ts b/packages/plugins/plugin-security/src/objects/sys-role.object.ts similarity index 100% rename from packages/platform-objects/src/security/sys-role.object.ts rename to packages/plugins/plugin-security/src/objects/sys-role.object.ts diff --git a/packages/platform-objects/src/security/sys-user-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts similarity index 100% rename from packages/platform-objects/src/security/sys-user-permission-set.object.ts rename to packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts diff --git a/packages/plugins/plugin-security/src/security-plugin.ts b/packages/plugins/plugin-security/src/security-plugin.ts index 0d551537c..6c847c8c3 100644 --- a/packages/plugins/plugin-security/src/security-plugin.ts +++ b/packages/plugins/plugin-security/src/security-plugin.ts @@ -132,6 +132,20 @@ export class SecurityPlugin implements Plugin { // can resolve them by name when SecurityPlugin middleware queries // `metadata.list('permissions')`. permissions: this.bootstrapPermissionSets, + // ADR-0029 D7 — contribute the RBAC entries into the Setup app's + // `group_access_control` slot. This plugin owns these objects (K2), so it + // ships their menu too; when the plugin is absent the entries don't appear. + navigationContributions: [ + { + app: 'setup', + group: 'group_access_control', + priority: 100, + items: [ + { id: 'nav_roles', type: 'object', label: 'Roles', objectName: 'sys_role', icon: 'shield-check' }, + { id: 'nav_permission_sets', type: 'object', label: 'Permission Sets', objectName: 'sys_permission_set', icon: 'lock' }, + ], + }, + ], }); ctx.logger.info('Security Plugin initialized', { diff --git a/packages/plugins/plugin-sharing/src/index.ts b/packages/plugins/plugin-sharing/src/index.ts index 6f2d7ce39..e3a818d47 100644 --- a/packages/plugins/plugin-sharing/src/index.ts +++ b/packages/plugins/plugin-sharing/src/index.ts @@ -9,7 +9,7 @@ * authenticated execution context. */ -export { SysRecordShare, SysSharingRule, SysShareLink } from '@objectstack/platform-objects/security'; +export { SysRecordShare, SysSharingRule, SysShareLink } from './objects/index.js'; export { SysDepartment, SysDepartmentMember } from '@objectstack/platform-objects/identity'; export { SharingService, diff --git a/packages/plugins/plugin-sharing/src/objects/index.ts b/packages/plugins/plugin-sharing/src/objects/index.ts new file mode 100644 index 000000000..dba72bff4 --- /dev/null +++ b/packages/plugins/plugin-sharing/src/objects/index.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Sharing objects owned by `@objectstack/plugin-sharing` (ADR-0029 K2). + * + * Moved here from the `@objectstack/platform-objects` monolith so the plugin + * owns its data model, behavior, and admin menu as one unit. The RBAC objects + * (role / permission-set / *-permission-set) live in + * `@objectstack/plugin-security`. + */ + +export { SysRecordShare } from './sys-record-share.object.js'; +export { SysSharingRule } from './sys-sharing-rule.object.js'; +export { SysShareLink } from './sys-share-link.object.js'; diff --git a/packages/platform-objects/src/security/sys-record-share.object.ts b/packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts similarity index 100% rename from packages/platform-objects/src/security/sys-record-share.object.ts rename to packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts diff --git a/packages/platform-objects/src/security/sys-share-link.object.ts b/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts similarity index 100% rename from packages/platform-objects/src/security/sys-share-link.object.ts rename to packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts diff --git a/packages/platform-objects/src/security/sys-sharing-rule.object.ts b/packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts similarity index 100% rename from packages/platform-objects/src/security/sys-sharing-rule.object.ts rename to packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts diff --git a/packages/plugins/plugin-sharing/src/sharing-plugin.ts b/packages/plugins/plugin-sharing/src/sharing-plugin.ts index 1de05c94c..fda9c417e 100644 --- a/packages/plugins/plugin-sharing/src/sharing-plugin.ts +++ b/packages/plugins/plugin-sharing/src/sharing-plugin.ts @@ -3,7 +3,7 @@ import type { Plugin, PluginContext } from '@objectstack/core'; import type { EngineMiddleware, OperationContext } from '@objectstack/objectql'; import type { IHttpServer } from '@objectstack/spec/contracts'; -import { SysRecordShare, SysSharingRule, SysShareLink } from '@objectstack/platform-objects/security'; +import { SysRecordShare, SysSharingRule, SysShareLink } from './objects/index.js'; import { SysDepartment, SysDepartmentMember } from '@objectstack/platform-objects/identity'; import { SharingService, type SharingEngine } from './sharing-service.js'; import { SharingRuleService } from './sharing-rule-service.js'; @@ -89,6 +89,20 @@ export class SharingServicePlugin implements Plugin { defaultDatasource: 'cloud', namespace: 'sys', objects: [SysRecordShare, SysSharingRule, SysDepartment, SysDepartmentMember, SysShareLink], + // ADR-0029 D7 — contribute the sharing entries into the Setup app's + // `group_access_control` slot (priority 200 so they sit after plugin- + // security's Roles / Permission Sets). This plugin owns these objects (K2). + navigationContributions: [ + { + app: 'setup', + group: 'group_access_control', + priority: 200, + items: [ + { id: 'nav_sharing_rules', type: 'object', label: 'Sharing Rules', objectName: 'sys_sharing_rule', icon: 'share-2', requiresObject: 'sys_sharing_rule', requiredPermissions: ['manage_platform_settings'] }, + { id: 'nav_record_shares', type: 'object', label: 'Record Shares', objectName: 'sys_record_share', icon: 'link', requiresObject: 'sys_record_share', requiredPermissions: ['manage_platform_settings'] }, + ], + }, + ], }); ctx.logger.info('SharingServicePlugin: schema registered'); }