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:
Weiko
2026-01-20 17:06:37 +01:00
committed by GitHub
parent 579c59bd11
commit b6635ba272
26 changed files with 314 additions and 24 deletions
@@ -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: [
@@ -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,
};
@@ -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',