@@ -1500,6 +1500,7 @@ export enum FeatureFlagKey {
|
||||
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
|
||||
|
||||
@@ -1472,6 +1472,7 @@ export enum FeatureFlagKey {
|
||||
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
|
||||
|
||||
+14
-5
@@ -7,6 +7,7 @@ import { SettingsRolePermissionsObjectLevelObjectFormObjectLevel } from '@/setti
|
||||
import { SettingsRolePermissionsObjectLevelRecordLevelSection } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelSection';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
@@ -16,6 +17,7 @@ import { Button } from 'twenty-ui/input';
|
||||
import {
|
||||
type BillingEntitlement,
|
||||
BillingEntitlementKey,
|
||||
FeatureFlagKey,
|
||||
useFindOneAgentQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -46,6 +48,8 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
|
||||
const workspaceBillingEntitlements = currentWorkspace?.billingEntitlements;
|
||||
|
||||
const isRLSBillingEntitlementEnabled =
|
||||
@@ -55,6 +59,9 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
|
||||
entitlement.value === true,
|
||||
) ?? false;
|
||||
|
||||
const isRowLevelPermissionPredicatesEnabled =
|
||||
featureFlagsMap[FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED];
|
||||
|
||||
const objectMetadataItem = objectMetadata.objectMetadataItem;
|
||||
|
||||
const objectLabelSingular = objectMetadataItem.labelSingular;
|
||||
@@ -157,11 +164,13 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
roleId={roleId}
|
||||
/>
|
||||
<SettingsRolePermissionsObjectLevelRecordLevelSection
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
roleId={roleId}
|
||||
hasOrganizationPlan={isRLSBillingEntitlementEnabled}
|
||||
/>
|
||||
{isRowLevelPermissionPredicatesEnabled && (
|
||||
<SettingsRolePermissionsObjectLevelRecordLevelSection
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
roleId={roleId}
|
||||
hasOrganizationPlan={isRLSBillingEntitlementEnabled}
|
||||
/>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
+9
@@ -20,6 +20,15 @@ export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
|
||||
'Enable many-to-many relations through junction tables configuration',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED,
|
||||
metadata: {
|
||||
label: 'Row Level Permissions',
|
||||
description: 'Enable row level permission',
|
||||
imagePath:
|
||||
'https://twenty.com/images/lab/is-row-level-permission-predicates-enabled.png',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_SSE_DB_EVENTS_ENABLED,
|
||||
metadata: {
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ export enum FeatureFlagKey {
|
||||
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
|
||||
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
|
||||
IS_FILES_FIELD_MIGRATED = 'IS_FILES_FIELD_MIGRATED',
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
|
||||
|
||||
+2
@@ -234,6 +234,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_TASK_TARGET_MIGRATED: false,
|
||||
IS_TIMELINE_ACTIVITY_MIGRATED: false,
|
||||
IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED: false,
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED: false,
|
||||
IS_JUNCTION_RELATIONS_ENABLED: false,
|
||||
IS_SSE_DB_EVENTS_ENABLED: false,
|
||||
IS_COMMAND_MENU_ITEM_ENABLED: false,
|
||||
@@ -266,6 +267,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_PUBLIC_DOMAIN_ENABLED: false,
|
||||
IS_EMAILING_DOMAIN_ENABLED: false,
|
||||
IS_DASHBOARD_V2_ENABLED: false,
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED: false,
|
||||
},
|
||||
permissionsPerRoleId: {},
|
||||
eventEmitterService: mockInternalContext.eventEmitterService,
|
||||
|
||||
+9
@@ -12,6 +12,7 @@ import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { type QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type';
|
||||
import { type RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
|
||||
import { type RelationDisconnectQueryFieldsByEntityIndex } from 'src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type';
|
||||
@@ -317,6 +318,14 @@ export class WorkspaceInsertQueryBuilder<
|
||||
}
|
||||
|
||||
private validateRLSPredicatesForInsert(): void {
|
||||
if (
|
||||
this.featureFlagMap[
|
||||
FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED
|
||||
] !== true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mainAliasTarget = this.getMainAliasTarget();
|
||||
const objectMetadata = getObjectMetadataFromEntityTarget(
|
||||
mainAliasTarget,
|
||||
|
||||
+9
@@ -10,6 +10,7 @@ import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interf
|
||||
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
@@ -357,6 +358,14 @@ export class WorkspaceSelectQueryBuilder<
|
||||
}
|
||||
|
||||
private applyRowLevelPermissionPredicates(): void {
|
||||
if (
|
||||
this.featureFlagMap[
|
||||
FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED
|
||||
] !== true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldBypassPermissionChecks) {
|
||||
return;
|
||||
}
|
||||
|
||||
+17
@@ -16,6 +16,7 @@ import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { type QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type';
|
||||
import { type RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
|
||||
import { type RelationDisconnectQueryFieldsByEntityIndex } from 'src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type';
|
||||
@@ -615,6 +616,14 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
}
|
||||
|
||||
private applyRowLevelPermissionPredicates(): void {
|
||||
if (
|
||||
this.featureFlagMap[
|
||||
FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED
|
||||
] !== true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldBypassPermissionChecks) {
|
||||
return;
|
||||
}
|
||||
@@ -640,6 +649,14 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
}: {
|
||||
updatedRecords: T[];
|
||||
}): void {
|
||||
if (
|
||||
this.featureFlagMap[
|
||||
FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED
|
||||
] !== true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mainAliasTarget = this.getMainAliasTarget();
|
||||
const objectMetadata = getObjectMetadataFromEntityTarget(
|
||||
mainAliasTarget,
|
||||
|
||||
+10
@@ -12,6 +12,7 @@ import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/
|
||||
|
||||
import { GraphqlQueryFilterFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { buildRowLevelPermissionRecordFilter } from 'src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util';
|
||||
@@ -29,7 +30,16 @@ export const applyRowLevelPermissionPredicates = <T extends ObjectLiteral>({
|
||||
objectMetadata,
|
||||
internalContext,
|
||||
authContext,
|
||||
featureFlagMap,
|
||||
}: ApplyRowLevelPermissionPredicatesArgs<T>): void => {
|
||||
if (
|
||||
featureFlagMap[
|
||||
FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED
|
||||
] !== true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roleId = authContext.userWorkspaceId
|
||||
? internalContext.userWorkspaceRoleMap[authContext.userWorkspaceId]
|
||||
: undefined;
|
||||
|
||||
+5
@@ -66,6 +66,11 @@ export const seedFeatureFlags = async ({
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_JUNCTION_RELATIONS_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
+14
@@ -3,6 +3,7 @@ import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-
|
||||
import { createOneRole } from 'test/integration/metadata/suites/role/utils/create-one-role.util';
|
||||
import { deleteOneRole } from 'test/integration/metadata/suites/role/utils/delete-one-role.util';
|
||||
import { upsertRowLevelPermissionPredicates } from 'test/integration/metadata/suites/row-level-permission-predicate/utils/upsert-row-level-permission-predicates.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { type UpsertRowLevelPermissionPredicatesInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/upsert-row-level-permission-predicates.input';
|
||||
|
||||
type TestSetup = {
|
||||
@@ -161,6 +163,12 @@ describe('Row Level Permission Predicate upsert should fail', () => {
|
||||
let companyNameFieldMetadataId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED,
|
||||
value: true,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
@@ -217,6 +225,12 @@ describe('Row Level Permission Predicate upsert should fail', () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED,
|
||||
value: false,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
if (createdRoleId) {
|
||||
await deleteOneRole({
|
||||
expectToFail: false,
|
||||
|
||||
+14
@@ -2,6 +2,7 @@ import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-
|
||||
import { createOneRole } from 'test/integration/metadata/suites/role/utils/create-one-role.util';
|
||||
import { deleteOneRole } from 'test/integration/metadata/suites/role/utils/delete-one-role.util';
|
||||
import { upsertRowLevelPermissionPredicates } from 'test/integration/metadata/suites/row-level-permission-predicate/utils/upsert-row-level-permission-predicates.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
import {
|
||||
RowLevelPermissionPredicateGroupLogicalOperator,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { type UpsertRowLevelPermissionPredicatesInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/upsert-row-level-permission-predicates.input';
|
||||
|
||||
describe('Row Level Permission Predicate upsert should succeed', () => {
|
||||
@@ -17,6 +19,12 @@ describe('Row Level Permission Predicate upsert should succeed', () => {
|
||||
let createdRoleId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED,
|
||||
value: true,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
@@ -73,6 +81,12 @@ describe('Row Level Permission Predicate upsert should succeed', () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED,
|
||||
value: false,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
if (createdRoleId) {
|
||||
await deleteOneRole({
|
||||
expectToFail: false,
|
||||
|
||||
Reference in New Issue
Block a user