Add RLS Entitlement check (#17179)
## Context - Add RLS entitlement to billing - Check value in the backend (for RLS predicate entity queries/mutations) - Expose billingEntitlements to the API inside currentWorkspace to check available features to the workspace and display the role components accordingly - Cleanup RLS when plan changes back to one without RLS. This should cover almost everything, imho we don't need to check in the ORM because => We can't create RLS without the correct PLAN and switching back to a PLAN without RLS deletes existing RLS through stripes webhooks
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -436,6 +436,18 @@ export type BillingEndTrialPeriodOutput = {
|
||||
status?: Maybe<SubscriptionStatus>;
|
||||
};
|
||||
|
||||
export type BillingEntitlement = {
|
||||
__typename?: 'BillingEntitlement';
|
||||
key: BillingEntitlementKey;
|
||||
value: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export enum BillingEntitlementKey {
|
||||
CUSTOM_DOMAIN = 'CUSTOM_DOMAIN',
|
||||
RLS = 'RLS',
|
||||
SSO = 'SSO'
|
||||
}
|
||||
|
||||
export type BillingLicensedProduct = BillingProductDto & {
|
||||
__typename?: 'BillingLicensedProduct';
|
||||
description: Scalars['String'];
|
||||
@@ -5188,6 +5200,7 @@ export type Workspace = {
|
||||
__typename?: 'Workspace';
|
||||
activationStatus: WorkspaceActivationStatus;
|
||||
allowImpersonation: Scalars['Boolean'];
|
||||
billingEntitlements: Array<BillingEntitlement>;
|
||||
billingSubscriptions: Array<BillingSubscription>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
currentBillingSubscription?: Maybe<BillingSubscription>;
|
||||
|
||||
@@ -3,9 +3,9 @@ import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
|
||||
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import { ApolloFactory, type Options } from '@/apollo/services/apollo.factory';
|
||||
import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant';
|
||||
import { WorkspaceActivationStatus } from '~/generated/graphql';
|
||||
import { ApolloFactory, type Options } from '@/apollo/services/apollo.factory';
|
||||
|
||||
enableFetchMocks();
|
||||
|
||||
@@ -45,6 +45,7 @@ const mockWorkspace = {
|
||||
allowImpersonation: false,
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
billingSubscriptions: [],
|
||||
billingEntitlements: [],
|
||||
currentBillingSubscription: null,
|
||||
workspaceMembersCount: 0,
|
||||
isPublicInviteLinkEnabled: false,
|
||||
|
||||
@@ -15,6 +15,7 @@ export type CurrentWorkspace = Pick<
|
||||
| 'featureFlags'
|
||||
| 'activationStatus'
|
||||
| 'billingSubscriptions'
|
||||
| 'billingEntitlements'
|
||||
| 'currentBillingSubscription'
|
||||
| 'workspaceMembersCount'
|
||||
| 'isPublicInviteLinkEnabled'
|
||||
|
||||
+3
-2
@@ -1,11 +1,11 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant';
|
||||
import { useColumnDefinitionsFromObjectMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromObjectMetadata';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import {
|
||||
SubscriptionInterval,
|
||||
SubscriptionStatus,
|
||||
@@ -50,6 +50,7 @@ const Wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
metadata: {},
|
||||
phases: [],
|
||||
},
|
||||
billingEntitlements: [],
|
||||
billingSubscriptions: [
|
||||
{
|
||||
id: '1',
|
||||
|
||||
+2
-4
@@ -1,4 +1,3 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { FormFieldPlaceholder } from '@/object-record/record-field/ui/form-types/components/FormFieldPlaceholder';
|
||||
import {
|
||||
@@ -9,6 +8,7 @@ import { VariableChipStandalone } from '@/object-record/record-field/ui/form-typ
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
const StyledRecordChip = styled(RecordChip)`
|
||||
margin: ${({ theme }) => theme.spacing(2)};
|
||||
@@ -64,7 +64,5 @@ export const FormSingleRecordFieldChip = ({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledPlaceholder>{t`Select a ${objectNameSingular}`}</StyledPlaceholder>
|
||||
);
|
||||
return <StyledPlaceholder>{t`Select`}</StyledPlaceholder>;
|
||||
};
|
||||
|
||||
+1
-3
@@ -160,8 +160,6 @@ export const FormSingleRecordPicker = ({
|
||||
}
|
||||
};
|
||||
|
||||
const objectNames = objectNameSingulars.join(' or ');
|
||||
|
||||
return (
|
||||
<FormFieldInputContainer data-testid={testId}>
|
||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||
@@ -214,7 +212,7 @@ export const FormSingleRecordPicker = ({
|
||||
focusId={dropdownId}
|
||||
componentInstanceId={dropdownId}
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={t`No ${objectNames}`}
|
||||
emptyLabel={t`No records`}
|
||||
onCancel={() => closeDropdown(dropdownId)}
|
||||
onMorphItemSelected={handleMorphItemSelected}
|
||||
objectNameSingulars={objectNameSingulars}
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ export const Elipsis: Story = {
|
||||
|
||||
export const Performance = getProfilingStory({
|
||||
componentName: 'DateTimeFieldDisplay',
|
||||
averageThresholdInMs: 0.15,
|
||||
averageThresholdInMs: 0.2,
|
||||
numberOfRuns: 30,
|
||||
numberOfTestsPerRun: 30,
|
||||
});
|
||||
|
||||
+14
-1
@@ -15,6 +15,8 @@ import { SettingsPath, type ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import {
|
||||
type BillingEntitlement,
|
||||
BillingEntitlementKey,
|
||||
FeatureFlagKey,
|
||||
useFindOneAgentQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@@ -48,10 +50,21 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
|
||||
});
|
||||
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
|
||||
const workspaceBillingEntitlements = currentWorkspace?.billingEntitlements;
|
||||
|
||||
const isRLSBillingEntitlementEnabled =
|
||||
workspaceBillingEntitlements?.some(
|
||||
(entitlement: BillingEntitlement) =>
|
||||
entitlement.key === BillingEntitlementKey.RLS &&
|
||||
entitlement.value === true,
|
||||
) ?? false;
|
||||
|
||||
const isRowLevelPermissionPredicatesEnabled =
|
||||
featureFlagsMap[
|
||||
FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED
|
||||
] && hasValidEnterpriseKey;
|
||||
] &&
|
||||
(hasValidEnterpriseKey || isRLSBillingEntitlementEnabled);
|
||||
|
||||
const objectMetadataItem = objectMetadata.objectMetadataItem;
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ const StyledFilterRow = styled.div`
|
||||
`;
|
||||
|
||||
const StyledOperandSelectContainer = styled.div`
|
||||
width: 46px;
|
||||
width: 50px;
|
||||
`;
|
||||
|
||||
type SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterRowProps = {
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ const StyledText = styled.div`
|
||||
const StyledContainer = styled.div`
|
||||
align-items: start;
|
||||
display: flex;
|
||||
min-width: ${({ theme }) => theme.spacing(20)};
|
||||
min-width: ${({ theme }) => theme.spacing(16)};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
|
||||
@@ -78,6 +78,10 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
billingSubscriptions {
|
||||
...BillingSubscriptionFragment
|
||||
}
|
||||
billingEntitlements {
|
||||
key
|
||||
value
|
||||
}
|
||||
workspaceMembersCount
|
||||
defaultRole {
|
||||
...RoleFragment
|
||||
|
||||
@@ -126,6 +126,7 @@ export const mockCurrentWorkspace = {
|
||||
},
|
||||
],
|
||||
},
|
||||
billingEntitlements: [],
|
||||
billingSubscriptions: [
|
||||
{
|
||||
__typename: 'BillingSubscription',
|
||||
|
||||
+3
-1
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BillingWebhookController } from 'src/engine/core-modules/billing-webhook/billing-webhook.controller';
|
||||
import { BillingWebhookAlertService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-alert.service';
|
||||
import { BillingWebhookCreditGrantService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-credit-grant.service';
|
||||
import { BillingWebhookCustomerService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-customer.service';
|
||||
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service';
|
||||
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-invoice.service';
|
||||
@@ -10,7 +11,6 @@ import { BillingWebhookPriceService } from 'src/engine/core-modules/billing-webh
|
||||
import { BillingWebhookProductService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-product.service';
|
||||
import { BillingWebhookSubscriptionScheduleService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription-schedule.service';
|
||||
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service';
|
||||
import { BillingWebhookCreditGrantService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-credit-grant.service';
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingEntitlementEntity } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||
@@ -27,6 +27,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RowLevelPermissionModule } from 'src/engine/metadata-modules/row-level-permission-predicate/row-level-permission.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -48,6 +49,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
UserWorkspaceEntity,
|
||||
FeatureFlagEntity,
|
||||
]),
|
||||
RowLevelPermissionModule,
|
||||
],
|
||||
controllers: [BillingWebhookController],
|
||||
providers: [
|
||||
|
||||
+22
-6
@@ -14,6 +14,8 @@ import {
|
||||
} from 'src/engine/core-modules/billing/billing.exception';
|
||||
import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingEntitlementEntity } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { RowLevelPermissionPredicateGroupService } from 'src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service';
|
||||
|
||||
@Injectable()
|
||||
export class BillingWebhookEntitlementService {
|
||||
@@ -22,6 +24,7 @@ export class BillingWebhookEntitlementService {
|
||||
private readonly billingCustomerRepository: Repository<BillingCustomerEntity>,
|
||||
@InjectRepository(BillingEntitlementEntity)
|
||||
private readonly billingEntitlementRepository: Repository<BillingEntitlementEntity>,
|
||||
private readonly rowLevelPermissionPredicateGroupService: RowLevelPermissionPredicateGroupService,
|
||||
) {}
|
||||
|
||||
async processStripeEvent(
|
||||
@@ -40,17 +43,30 @@ export class BillingWebhookEntitlementService {
|
||||
|
||||
const workspaceId = billingCustomer.workspaceId;
|
||||
|
||||
await this.billingEntitlementRepository.upsert(
|
||||
const billingEntitlements =
|
||||
transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
|
||||
workspaceId,
|
||||
data,
|
||||
),
|
||||
{
|
||||
conflictPaths: ['workspaceId', 'key'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
},
|
||||
);
|
||||
|
||||
await this.billingEntitlementRepository.upsert(billingEntitlements, {
|
||||
conflictPaths: ['workspaceId', 'key'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
});
|
||||
|
||||
const isRowLevelPermissionDisabled = billingEntitlements.some(
|
||||
(entitlement) =>
|
||||
entitlement.workspaceId === workspaceId &&
|
||||
entitlement.key === BillingEntitlementKey.RLS &&
|
||||
entitlement.value === false,
|
||||
);
|
||||
|
||||
if (isRowLevelPermissionDisabled) {
|
||||
await this.rowLevelPermissionPredicateGroupService.deleteAllRowLevelPermissionPredicateGroups(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
stripeEntitlementCustomerId: data.object.customer,
|
||||
};
|
||||
|
||||
+13
-1
@@ -47,6 +47,12 @@ describe('transformStripeEntitlementUpdatedEventToDatabaseEntitlement', () => {
|
||||
value: false,
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
{
|
||||
key: BillingEntitlementKey.RLS,
|
||||
stripeCustomerId: 'cus_123',
|
||||
value: false,
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -86,7 +92,13 @@ describe('transformStripeEntitlementUpdatedEventToDatabaseEntitlement', () => {
|
||||
stripeCustomerId: 'cus_123',
|
||||
},
|
||||
{
|
||||
key: 'CUSTOM_DOMAIN',
|
||||
key: BillingEntitlementKey.CUSTOM_DOMAIN,
|
||||
stripeCustomerId: 'cus_123',
|
||||
value: false,
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
{
|
||||
key: BillingEntitlementKey.RLS,
|
||||
stripeCustomerId: 'cus_123',
|
||||
value: false,
|
||||
workspaceId: 'workspaceId',
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
|
||||
registerEnumType(BillingEntitlementKey, {
|
||||
name: 'BillingEntitlementKey',
|
||||
});
|
||||
|
||||
@ObjectType('BillingEntitlement')
|
||||
export class BillingEntitlementDTO {
|
||||
@Field(() => BillingEntitlementKey)
|
||||
key: BillingEntitlementKey;
|
||||
|
||||
@Field(() => Boolean)
|
||||
value: boolean;
|
||||
}
|
||||
+1
@@ -3,4 +3,5 @@
|
||||
export enum BillingEntitlementKey {
|
||||
SSO = 'SSO',
|
||||
CUSTOM_DOMAIN = 'CUSTOM_DOMAIN',
|
||||
RLS = 'RLS',
|
||||
}
|
||||
|
||||
+16
@@ -175,6 +175,22 @@ export class BillingSubscriptionService {
|
||||
};
|
||||
}
|
||||
|
||||
async getWorkspaceEntitlements(
|
||||
workspaceId: string,
|
||||
): Promise<BillingEntitlementEntity[]> {
|
||||
const isBillingEnabled = this.twentyConfigService.get('IS_BILLING_ENABLED');
|
||||
|
||||
if (!isBillingEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entitlements = await this.billingEntitlementRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
|
||||
return entitlements;
|
||||
}
|
||||
|
||||
async getWorkspaceEntitlementByKey(
|
||||
workspaceId: string,
|
||||
key: BillingEntitlementKey,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { fromFlatApplicationToApplicationDto } from 'src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util';
|
||||
import { BillingEntitlementDTO } from 'src/engine/core-modules/billing/dtos/billing-entitlement.dto';
|
||||
import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
|
||||
@@ -320,6 +321,13 @@ export class WorkspaceResolver {
|
||||
return workspace.logo ?? '';
|
||||
}
|
||||
|
||||
@ResolveField(() => [BillingEntitlementDTO])
|
||||
billingEntitlements(@Parent() workspace: WorkspaceEntity) {
|
||||
return this.billingSubscriptionService.getWorkspaceEntitlements(
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
@ResolveField(() => Boolean)
|
||||
hasValidEnterpriseKey(): boolean {
|
||||
return isDefined(this.twentyConfigService.get('ENTERPRISE_KEY'));
|
||||
|
||||
+3
@@ -16,6 +16,8 @@ export const RowLevelPermissionPredicateGroupExceptionCode =
|
||||
'INVALID_ROW_LEVEL_PERMISSION_PREDICATE_GROUP_DATA',
|
||||
ROLE_NOT_FOUND: 'ROLE_NOT_FOUND',
|
||||
UNAUTHORIZED_ROLE_MODIFICATION: 'UNAUTHORIZED_ROLE_MODIFICATION',
|
||||
ROW_LEVEL_PERMISSION_FEATURE_DISABLED:
|
||||
'ROW_LEVEL_PERMISSION_FEATURE_DISABLED',
|
||||
} as const);
|
||||
|
||||
const rowLevelPermissionPredicateGroupExceptionUserFriendlyMessages: Record<
|
||||
@@ -26,6 +28,7 @@ const rowLevelPermissionPredicateGroupExceptionUserFriendlyMessages: Record<
|
||||
INVALID_ROW_LEVEL_PERMISSION_PREDICATE_GROUP_DATA: msg`Invalid row level permission predicate group data.`,
|
||||
ROLE_NOT_FOUND: msg`Role not found.`,
|
||||
UNAUTHORIZED_ROLE_MODIFICATION: msg`Cannot modify predicate group belonging to a different role.`,
|
||||
ROW_LEVEL_PERMISSION_FEATURE_DISABLED: msg`Row level permission predicate feature is disabled.`,
|
||||
INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`,
|
||||
};
|
||||
|
||||
|
||||
+3
@@ -19,6 +19,8 @@ export const RowLevelPermissionPredicateExceptionCode =
|
||||
ROLE_NOT_FOUND: 'ROLE_NOT_FOUND',
|
||||
UNAUTHORIZED_ROLE_MODIFICATION: 'UNAUTHORIZED_ROLE_MODIFICATION',
|
||||
UNAUTHORIZED_OBJECT_MODIFICATION: 'UNAUTHORIZED_OBJECT_MODIFICATION',
|
||||
ROW_LEVEL_PERMISSION_FEATURE_DISABLED:
|
||||
'ROW_LEVEL_PERMISSION_FEATURE_DISABLED',
|
||||
} as const);
|
||||
|
||||
const rowLevelPermissionPredicateExceptionUserFriendlyMessages: Record<
|
||||
@@ -32,6 +34,7 @@ const rowLevelPermissionPredicateExceptionUserFriendlyMessages: Record<
|
||||
ROLE_NOT_FOUND: msg`Role not found.`,
|
||||
UNAUTHORIZED_ROLE_MODIFICATION: msg`Cannot modify predicate belonging to a different role.`,
|
||||
UNAUTHORIZED_OBJECT_MODIFICATION: msg`Cannot modify predicate belonging to a different object.`,
|
||||
ROW_LEVEL_PERMISSION_FEATURE_DISABLED: msg`Row level permission predicate feature is disabled.`,
|
||||
INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`,
|
||||
};
|
||||
|
||||
|
||||
+2
@@ -3,6 +3,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
|
||||
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
|
||||
@@ -22,6 +23,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
WorkspaceMigrationModule,
|
||||
BillingModule,
|
||||
],
|
||||
providers: [
|
||||
RowLevelPermissionPredicateService,
|
||||
|
||||
+86
@@ -1,9 +1,14 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
@@ -17,6 +22,11 @@ import { type DeleteRowLevelPermissionPredicateGroupInput } from 'src/engine/met
|
||||
import { type DestroyRowLevelPermissionPredicateGroupInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/destroy-row-level-permission-predicate-group.input';
|
||||
import { type UpdateRowLevelPermissionPredicateGroupInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate-group.input';
|
||||
import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto';
|
||||
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
|
||||
import {
|
||||
RowLevelPermissionPredicateGroupException,
|
||||
RowLevelPermissionPredicateGroupExceptionCode,
|
||||
} from 'src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate-group.exception';
|
||||
import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
@@ -28,6 +38,10 @@ export class RowLevelPermissionPredicateGroupService {
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly billingService: BillingService,
|
||||
@InjectRepository(RowLevelPermissionPredicateGroupEntity)
|
||||
private readonly rowLevelPermissionPredicateGroupRepository: Repository<RowLevelPermissionPredicateGroupEntity>,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async createOne({
|
||||
@@ -37,6 +51,8 @@ export class RowLevelPermissionPredicateGroupService {
|
||||
createRowLevelPermissionPredicateGroupInput: CreateRowLevelPermissionPredicateGroupInput;
|
||||
workspaceId: string;
|
||||
}): Promise<RowLevelPermissionPredicateGroupDTO> {
|
||||
await this.hasRowLevelPermissionFeatureOrThrow(workspaceId);
|
||||
|
||||
const flatGroupToCreate =
|
||||
fromCreateRowLevelPermissionPredicateGroupInputToFlatRowLevelPermissionPredicateGroupToCreate(
|
||||
{
|
||||
@@ -73,6 +89,8 @@ export class RowLevelPermissionPredicateGroupService {
|
||||
updateRowLevelPermissionPredicateGroupInput: UpdateRowLevelPermissionPredicateGroupInput;
|
||||
workspaceId: string;
|
||||
}): Promise<RowLevelPermissionPredicateGroupDTO> {
|
||||
await this.hasRowLevelPermissionFeatureOrThrow(workspaceId);
|
||||
|
||||
const { flatRowLevelPermissionPredicateGroupMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -117,6 +135,8 @@ export class RowLevelPermissionPredicateGroupService {
|
||||
deleteRowLevelPermissionPredicateGroupInput: DeleteRowLevelPermissionPredicateGroupInput;
|
||||
workspaceId: string;
|
||||
}): Promise<RowLevelPermissionPredicateGroupDTO> {
|
||||
await this.hasRowLevelPermissionFeatureOrThrow(workspaceId);
|
||||
|
||||
const { flatRowLevelPermissionPredicateGroupMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -161,6 +181,8 @@ export class RowLevelPermissionPredicateGroupService {
|
||||
destroyRowLevelPermissionPredicateGroupInput: DestroyRowLevelPermissionPredicateGroupInput;
|
||||
workspaceId: string;
|
||||
}): Promise<RowLevelPermissionPredicateGroupDTO> {
|
||||
await this.hasRowLevelPermissionFeatureOrThrow(workspaceId);
|
||||
|
||||
const { flatRowLevelPermissionPredicateGroupMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -188,6 +210,13 @@ export class RowLevelPermissionPredicateGroupService {
|
||||
async findByWorkspaceId(
|
||||
workspaceId: string,
|
||||
): Promise<RowLevelPermissionPredicateGroupDTO[]> {
|
||||
const hasRowLevelPermissionFeature =
|
||||
await this.hasRowLevelPermissionFeature(workspaceId);
|
||||
|
||||
if (!hasRowLevelPermissionFeature) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { flatRowLevelPermissionPredicateGroupMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -211,6 +240,13 @@ export class RowLevelPermissionPredicateGroupService {
|
||||
workspaceId: string,
|
||||
roleId: string,
|
||||
): Promise<RowLevelPermissionPredicateGroupDTO[]> {
|
||||
const hasRowLevelPermissionFeature =
|
||||
await this.hasRowLevelPermissionFeature(workspaceId);
|
||||
|
||||
if (!hasRowLevelPermissionFeature) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { flatRowLevelPermissionPredicateGroupMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -234,6 +270,13 @@ export class RowLevelPermissionPredicateGroupService {
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<RowLevelPermissionPredicateGroupDTO | null> {
|
||||
const hasRowLevelPermissionFeature =
|
||||
await this.hasRowLevelPermissionFeature(workspaceId);
|
||||
|
||||
if (!hasRowLevelPermissionFeature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { flatRowLevelPermissionPredicateGroupMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -254,6 +297,18 @@ export class RowLevelPermissionPredicateGroupService {
|
||||
return fromFlatRowLevelPermissionPredicateGroupToDto(flatGroup);
|
||||
}
|
||||
|
||||
public async deleteAllRowLevelPermissionPredicateGroups(workspaceId: string) {
|
||||
await this.rowLevelPermissionPredicateGroupRepository.delete({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'rolesPermissions',
|
||||
'flatRowLevelPermissionPredicateMaps',
|
||||
'flatRowLevelPermissionPredicateGroupMaps',
|
||||
]);
|
||||
}
|
||||
|
||||
private async runMigration({
|
||||
workspaceId,
|
||||
flatEntityToCreate = [],
|
||||
@@ -292,4 +347,35 @@ export class RowLevelPermissionPredicateGroupService {
|
||||
'flatRowLevelPermissionPredicateMaps',
|
||||
]);
|
||||
}
|
||||
|
||||
private async hasRowLevelPermissionFeature(
|
||||
workspaceId: string,
|
||||
): Promise<boolean> {
|
||||
const isBillingEnabled = this.configService.get('IS_BILLING_ENABLED');
|
||||
const entrepriseKey = this.configService.get('ENTERPRISE_KEY');
|
||||
|
||||
const isRowLevelPermissionEnabled =
|
||||
await this.billingService.hasEntitlement(
|
||||
workspaceId,
|
||||
BillingEntitlementKey.RLS,
|
||||
);
|
||||
|
||||
if (isDefined(entrepriseKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isBillingEnabled && isRowLevelPermissionEnabled;
|
||||
}
|
||||
|
||||
private async hasRowLevelPermissionFeatureOrThrow(workspaceId: string) {
|
||||
const hasRowLevelPermissionFeature =
|
||||
await this.hasRowLevelPermissionFeature(workspaceId);
|
||||
|
||||
if (!hasRowLevelPermissionFeature) {
|
||||
throw new RowLevelPermissionPredicateGroupException(
|
||||
'Row level permission predicate feature is disabled',
|
||||
RowLevelPermissionPredicateGroupExceptionCode.ROW_LEVEL_PERMISSION_FEATURE_DISABLED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+71
@@ -1,10 +1,13 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
@@ -26,6 +29,10 @@ import {
|
||||
} from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/upsert-row-level-permission-predicates.input';
|
||||
import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto';
|
||||
import { RowLevelPermissionPredicateDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto';
|
||||
import {
|
||||
RowLevelPermissionPredicateException,
|
||||
RowLevelPermissionPredicateExceptionCode,
|
||||
} from 'src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception';
|
||||
import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type';
|
||||
import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate.type';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
@@ -38,6 +45,8 @@ export class RowLevelPermissionPredicateService {
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly billingService: BillingService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async createOne({
|
||||
@@ -47,6 +56,8 @@ export class RowLevelPermissionPredicateService {
|
||||
createRowLevelPermissionPredicateInput: CreateRowLevelPermissionPredicateInput;
|
||||
workspaceId: string;
|
||||
}): Promise<RowLevelPermissionPredicateDTO> {
|
||||
await this.hasRowLevelPermissionFeatureOrThrow(workspaceId);
|
||||
|
||||
const flatPredicateToCreate =
|
||||
fromCreateRowLevelPermissionPredicateInputToFlatRowLevelPermissionPredicateToCreate(
|
||||
{
|
||||
@@ -83,6 +94,8 @@ export class RowLevelPermissionPredicateService {
|
||||
updateRowLevelPermissionPredicateInput: UpdateRowLevelPermissionPredicateInput;
|
||||
workspaceId: string;
|
||||
}): Promise<RowLevelPermissionPredicateDTO> {
|
||||
await this.hasRowLevelPermissionFeatureOrThrow(workspaceId);
|
||||
|
||||
const { flatRowLevelPermissionPredicateMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -127,6 +140,8 @@ export class RowLevelPermissionPredicateService {
|
||||
deleteRowLevelPermissionPredicateInput: DeleteRowLevelPermissionPredicateInput;
|
||||
workspaceId: string;
|
||||
}): Promise<RowLevelPermissionPredicateDTO> {
|
||||
await this.hasRowLevelPermissionFeatureOrThrow(workspaceId);
|
||||
|
||||
const { flatRowLevelPermissionPredicateMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -171,6 +186,8 @@ export class RowLevelPermissionPredicateService {
|
||||
destroyRowLevelPermissionPredicateInput: DestroyRowLevelPermissionPredicateInput;
|
||||
workspaceId: string;
|
||||
}): Promise<RowLevelPermissionPredicateDTO> {
|
||||
await this.hasRowLevelPermissionFeatureOrThrow(workspaceId);
|
||||
|
||||
const { flatRowLevelPermissionPredicateMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -198,6 +215,13 @@ export class RowLevelPermissionPredicateService {
|
||||
async findByWorkspaceId(
|
||||
workspaceId: string,
|
||||
): Promise<RowLevelPermissionPredicateDTO[]> {
|
||||
const hasRowLevelPermissionFeature =
|
||||
await this.hasRowLevelPermissionFeature(workspaceId);
|
||||
|
||||
if (!hasRowLevelPermissionFeature) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { flatRowLevelPermissionPredicateMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -222,6 +246,13 @@ export class RowLevelPermissionPredicateService {
|
||||
roleId: string,
|
||||
objectMetadataId: string,
|
||||
): Promise<RowLevelPermissionPredicateDTO[]> {
|
||||
const hasRowLevelPermissionFeature =
|
||||
await this.hasRowLevelPermissionFeature(workspaceId);
|
||||
|
||||
if (!hasRowLevelPermissionFeature) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { flatRowLevelPermissionPredicateMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -250,6 +281,13 @@ export class RowLevelPermissionPredicateService {
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<RowLevelPermissionPredicateDTO | null> {
|
||||
const hasRowLevelPermissionFeature =
|
||||
await this.hasRowLevelPermissionFeature(workspaceId);
|
||||
|
||||
if (!hasRowLevelPermissionFeature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { flatRowLevelPermissionPredicateMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -280,6 +318,8 @@ export class RowLevelPermissionPredicateService {
|
||||
predicates: RowLevelPermissionPredicateDTO[];
|
||||
predicateGroups: RowLevelPermissionPredicateGroupDTO[];
|
||||
}> {
|
||||
await this.hasRowLevelPermissionFeatureOrThrow(workspaceId);
|
||||
|
||||
const { roleId, objectMetadataId, predicates, predicateGroups } = input;
|
||||
|
||||
const {
|
||||
@@ -653,4 +693,35 @@ export class RowLevelPermissionPredicateService {
|
||||
'flatRowLevelPermissionPredicateGroupMaps',
|
||||
]);
|
||||
}
|
||||
|
||||
private async hasRowLevelPermissionFeature(
|
||||
workspaceId: string,
|
||||
): Promise<boolean> {
|
||||
const isBillingEnabled = this.configService.get('IS_BILLING_ENABLED');
|
||||
const entrepriseKey = this.configService.get('ENTERPRISE_KEY');
|
||||
|
||||
const isRowLevelPermissionEnabled =
|
||||
await this.billingService.hasEntitlement(
|
||||
workspaceId,
|
||||
BillingEntitlementKey.RLS,
|
||||
);
|
||||
|
||||
if (isDefined(entrepriseKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isBillingEnabled && isRowLevelPermissionEnabled;
|
||||
}
|
||||
|
||||
private async hasRowLevelPermissionFeatureOrThrow(workspaceId: string) {
|
||||
const hasRowLevelPermissionFeature =
|
||||
await this.hasRowLevelPermissionFeature(workspaceId);
|
||||
|
||||
if (!hasRowLevelPermissionFeature) {
|
||||
throw new RowLevelPermissionPredicateException(
|
||||
'Row level permission predicate feature is disabled',
|
||||
RowLevelPermissionPredicateExceptionCode.ROW_LEVEL_PERMISSION_FEATURE_DISABLED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -13,6 +13,7 @@ import {
|
||||
type EmailsFilter,
|
||||
type FloatFilter,
|
||||
type FullNameFilter,
|
||||
type IsFilter,
|
||||
type LeafObjectRecordFilter,
|
||||
type LinksFilter,
|
||||
type MultiSelectFilter,
|
||||
@@ -210,6 +211,10 @@ export const isRecordMatchingRLSRowLevelPermissionPredicate = ({
|
||||
const recordFieldValue = record[filterKey];
|
||||
|
||||
if (!isDefined(recordFieldValue)) {
|
||||
if (isObject(filterValue)) {
|
||||
return (filterValue as { is?: IsFilter })?.is === 'NULL';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user