diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 9f62e87359bd..2784887086bd 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -196,6 +196,7 @@ exports[`should create default config 1`] = ` "actionSetFilterValues": 25, "actionSetFilters": 5, "actionSetsPerProject": 5, + "constraintValues": 250, "environments": 50, "featureEnvironmentStrategies": 30, "segmentValues": 1000, diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index fd6c4ef3dc93..cad1a4e6519c 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -653,6 +653,10 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { process.env.UNLEASH_FEATURE_ENVIRONMENT_STRATEGIES_LIMIT, 30, ), + constraintValues: parseEnvVarNumber( + process.env.UNLEASH_CONSTRAINT_VALUES_LIMIT, + 250, + ), environments: parseEnvVarNumber( process.env.UNLEASH_ENVIRONMENTS_LIMIT, 50, diff --git a/src/lib/features/feature-toggle/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts index 9ccfe9010599..eb563bb1a56c 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -366,15 +366,14 @@ class FeatureToggleService { } } - async validateStrategyLimit( - featureEnv: { - projectId: string; - environment: string; - featureName: string; - }, - limit: number, - ) { + async validateStrategyLimit(featureEnv: { + projectId: string; + environment: string; + featureName: string; + }) { if (!this.flagResolver.isEnabled('resourceLimits')) return; + + const limit = this.resourceLimits.featureEnvironmentStrategies; const existingCount = ( await this.featureStrategiesStore.getStrategiesForFeatureEnv( featureEnv.projectId, @@ -387,6 +386,22 @@ class FeatureToggleService { } } + validateContraintValuesLimit(updatedConstrains: IConstraint[]) { + if (!this.flagResolver.isEnabled('resourceLimits')) return; + + const limit = this.resourceLimits.constraintValues; + const constraintOverLimit = updatedConstrains.find( + (constraint) => + Array.isArray(constraint.values) && + constraint.values?.length > limit, + ); + if (constraintOverLimit) { + throw new BadDataError( + `Constraint values limit of ${limit} is exceeded for ${constraintOverLimit.contextName}.`, + ); + } + } + async validateStrategyType( strategyName: string | undefined, ): Promise { @@ -632,6 +647,7 @@ class FeatureToggleService { strategyConfig.constraints && strategyConfig.constraints.length > 0 ) { + this.validateContraintValuesLimit(strategyConfig.constraints); strategyConfig.constraints = await this.validateConstraints( strategyConfig.constraints, ); @@ -653,10 +669,11 @@ class FeatureToggleService { strategyConfig.variants = fixedVariants; } - await this.validateStrategyLimit( - { featureName, projectId, environment }, - this.resourceLimits.featureEnvironmentStrategies, - ); + await this.validateStrategyLimit({ + featureName, + projectId, + environment, + }); try { const newFeatureStrategy = @@ -789,6 +806,7 @@ class FeatureToggleService { if (existingStrategy.id === id) { if (updates.constraints && updates.constraints.length > 0) { + this.validateContraintValuesLimit(updates.constraints); updates.constraints = await this.validateConstraints( updates.constraints, ); diff --git a/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts b/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts index d0ade8abd310..4a29d0cae350 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts @@ -1,6 +1,7 @@ import { createFakeFeatureToggleService } from '../createFeatureToggleService'; import type { IAuditUser, + IConstraint, IFlagResolver, IStrategyConfig, IUnleashConfig, @@ -43,3 +44,41 @@ test('Should not allow to exceed strategy limit', async () => { `Strategy limit of ${LIMIT} exceeded`, ); }); + +test('Should not allow to exceed constraint values limit', async () => { + const LIMIT = 3; + const { featureToggleService, featureToggleStore } = + createFakeFeatureToggleService({ + getLogger, + flagResolver: alwaysOnFlagResolver, + resourceLimits: { + constraintValues: LIMIT, + }, + } as unknown as IUnleashConfig); + + const addStrategyWithConstraints = (constraints: IConstraint[]) => + featureToggleService.unprotectedCreateStrategy( + { + name: 'default', + featureName: 'feature', + constraints, + } as IStrategyConfig, + { projectId: 'default', featureName: 'feature' } as any, + {} as IAuditUser, + ); + await featureToggleStore.create('default', { + name: 'feature', + createdByUserId: 1, + }); + await expect(() => + addStrategyWithConstraints([ + { + contextName: 'userId', + operator: 'IN', + values: ['1', '2', '3', '4'], + }, + ]), + ).rejects.toThrow( + `Constraint values limit of ${LIMIT} is exceeded for userId`, + ); +}); diff --git a/src/lib/openapi/spec/resource-limits-schema.ts b/src/lib/openapi/spec/resource-limits-schema.ts index 7280bb153fcc..cdb3be1d43ea 100644 --- a/src/lib/openapi/spec/resource-limits-schema.ts +++ b/src/lib/openapi/spec/resource-limits-schema.ts @@ -14,6 +14,7 @@ export const resourceLimitsSchema = { 'signalEndpoints', 'signalTokensPerEndpoint', 'featureEnvironmentStrategies', + 'constraintValues', 'environments', ], additionalProperties: false, @@ -69,6 +70,12 @@ export const resourceLimitsSchema = { description: 'The maximum number of feature environment strategies allowed.', }, + constraintValues: { + type: 'integer', + example: 250, + description: + 'The maximum number of values for a single constraint.', + }, environments: { type: 'integer', minimum: 1,