feat(sdk): declare row-level permission predicates in the role manifest (#21919)
## Why Apps can declare object and field permissions on a role via `defineRole`, but **not row-level security**. The RLS engine and the metadata-sync machinery already support predicates fully — they're first-class universal flat entities, the `FlatRole` already carries `rowLevelPermissionPredicateUniversalIdentifiers`, and the workspace-migration layer has builders/validators/handlers for them. The only gap was the **manifest layer**: `RoleManifest` had no field for predicates, so the sync converter always left them empty. As a result, the only way to ship RLS with an app was a post-install script that pushed predicates through the `upsertRowLevelPermissionPredicates` mutation. That mutation assigns predicates to the workspace's **generic custom application**, not the app that owns the role — so a single role's definition ends up split across two applications and drifts on every upgrade (you have to remember to re-run the script). The Partner app does exactly this today via `configure-partner-rls.ts`. ## What Adds `rowLevelPermissionPredicates` and `rowLevelPermissionPredicateGroups` to `RoleManifest` / `RoleConfig`, mirroring how `objectPermissions` / `fieldPermissions` already flow end-to-end: - **twenty-shared** — predicate + predicate-group manifest types on `RoleManifest` (referencing objects/fields by `universalIdentifier`, operand/logical-operator from the existing GraphQL enums). - **twenty-sdk** — `defineRole` accepts and validates them; the build derives deterministic predicate `universalIdentifier`s (groups keep an explicit one so predicates can reference them). - **twenty-server** — two converters turn manifest predicates/groups into universal flat entities during application-manifest sync, so they are created/updated/deleted together with the role and **owned by the app that ships it**. ### Bug fix found along the way The migration build order ran the `rowLevelPermissionPredicate(Group)` builders **before** the `role` builder, so a predicate declared alongside a brand-new role failed validation with `ROLE_NOT_FOUND`. They now run **after** the role builder, exactly like object/field permissions. ## Partner app (second commit) Converts `partner.role.ts` to declare its five predicates inline and **deletes `configure-partner-rls.ts`** + the `rls:configure` scripts — the workaround this PR is meant to retire. The predicates are byte-for-byte the same semantics as the script produced. > Live-deployment note: the existing script-created predicates are owned by the *custom* application, so the Partner app sync won't touch them. Clear them once (e.g. an empty upsert on the Partner role) around deploy to avoid duplicates. Kept as a **separate commit** so it can be split out if reviewers prefer. ## Testing - **Integration (full app):** new `successful-manifest-sync-row-level-permission-predicate.integration-spec.ts` — installs an app whose role declares a predicate and asserts the predicate row is created (and **owned by the app**, not the custom app), updated in place on re-sync, removed when dropped from the manifest, and removed on uninstall. Ran locally against a seeded test DB ✅. - Re-ran the existing cross-app permission + view-field manifest suites to confirm the build-order change doesn't regress object/field-permission sync (13/13 ✅). - **Unit (utils only):** `defineRole` validation and `fromRoleConfigToRoleManifest` deterministic-id derivation. - Docs: new "Row-level security" section in `apps/config/roles.mdx`. ## Scope notes / possible follow-ups - Surfacing RLS in the app-install permission summary UI was intentionally left out (predicates *restrict* rather than grant, and typically live on a non-default role) — easy follow-up if wanted. - The `upsertRowLevelPermissionPredicates` mutation still homes out-of-band predicates on the custom app for app-owned roles; making that consistent (or rejecting it, like field permissions already do) is a sensible follow-up. https://claude.ai/code/session_01MipAis9z9okd4oCm9HCKEf --- _Generated by [Claude Code](https://claude.ai/code/session_01MipAis9z9okd4oCm9HCKEf)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21919?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -50,6 +50,124 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
## Row-level security
|
||||
|
||||
Object and field permissions decide _which objects and fields_ a role can touch. **Row-level
|
||||
permission predicates** go further and decide _which records_ a role can see and act on — for
|
||||
example, a self-service role where each external user sees only their own records.
|
||||
|
||||
Declare predicates with `rowLevelPermissionPredicates` on the role. Like the rest of the manifest,
|
||||
each predicate carries its own `universalIdentifier`, and references an object and a field by their
|
||||
`universalIdentifier`, an `operand`, and (optionally) a workspaceMember field whose value is injected
|
||||
at query time — so you can express "the record's owner relation **is** the current workspace member":
|
||||
|
||||
```ts src/roles/partner-role.ts
|
||||
import {
|
||||
defineRole,
|
||||
RowLevelPermissionPredicateOperand,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
|
||||
label: 'Partner',
|
||||
description: 'External partner — sees only its own records',
|
||||
canBeAssignedToUsers: true,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
},
|
||||
],
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: RowLevelPermissionPredicateOperand.IS,
|
||||
workspaceMemberFieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
|
||||
.universalIdentifier,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Because predicates ship in the manifest, they are created, updated, and removed together with the
|
||||
role on every install and upgrade — there is no separate post-install step to keep in sync.
|
||||
|
||||
### Combining predicates with groups
|
||||
|
||||
By default a role's predicates are combined with `AND`. To combine some of them with `OR` (or to
|
||||
nest logic), declare a `rowLevelPermissionPredicateGroups` entry and point each predicate at it via
|
||||
`predicateGroupUniversalIdentifier`. This role lets a partner see an Opportunity it either **owns**
|
||||
or is the **point of contact** for:
|
||||
|
||||
```ts src/roles/partner-opportunities-role.ts
|
||||
import {
|
||||
defineRole,
|
||||
RowLevelPermissionPredicateGroupLogicalOperator,
|
||||
RowLevelPermissionPredicateOperand,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
|
||||
const CURRENT_MEMBER =
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
|
||||
.universalIdentifier;
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
|
||||
label: 'Partner (opportunities)',
|
||||
canBeAssignedToUsers: true,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
},
|
||||
],
|
||||
rowLevelPermissionPredicateGroups: [
|
||||
{
|
||||
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
|
||||
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
|
||||
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
|
||||
},
|
||||
],
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
|
||||
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
|
||||
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
|
||||
operand: RowLevelPermissionPredicateOperand.IS,
|
||||
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
|
||||
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
|
||||
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
|
||||
operand: RowLevelPermissionPredicateOperand.IS,
|
||||
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
|
||||
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Give every predicate and group a stable `universalIdentifier` (any uuid) — it keys the entity
|
||||
across upgrades, and predicates reference groups by it.
|
||||
- Predicates can reference objects and fields owned by your app or by Twenty's standard objects.
|
||||
- Row-level security is enforced for workspaces on plans that include it; the predicates still sync
|
||||
on other plans, they are simply not enforced.
|
||||
|
||||
## The default function role
|
||||
|
||||
When you scaffold a new app, the CLI creates a default role file declared with `defineApplicationRole()`:
|
||||
|
||||
+2
@@ -333,6 +333,8 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
canBeAssignedToApiKeys: false,
|
||||
fieldPermissions: [],
|
||||
objectPermissions: [],
|
||||
rowLevelPermissionPredicateGroups: [],
|
||||
rowLevelPermissionPredicates: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
},
|
||||
],
|
||||
|
||||
+4
@@ -1399,6 +1399,8 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
universalIdentifier: 'c0c1c2c3-c4c5-4000-8000-000000000001',
|
||||
fieldPermissions: [],
|
||||
objectPermissions: [],
|
||||
rowLevelPermissionPredicateGroups: [],
|
||||
rowLevelPermissionPredicates: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
},
|
||||
{
|
||||
@@ -1431,6 +1433,8 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
},
|
||||
],
|
||||
rowLevelPermissionPredicateGroups: [],
|
||||
rowLevelPermissionPredicates: [],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
},
|
||||
|
||||
+2
@@ -14,6 +14,8 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
|
||||
"OnDeleteAction",
|
||||
"PageLayoutTabLayoutMode",
|
||||
"RelationType",
|
||||
"RowLevelPermissionPredicateGroupLogicalOperator",
|
||||
"RowLevelPermissionPredicateOperand",
|
||||
"STANDARD_OBJECT",
|
||||
"STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS",
|
||||
"STANDARD_PAGE_LAYOUT",
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { fromRoleConfigToRoleManifest } from '@/cli/utilities/build/manifest/utils/from-role-config-to-role-manifest';
|
||||
import { type RoleConfig } from '@/sdk/define/roles/role-config';
|
||||
import {
|
||||
RowLevelPermissionPredicateGroupLogicalOperator,
|
||||
RowLevelPermissionPredicateOperand,
|
||||
} from '@/sdk/define';
|
||||
|
||||
const ROLE_UNIVERSAL_IDENTIFIER = 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343';
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = '39101b39-1c16-4148-9e82-45dc271bb90d';
|
||||
const FIELD_UNIVERSAL_IDENTIFIER = '0e49f2e4-1e45-433d-bf49-79acc0b06d0e';
|
||||
const PREDICATE_UNIVERSAL_IDENTIFIER = '22222222-0000-4000-8000-000000000000';
|
||||
const GROUP_UNIVERSAL_IDENTIFIER = '11111111-0000-4000-8000-000000000000';
|
||||
|
||||
const baseConfig: RoleConfig = {
|
||||
universalIdentifier: ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Partner',
|
||||
};
|
||||
|
||||
describe('fromRoleConfigToRoleManifest', () => {
|
||||
it('passes predicates through with their explicit universalIdentifier', () => {
|
||||
const config: RoleConfig = {
|
||||
...baseConfig,
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
universalIdentifier: PREDICATE_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
fieldUniversalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: RowLevelPermissionPredicateOperand.IS,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const manifest = fromRoleConfigToRoleManifest(config);
|
||||
const predicate = manifest.rowLevelPermissionPredicates?.[0];
|
||||
|
||||
expect(predicate?.universalIdentifier).toBe(PREDICATE_UNIVERSAL_IDENTIFIER);
|
||||
expect(predicate?.objectUniversalIdentifier).toBe(
|
||||
OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
expect(predicate?.fieldUniversalIdentifier).toBe(FIELD_UNIVERSAL_IDENTIFIER);
|
||||
expect(predicate?.operand).toBe(RowLevelPermissionPredicateOperand.IS);
|
||||
});
|
||||
|
||||
it('passes predicate groups through with their explicit universalIdentifier', () => {
|
||||
const config: RoleConfig = {
|
||||
...baseConfig,
|
||||
rowLevelPermissionPredicateGroups: [
|
||||
{
|
||||
universalIdentifier: GROUP_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const manifest = fromRoleConfigToRoleManifest(config);
|
||||
|
||||
expect(manifest.rowLevelPermissionPredicateGroups).toHaveLength(1);
|
||||
expect(
|
||||
manifest.rowLevelPermissionPredicateGroups?.[0]?.universalIdentifier,
|
||||
).toBe(GROUP_UNIVERSAL_IDENTIFIER);
|
||||
});
|
||||
|
||||
it('defaults predicate collections to empty arrays', () => {
|
||||
const manifest = fromRoleConfigToRoleManifest(baseConfig);
|
||||
|
||||
expect(manifest.rowLevelPermissionPredicates).toEqual([]);
|
||||
expect(manifest.rowLevelPermissionPredicateGroups).toEqual([]);
|
||||
});
|
||||
});
|
||||
+4
@@ -28,6 +28,10 @@ export const fromRoleConfigToRoleManifest = (
|
||||
),
|
||||
}),
|
||||
),
|
||||
rowLevelPermissionPredicateGroups:
|
||||
roleConfig.rowLevelPermissionPredicateGroups ?? [],
|
||||
rowLevelPermissionPredicates:
|
||||
roleConfig.rowLevelPermissionPredicates ?? [],
|
||||
permissionFlagUniversalIdentifiers:
|
||||
roleConfig.permissionFlagUniversalIdentifiers ?? [],
|
||||
};
|
||||
|
||||
@@ -139,6 +139,14 @@ export type {
|
||||
|
||||
export { defineApplicationRole } from '@/sdk/define/roles/define-application-role';
|
||||
export { defineRole } from '@/sdk/define/roles/define-role';
|
||||
export type {
|
||||
RowLevelPermissionPredicateManifest,
|
||||
RowLevelPermissionPredicateGroupManifest,
|
||||
} from 'twenty-shared/application';
|
||||
export {
|
||||
RowLevelPermissionPredicateGroupLogicalOperator,
|
||||
RowLevelPermissionPredicateOperand,
|
||||
} from 'twenty-shared/types';
|
||||
export { SystemPermissionFlag } from 'twenty-shared/constants';
|
||||
|
||||
export { defineSkill } from '@/sdk/define/skills/define-skill';
|
||||
|
||||
@@ -124,4 +124,227 @@ describe('defineRole', () => {
|
||||
'Field permission must have a fieldUniversalIdentifier',
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept row level permission predicates', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
universalIdentifier: '22222222-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
fieldUniversalIdentifier: 'dd14cab4-0829-4475-a794-d0d4959161e6',
|
||||
operand: 'IS',
|
||||
workspaceMemberFieldUniversalIdentifier:
|
||||
'b1c2d3e4-0829-4475-a794-d0d4959161e6',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return error when predicate has no universalIdentifier', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
fieldUniversalIdentifier: 'dd14cab4-0829-4475-a794-d0d4959161e6',
|
||||
operand: 'IS',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Row level permission predicate must have a universalIdentifier',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when two predicates share a universalIdentifier', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
universalIdentifier: '22222222-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
fieldUniversalIdentifier: 'dd14cab4-0829-4475-a794-d0d4959161e6',
|
||||
operand: 'IS',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '22222222-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
fieldUniversalIdentifier: 'dd14cab4-0829-4475-a794-d0d4959161e6',
|
||||
operand: 'IS_NOT',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Duplicate row level permission predicate universalIdentifier "22222222-0000-4000-8000-000000000000"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when predicate has no objectUniversalIdentifier', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
universalIdentifier: '22222222-0000-4000-8000-000000000000',
|
||||
fieldUniversalIdentifier: 'dd14cab4-0829-4475-a794-d0d4959161e6',
|
||||
operand: 'IS',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Row level permission predicate must have an objectUniversalIdentifier',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when predicate has no fieldUniversalIdentifier', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
universalIdentifier: '22222222-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
operand: 'IS',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Row level permission predicate must have a fieldUniversalIdentifier',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when predicate has no operand', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
universalIdentifier: '22222222-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
fieldUniversalIdentifier: 'dd14cab4-0829-4475-a794-d0d4959161e6',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Row level permission predicate must have an operand',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when predicate references an unknown predicate group', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
universalIdentifier: '22222222-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
fieldUniversalIdentifier: 'dd14cab4-0829-4475-a794-d0d4959161e6',
|
||||
operand: 'IS',
|
||||
predicateGroupUniversalIdentifier:
|
||||
'00000000-0000-4000-8000-000000000000',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Row level permission predicate references unknown predicate group "00000000-0000-4000-8000-000000000000"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept a predicate referencing a declared predicate group', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
rowLevelPermissionPredicateGroups: [
|
||||
{
|
||||
universalIdentifier: '11111111-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
logicalOperator: 'OR',
|
||||
},
|
||||
],
|
||||
rowLevelPermissionPredicates: [
|
||||
{
|
||||
universalIdentifier: '22222222-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
fieldUniversalIdentifier: 'dd14cab4-0829-4475-a794-d0d4959161e6',
|
||||
operand: 'IS',
|
||||
predicateGroupUniversalIdentifier:
|
||||
'11111111-0000-4000-8000-000000000000',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return error when predicate group has no logicalOperator', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
rowLevelPermissionPredicateGroups: [
|
||||
{
|
||||
universalIdentifier: '11111111-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Row level permission predicate group must have a logicalOperator',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when two predicate groups share a universalIdentifier', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
rowLevelPermissionPredicateGroups: [
|
||||
{
|
||||
universalIdentifier: '11111111-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
logicalOperator: 'AND',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '11111111-0000-4000-8000-000000000000',
|
||||
objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18',
|
||||
logicalOperator: 'OR',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Duplicate row level permission predicate group universalIdentifier "11111111-0000-4000-8000-000000000000"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,5 +33,84 @@ export const defineRole: DefineEntity<RoleConfig> = (config) => {
|
||||
}
|
||||
}
|
||||
|
||||
const predicateGroupUniversalIdentifiers = new Set<string>();
|
||||
|
||||
if (config.rowLevelPermissionPredicateGroups) {
|
||||
for (const group of config.rowLevelPermissionPredicateGroups) {
|
||||
if (!group.universalIdentifier) {
|
||||
errors.push(
|
||||
'Row level permission predicate group must have a universalIdentifier',
|
||||
);
|
||||
} else if (
|
||||
predicateGroupUniversalIdentifiers.has(group.universalIdentifier)
|
||||
) {
|
||||
errors.push(
|
||||
`Duplicate row level permission predicate group universalIdentifier "${group.universalIdentifier}"`,
|
||||
);
|
||||
} else {
|
||||
predicateGroupUniversalIdentifiers.add(group.universalIdentifier);
|
||||
}
|
||||
|
||||
if (!group.objectUniversalIdentifier) {
|
||||
errors.push(
|
||||
'Row level permission predicate group must have an objectUniversalIdentifier',
|
||||
);
|
||||
}
|
||||
|
||||
if (!group.logicalOperator) {
|
||||
errors.push(
|
||||
'Row level permission predicate group must have a logicalOperator',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const predicateUniversalIdentifiers = new Set<string>();
|
||||
|
||||
if (config.rowLevelPermissionPredicates) {
|
||||
for (const predicate of config.rowLevelPermissionPredicates) {
|
||||
if (!predicate.universalIdentifier) {
|
||||
errors.push(
|
||||
'Row level permission predicate must have a universalIdentifier',
|
||||
);
|
||||
} else if (
|
||||
predicateUniversalIdentifiers.has(predicate.universalIdentifier)
|
||||
) {
|
||||
errors.push(
|
||||
`Duplicate row level permission predicate universalIdentifier "${predicate.universalIdentifier}"`,
|
||||
);
|
||||
} else {
|
||||
predicateUniversalIdentifiers.add(predicate.universalIdentifier);
|
||||
}
|
||||
|
||||
if (!predicate.objectUniversalIdentifier) {
|
||||
errors.push(
|
||||
'Row level permission predicate must have an objectUniversalIdentifier',
|
||||
);
|
||||
}
|
||||
|
||||
if (!predicate.fieldUniversalIdentifier) {
|
||||
errors.push(
|
||||
'Row level permission predicate must have a fieldUniversalIdentifier',
|
||||
);
|
||||
}
|
||||
|
||||
if (!predicate.operand) {
|
||||
errors.push('Row level permission predicate must have an operand');
|
||||
}
|
||||
|
||||
if (
|
||||
predicate.predicateGroupUniversalIdentifier &&
|
||||
!predicateGroupUniversalIdentifiers.has(
|
||||
predicate.predicateGroupUniversalIdentifier,
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
`Row level permission predicate references unknown predicate group "${predicate.predicateGroupUniversalIdentifier}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createValidationResult({ config, errors });
|
||||
};
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { type RowLevelPermissionPredicateGroupManifest } from 'twenty-shared/application';
|
||||
|
||||
import { type UniversalFlatRowLevelPermissionPredicateGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate-group.type';
|
||||
|
||||
export const fromRowLevelPermissionPredicateGroupManifestToUniversalFlatRowLevelPermissionPredicateGroup =
|
||||
({
|
||||
rowLevelPermissionPredicateGroupManifest,
|
||||
roleUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}: {
|
||||
rowLevelPermissionPredicateGroupManifest: RowLevelPermissionPredicateGroupManifest;
|
||||
roleUniversalIdentifier: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
}): UniversalFlatRowLevelPermissionPredicateGroup => {
|
||||
return {
|
||||
universalIdentifier:
|
||||
rowLevelPermissionPredicateGroupManifest.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
rowLevelPermissionPredicateGroupManifest.objectUniversalIdentifier,
|
||||
logicalOperator: rowLevelPermissionPredicateGroupManifest.logicalOperator,
|
||||
parentRowLevelPermissionPredicateGroupUniversalIdentifier:
|
||||
rowLevelPermissionPredicateGroupManifest.parentPredicateGroupUniversalIdentifier ??
|
||||
null,
|
||||
positionInRowLevelPermissionPredicateGroup:
|
||||
rowLevelPermissionPredicateGroupManifest.position ?? null,
|
||||
childRowLevelPermissionPredicateGroupUniversalIdentifiers: [],
|
||||
rowLevelPermissionPredicateUniversalIdentifiers: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
};
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { type RowLevelPermissionPredicateManifest } from 'twenty-shared/application';
|
||||
|
||||
import { type UniversalFlatRowLevelPermissionPredicate } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate.type';
|
||||
|
||||
export const fromRowLevelPermissionPredicateManifestToUniversalFlatRowLevelPermissionPredicate =
|
||||
({
|
||||
rowLevelPermissionPredicateManifest,
|
||||
roleUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}: {
|
||||
rowLevelPermissionPredicateManifest: RowLevelPermissionPredicateManifest;
|
||||
roleUniversalIdentifier: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
}): UniversalFlatRowLevelPermissionPredicate => {
|
||||
return {
|
||||
universalIdentifier:
|
||||
rowLevelPermissionPredicateManifest.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
rowLevelPermissionPredicateManifest.objectUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
rowLevelPermissionPredicateManifest.fieldUniversalIdentifier,
|
||||
operand: rowLevelPermissionPredicateManifest.operand,
|
||||
value: rowLevelPermissionPredicateManifest.value ?? null,
|
||||
subFieldName: rowLevelPermissionPredicateManifest.subFieldName ?? null,
|
||||
workspaceMemberFieldMetadataUniversalIdentifier:
|
||||
rowLevelPermissionPredicateManifest.workspaceMemberFieldUniversalIdentifier ??
|
||||
null,
|
||||
workspaceMemberSubFieldName:
|
||||
rowLevelPermissionPredicateManifest.workspaceMemberSubFieldName ?? null,
|
||||
rowLevelPermissionPredicateGroupUniversalIdentifier:
|
||||
rowLevelPermissionPredicateManifest.predicateGroupUniversalIdentifier ??
|
||||
null,
|
||||
positionInRowLevelPermissionPredicateGroup:
|
||||
rowLevelPermissionPredicateManifest.position ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
};
|
||||
};
|
||||
+36
@@ -22,6 +22,8 @@ import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src
|
||||
import { fromPermissionFlagManifestToUniversalFlatPermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-manifest-to-universal-flat-permission-flag.util';
|
||||
import { fromPermissionFlagToUniversalFlatRolePermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-to-universal-flat-role-permission-flag.util';
|
||||
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util';
|
||||
import { fromRowLevelPermissionPredicateGroupManifestToUniversalFlatRowLevelPermissionPredicateGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-row-level-permission-predicate-group-manifest-to-universal-flat-row-level-permission-predicate-group.util';
|
||||
import { fromRowLevelPermissionPredicateManifestToUniversalFlatRowLevelPermissionPredicate } from 'src/engine/core-modules/application/application-manifest/converters/from-row-level-permission-predicate-manifest-to-universal-flat-row-level-permission-predicate.util';
|
||||
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/application-manifest/converters/from-skill-manifest-to-universal-flat-skill.util';
|
||||
import { computeSearchVectorUniversalSettingsFromObjectManifest } from 'src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util';
|
||||
import { fromViewFieldGroupManifestToUniversalFlatViewFieldGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-group-manifest-to-universal-flat-view-field-group.util';
|
||||
@@ -350,6 +352,40 @@ export class ComputeApplicationManifestAllUniversalFlatEntityMapsService {
|
||||
allUniversalFlatEntityMaps.flatRolePermissionFlagMaps,
|
||||
});
|
||||
}
|
||||
|
||||
for (const rowLevelPermissionPredicateGroupManifest of roleManifest.rowLevelPermissionPredicateGroups ??
|
||||
[]) {
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity:
|
||||
fromRowLevelPermissionPredicateGroupManifestToUniversalFlatRowLevelPermissionPredicateGroup(
|
||||
{
|
||||
rowLevelPermissionPredicateGroupManifest,
|
||||
roleUniversalIdentifier: roleManifest.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
},
|
||||
),
|
||||
universalFlatEntityMapsToMutate:
|
||||
allUniversalFlatEntityMaps.flatRowLevelPermissionPredicateGroupMaps,
|
||||
});
|
||||
}
|
||||
|
||||
for (const rowLevelPermissionPredicateManifest of roleManifest.rowLevelPermissionPredicates ??
|
||||
[]) {
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity:
|
||||
fromRowLevelPermissionPredicateManifestToUniversalFlatRowLevelPermissionPredicate(
|
||||
{
|
||||
rowLevelPermissionPredicateManifest,
|
||||
roleUniversalIdentifier: roleManifest.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
},
|
||||
),
|
||||
universalFlatEntityMapsToMutate:
|
||||
allUniversalFlatEntityMaps.flatRowLevelPermissionPredicateMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const skillManifest of manifest.skills ?? []) {
|
||||
|
||||
+8
-8
@@ -204,14 +204,6 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
ALL_METADATA_NAME.viewSort,
|
||||
workspaceMigrationViewSortActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.rowLevelPermissionPredicateGroup,
|
||||
workspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.rowLevelPermissionPredicate,
|
||||
workspaceMigrationRowLevelPermissionPredicateActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.logicFunction,
|
||||
workspaceMigrationLogicFunctionActionsBuilderService,
|
||||
@@ -220,6 +212,14 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
ALL_METADATA_NAME.role,
|
||||
workspaceMigrationRoleActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.rowLevelPermissionPredicateGroup,
|
||||
workspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.rowLevelPermissionPredicate,
|
||||
workspaceMigrationRowLevelPermissionPredicateActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.objectPermission,
|
||||
workspaceMigrationObjectPermissionActionsBuilderService,
|
||||
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
|
||||
import { type FieldManifest, type Manifest } from 'twenty-shared/application';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RowLevelPermissionPredicateOperand,
|
||||
} from 'twenty-shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
const TEST_WORKSPACE_ID = SEED_APPLE_WORKSPACE_ID;
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
const TEST_OBJECT_PERMISSION_ID = uuidv4();
|
||||
const TEST_FIELD_ID = uuidv4();
|
||||
const TEST_PREDICATE_ID = uuidv4();
|
||||
|
||||
const PERSON_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_OBJECTS.person.universalIdentifier;
|
||||
const WORKSPACE_MEMBER_ID_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_OBJECTS.workspaceMember.fields.id.universalIdentifier;
|
||||
|
||||
const personScopingFieldManifest: FieldManifest = {
|
||||
universalIdentifier: TEST_FIELD_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'integrationRlsScopingColumn',
|
||||
label: 'Integration RLS Scoping Column',
|
||||
description: 'Custom field a row-level predicate scopes on',
|
||||
icon: 'IconLock',
|
||||
objectUniversalIdentifier: PERSON_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
};
|
||||
|
||||
const buildManifestWithPredicates = (
|
||||
predicates: NonNullable<
|
||||
Manifest['roles'][number]['rowLevelPermissionPredicates']
|
||||
>,
|
||||
): Manifest =>
|
||||
buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
overrides: {
|
||||
fields: [personScopingFieldManifest],
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'RLS Test Role',
|
||||
description: 'Role exercising declarative row-level predicates',
|
||||
objectPermissions: [
|
||||
{
|
||||
universalIdentifier: TEST_OBJECT_PERMISSION_ID,
|
||||
objectUniversalIdentifier: PERSON_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
canReadObjectRecords: true,
|
||||
},
|
||||
],
|
||||
rowLevelPermissionPredicates: predicates,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const partnerUserStylePredicate = (
|
||||
operand: RowLevelPermissionPredicateOperand,
|
||||
) => ({
|
||||
universalIdentifier: TEST_PREDICATE_ID,
|
||||
objectUniversalIdentifier: PERSON_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
fieldUniversalIdentifier: TEST_FIELD_ID,
|
||||
operand,
|
||||
workspaceMemberFieldUniversalIdentifier:
|
||||
WORKSPACE_MEMBER_ID_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
type PredicateRow = {
|
||||
id: string;
|
||||
operand: string;
|
||||
roleId: string;
|
||||
objectMetadataId: string;
|
||||
fieldMetadataId: string;
|
||||
workspaceMemberFieldMetadataId: string | null;
|
||||
applicationId: string;
|
||||
};
|
||||
|
||||
const findActivePredicateRows = async (): Promise<PredicateRow[]> =>
|
||||
globalThis.testDataSource.query(
|
||||
`SELECT id, operand, "roleId", "objectMetadataId", "fieldMetadataId",
|
||||
"workspaceMemberFieldMetadataId", "applicationId"
|
||||
FROM core."rowLevelPermissionPredicate"
|
||||
WHERE "universalIdentifier" = $1 AND "workspaceId" = $2 AND "deletedAt" IS NULL`,
|
||||
[TEST_PREDICATE_ID, TEST_WORKSPACE_ID],
|
||||
);
|
||||
|
||||
const findApplicationId = async (): Promise<string> => {
|
||||
const rows = await globalThis.testDataSource.query(
|
||||
`SELECT id FROM core."application"
|
||||
WHERE "universalIdentifier" = $1 AND "workspaceId" = $2`,
|
||||
[TEST_APP_ID, TEST_WORKSPACE_ID],
|
||||
);
|
||||
|
||||
return rows[0]?.id;
|
||||
};
|
||||
|
||||
const findRoleId = async (): Promise<string> => {
|
||||
const rows = await globalThis.testDataSource.query(
|
||||
`SELECT id FROM core."role"
|
||||
WHERE "universalIdentifier" = $1 AND "workspaceId" = $2`,
|
||||
[TEST_ROLE_ID, TEST_WORKSPACE_ID],
|
||||
);
|
||||
|
||||
return rows[0]?.id;
|
||||
};
|
||||
|
||||
const findPersonObjectId = async (): Promise<string> => {
|
||||
const rows = await globalThis.testDataSource.query(
|
||||
`SELECT id FROM core."objectMetadata"
|
||||
WHERE "universalIdentifier" = $1 AND "workspaceId" = $2`,
|
||||
[PERSON_OBJECT_UNIVERSAL_IDENTIFIER, TEST_WORKSPACE_ID],
|
||||
);
|
||||
|
||||
return rows[0]?.id;
|
||||
};
|
||||
|
||||
describe('Manifest sync - row level permission predicates declared on a role', () => {
|
||||
beforeEach(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
name: 'Test RLS Predicate Application',
|
||||
description: 'App for testing declarative row-level predicate sync',
|
||||
sourcePath: 'test-manifest-rls-predicate',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupApplicationAndAppRegistration({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a row-level predicate owned by the app and pointing at the declared role/object/field', async () => {
|
||||
const { errors } = await syncApplication({
|
||||
manifest: buildManifestWithPredicates([
|
||||
partnerUserStylePredicate(RowLevelPermissionPredicateOperand.IS),
|
||||
]),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
|
||||
const [applicationId, roleId, personObjectId, predicateRows] =
|
||||
await Promise.all([
|
||||
findApplicationId(),
|
||||
findRoleId(),
|
||||
findPersonObjectId(),
|
||||
findActivePredicateRows(),
|
||||
]);
|
||||
|
||||
expect(predicateRows).toHaveLength(1);
|
||||
|
||||
const predicate = predicateRows[0];
|
||||
|
||||
expect(predicate.operand).toBe(RowLevelPermissionPredicateOperand.IS);
|
||||
expect(predicate.roleId).toBe(roleId);
|
||||
expect(predicate.objectMetadataId).toBe(personObjectId);
|
||||
expect(predicate.fieldMetadataId).toBeTruthy();
|
||||
expect(predicate.workspaceMemberFieldMetadataId).toBeTruthy();
|
||||
expect(predicate.applicationId).toBe(applicationId);
|
||||
}, 60000);
|
||||
|
||||
it('updates the predicate in place on re-sync and removes it when dropped from the manifest', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifestWithPredicates([
|
||||
partnerUserStylePredicate(RowLevelPermissionPredicateOperand.IS),
|
||||
]),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const createdRows = await findActivePredicateRows();
|
||||
|
||||
expect(createdRows).toHaveLength(1);
|
||||
const predicateId = createdRows[0].id;
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifestWithPredicates([
|
||||
partnerUserStylePredicate(RowLevelPermissionPredicateOperand.IS_NOT),
|
||||
]),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const updatedRows = await findActivePredicateRows();
|
||||
|
||||
expect(updatedRows).toHaveLength(1);
|
||||
expect(updatedRows[0].id).toBe(predicateId);
|
||||
expect(updatedRows[0].operand).toBe(
|
||||
RowLevelPermissionPredicateOperand.IS_NOT,
|
||||
);
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifestWithPredicates([]),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(await findActivePredicateRows()).toHaveLength(0);
|
||||
}, 60000);
|
||||
|
||||
it('removes the predicate on uninstall', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifestWithPredicates([
|
||||
partnerUserStylePredicate(RowLevelPermissionPredicateOperand.IS),
|
||||
]),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(await findActivePredicateRows()).toHaveLength(1);
|
||||
|
||||
await uninstallApplication({
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(await findActivePredicateRows()).toHaveLength(0);
|
||||
}, 60000);
|
||||
});
|
||||
@@ -64,6 +64,8 @@ export type { PreInstallLogicFunctionApplicationManifest } from './preInstallLog
|
||||
export type {
|
||||
ObjectPermissionManifest,
|
||||
FieldPermissionManifest,
|
||||
RowLevelPermissionPredicateGroupManifest,
|
||||
RowLevelPermissionPredicateManifest,
|
||||
RoleManifest,
|
||||
} from './roleManifestType';
|
||||
export type { RunAgentInput, RunAgentResult } from './runAgentType';
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
|
||||
import {
|
||||
type RowLevelPermissionPredicateGroupLogicalOperator,
|
||||
type RowLevelPermissionPredicateOperand,
|
||||
type RowLevelPermissionPredicateValue,
|
||||
} from '@/types';
|
||||
|
||||
export type ObjectPermissionManifest = SyncableEntityOptions & {
|
||||
objectUniversalIdentifier: string;
|
||||
@@ -15,6 +20,25 @@ export type FieldPermissionManifest = SyncableEntityOptions & {
|
||||
canUpdateFieldValue?: boolean;
|
||||
};
|
||||
|
||||
export type RowLevelPermissionPredicateGroupManifest = SyncableEntityOptions & {
|
||||
objectUniversalIdentifier: string;
|
||||
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator;
|
||||
parentPredicateGroupUniversalIdentifier?: string | null;
|
||||
position?: number | null;
|
||||
};
|
||||
|
||||
export type RowLevelPermissionPredicateManifest = SyncableEntityOptions & {
|
||||
objectUniversalIdentifier: string;
|
||||
fieldUniversalIdentifier: string;
|
||||
operand: RowLevelPermissionPredicateOperand;
|
||||
value?: RowLevelPermissionPredicateValue | null;
|
||||
subFieldName?: string | null;
|
||||
workspaceMemberFieldUniversalIdentifier?: string | null;
|
||||
workspaceMemberSubFieldName?: string | null;
|
||||
predicateGroupUniversalIdentifier?: string | null;
|
||||
position?: number | null;
|
||||
};
|
||||
|
||||
export type RoleManifest = SyncableEntityOptions & {
|
||||
label: string;
|
||||
description?: string;
|
||||
@@ -30,5 +54,7 @@ export type RoleManifest = SyncableEntityOptions & {
|
||||
canBeAssignedToApiKeys?: boolean;
|
||||
objectPermissions?: ObjectPermissionManifest[];
|
||||
fieldPermissions?: FieldPermissionManifest[];
|
||||
rowLevelPermissionPredicates?: RowLevelPermissionPredicateManifest[];
|
||||
rowLevelPermissionPredicateGroups?: RowLevelPermissionPredicateGroupManifest[];
|
||||
permissionFlagUniversalIdentifiers?: string[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user