From 670ff1583eb9ea9ef65db29abb48365bc75b92f3 Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 22 Jan 2026 11:48:24 +0100 Subject: [PATCH] Fix RLS entitlement check + fix role page with RLS on object without object-permission not being displayed (#17326) --- .../useGetObjectPermissionDerivedStates.ts | 14 ++++++++++ .../useFilterObjectWithPermissionOverride.ts | 2 ++ ...gsRolePermissionsObjectLevelObjectForm.tsx | 6 ++-- .../services/billing-subscription.service.ts | 28 ++++++++++++++----- .../workspace/workspace.resolver.ts | 2 +- ...evel-permission-predicate-group.service.ts | 11 +++----- .../row-level-permission-predicate.service.ts | 11 +++----- 7 files changed, 48 insertions(+), 26 deletions(-) diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useGetObjectPermissionDerivedStates.ts b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useGetObjectPermissionDerivedStates.ts index fc9cf21d90..f66c7f7dfd 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useGetObjectPermissionDerivedStates.ts +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useGetObjectPermissionDerivedStates.ts @@ -163,6 +163,19 @@ export const useGetObjectPermissionDerivedStates = ({ ((isThereAnyFieldPermissionThatRevokeRead && canRestrictFieldRead) || (isThereAnyFieldPermissionThatRevokeUpdate && canRestrictFieldUpdate)); + const rowLevelPermissionPredicatesForThisObject = + settingsDraftRole.rowLevelPermissionPredicates?.filter( + (predicateToFilter) => + predicateToFilter.objectMetadataId === objectMetadataItemId, + ) ?? []; + + const isThereAnyRowLevelPermissionPredicateForThisObject = + rowLevelPermissionPredicatesForThisObject.length > 0; + + const objectHasNoOverrideButRowLevelPermissionShouldBeTakenIntoAccount = + objectHasNoOverrideOnObjectPermission && + isThereAnyRowLevelPermissionPredicateForThisObject; + const objectHasOverrideOnObjectPermissions = !objectHasNoOverrideOnObjectPermission; @@ -182,6 +195,7 @@ export const useGetObjectPermissionDerivedStates = ({ objectHasNoOverrideOnObjectPermission, thereAreFieldPermissionsButTheyShouldntBeTakenIntoAccountBecauseObjectPermissionsDontAllowIt, objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount, + objectHasNoOverrideButRowLevelPermissionShouldBeTakenIntoAccount, objectPermissionHasOnlyNullPermissions, objectHasOverrideOnObjectPermissions, }; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/hooks/useFilterObjectWithPermissionOverride.ts b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/hooks/useFilterObjectWithPermissionOverride.ts index 9b637c4b98..3f522eb4ec 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/hooks/useFilterObjectWithPermissionOverride.ts +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/hooks/useFilterObjectWithPermissionOverride.ts @@ -17,10 +17,12 @@ export const useFilterObjectMetadataItemsWithPermissionOverride = ({ const { objectHasOverrideOnObjectPermissions, objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount, + objectHasNoOverrideButRowLevelPermissionShouldBeTakenIntoAccount, } = getObjectPermissionDerivedStates(objectMetadataItem.id); const hasOverride = objectHasNoOverrideButFieldPermissionsShouldBeTakenIntoAccount || + objectHasNoOverrideButRowLevelPermissionShouldBeTakenIntoAccount || objectHasOverrideOnObjectPermissions; return hasOverride; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx index 0b9ff849a1..fd561eb343 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx @@ -34,8 +34,7 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ const fromAgentId = searchParams.get('fromAgent'); const currentWorkspace = useRecoilValue(currentWorkspaceState); - const hasValidEnterpriseKey = - currentWorkspace?.hasValidEnterpriseKey === true; + const settingsDraftRole = useRecoilValue( settingsDraftRoleFamilyState(roleId), ); @@ -63,8 +62,7 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ const isRowLevelPermissionPredicatesEnabled = featureFlagsMap[ FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED - ] && - (hasValidEnterpriseKey || isRLSBillingEntitlementEnabled); + ] && isRLSBillingEntitlementEnabled; const objectMetadataItem = objectMetadata.objectMetadataItem; diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts index b95fc9f296..4ae2a27c3b 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts @@ -23,11 +23,12 @@ import { BillingException, BillingExceptionCode, } from 'src/engine/core-modules/billing/billing.exception'; +import { BillingEntitlementDTO } from 'src/engine/core-modules/billing/dtos/billing-entitlement.dto'; 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 { BillingSubscriptionItemEntity } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; -import { type BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service'; @@ -177,18 +178,31 @@ export class BillingSubscriptionService { async getWorkspaceEntitlements( workspaceId: string, - ): Promise { + ): Promise { const isBillingEnabled = this.twentyConfigService.get('IS_BILLING_ENABLED'); - - if (!isBillingEnabled) { - return []; - } + const hasValidEnterpriseKey = isDefined( + this.twentyConfigService.get('ENTERPRISE_KEY'), + ); const entitlements = await this.billingEntitlementRepository.find({ where: { workspaceId }, }); - return entitlements; + const entitlementsByKey = entitlements.reduce( + (acc, entitlement) => { + acc[entitlement.key] = entitlement; + + return acc; + }, + {} as Record, + ); + + return Object.values(BillingEntitlementKey).map((key) => ({ + key, + value: + hasValidEnterpriseKey && + (!isBillingEnabled || (entitlementsByKey[key]?.value ?? false)), + })); } async getWorkspaceEntitlementByKey( diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index 538834c137..5fe1790a16 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -18,8 +18,8 @@ import assert from 'assert'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import { PermissionFlagType } from 'twenty-shared/constants'; -import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { FileFolder } from 'twenty-shared/types'; +import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import type { FileUpload } from 'graphql-upload/processRequest.mjs'; diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service.ts index d8ecf830fc..b2e6a29ea9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service.ts @@ -360,8 +360,9 @@ export class RowLevelPermissionPredicateGroupService { private async hasRowLevelPermissionFeature( workspaceId: string, ): Promise { - const isBillingEnabled = this.configService.get('IS_BILLING_ENABLED'); - const entrepriseKey = this.configService.get('ENTERPRISE_KEY'); + const hasValidEnterpriseKey = isDefined( + this.configService.get('ENTERPRISE_KEY'), + ); const isRowLevelPermissionEnabled = await this.billingService.hasEntitlement( @@ -369,11 +370,7 @@ export class RowLevelPermissionPredicateGroupService { BillingEntitlementKey.RLS, ); - if (isDefined(entrepriseKey)) { - return true; - } - - return isBillingEnabled && isRowLevelPermissionEnabled; + return hasValidEnterpriseKey && isRowLevelPermissionEnabled; } private async hasRowLevelPermissionFeatureOrThrow(workspaceId: string) { diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts index 1b09832b0a..3005558d95 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts @@ -718,8 +718,9 @@ export class RowLevelPermissionPredicateService { private async hasRowLevelPermissionFeature( workspaceId: string, ): Promise { - const isBillingEnabled = this.configService.get('IS_BILLING_ENABLED'); - const entrepriseKey = this.configService.get('ENTERPRISE_KEY'); + const hasValidEnterpriseKey = isDefined( + this.configService.get('ENTERPRISE_KEY'), + ); const isRowLevelPermissionEnabled = await this.billingService.hasEntitlement( @@ -727,11 +728,7 @@ export class RowLevelPermissionPredicateService { BillingEntitlementKey.RLS, ); - if (isDefined(entrepriseKey)) { - return true; - } - - return isBillingEnabled && isRowLevelPermissionEnabled; + return hasValidEnterpriseKey && isRowLevelPermissionEnabled; } private async hasRowLevelPermissionFeatureOrThrow(workspaceId: string) {