mirror of
https://github.com/Unleash/unleash.git
synced 2024-10-28 19:06:12 +01:00
feat: add resource limits for API tokens (#7510)
This PR adds the back end for API token resource limits. It adds the limit to the schema and checks the limit in the service. ## Discussion points The PAT service uses a different service and different store entirely, so I have not included testing any edge cases where PATs are included. However, that could be seen as "knowing too much". We could add tests that check both of the stores in tandem, but I think it's overkill for now.
This commit is contained in:
parent
0a2f7e5a61
commit
b2522f9199
@ -196,6 +196,7 @@ exports[`should create default config 1`] = `
|
||||
"actionSetFilterValues": 25,
|
||||
"actionSetFilters": 5,
|
||||
"actionSetsPerProject": 5,
|
||||
"apiTokens": 2000,
|
||||
"constraintValues": 250,
|
||||
"environments": 50,
|
||||
"featureEnvironmentStrategies": 30,
|
||||
|
@ -661,6 +661,10 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
||||
process.env.UNLEASH_ENVIRONMENTS_LIMIT,
|
||||
50,
|
||||
),
|
||||
apiTokens: Math.max(
|
||||
0,
|
||||
parseEnvVarNumber(process.env.UNLEASH_API_TOKENS_LIMIT, 2000),
|
||||
),
|
||||
projects: parseEnvVarNumber(process.env.UNLEASH_PROJECTS_LIMIT, 500),
|
||||
};
|
||||
|
||||
|
@ -83,6 +83,13 @@ export const resourceLimitsSchema = {
|
||||
example: 50,
|
||||
description: 'The maximum number of environments allowed.',
|
||||
},
|
||||
apiTokens: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
example: 2000,
|
||||
description:
|
||||
'The maximum number of SDK and admin API tokens you can have at the same time. This limit applies only to server-side and client-side SDK tokens and to admin tokens. Personal access tokens are not subject to this limit. The limit applies to the total number of tokens across all projects in your organization.',
|
||||
},
|
||||
projects: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
|
99
src/lib/services/api-token-service.limit.test.ts
Normal file
99
src/lib/services/api-token-service.limit.test.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { ApiTokenService } from './api-token-service';
|
||||
import { createTestConfig } from '../../test/config/test-config';
|
||||
import type { IUnleashConfig } from '../server-impl';
|
||||
import { ApiTokenType } from '../types/models/api-token';
|
||||
import FakeApiTokenStore from '../../test/fixtures/fake-api-token-store';
|
||||
import FakeEnvironmentStore from '../features/project-environments/fake-environment-store';
|
||||
import { createFakeEventsService } from '../../lib/features';
|
||||
import { ExceedsLimitError } from '../error/exceeds-limit-error';
|
||||
|
||||
const createServiceWithLimit = (limit: number) => {
|
||||
const config: IUnleashConfig = createTestConfig({
|
||||
experimental: {
|
||||
flags: {
|
||||
resourceLimits: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const apiTokenStore = new FakeApiTokenStore();
|
||||
const environmentStore = new FakeEnvironmentStore();
|
||||
environmentStore.create({
|
||||
name: 'production',
|
||||
type: 'production',
|
||||
enabled: true,
|
||||
protected: true,
|
||||
sortOrder: 1,
|
||||
});
|
||||
|
||||
const eventService = createFakeEventsService(config);
|
||||
|
||||
config.resourceLimits.apiTokens = limit;
|
||||
|
||||
const service = new ApiTokenService(
|
||||
{ apiTokenStore, environmentStore },
|
||||
config,
|
||||
eventService,
|
||||
);
|
||||
|
||||
return service;
|
||||
};
|
||||
|
||||
test('Should allow you to create tokens up to and including the limit', async () => {
|
||||
const limit = 3;
|
||||
const service = createServiceWithLimit(limit);
|
||||
|
||||
for (let i = 0; i < limit; i++) {
|
||||
await service.createApiTokenWithProjects(
|
||||
{
|
||||
tokenName: `token-${i}`,
|
||||
type: ApiTokenType.CLIENT,
|
||||
environment: 'production',
|
||||
projects: ['*'],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
username: 'audit user',
|
||||
ip: '127.0.0.1',
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([ApiTokenType.ADMIN, ApiTokenType.CLIENT, ApiTokenType.FRONTEND])(
|
||||
"Should prevent you from creating %s tokens when you're already at the limit",
|
||||
async (tokenType) => {
|
||||
const limit = 1;
|
||||
const service = createServiceWithLimit(limit);
|
||||
const auditUser = {
|
||||
id: 1,
|
||||
username: 'audit user',
|
||||
ip: '127.0.0.1',
|
||||
};
|
||||
|
||||
await service.createApiTokenWithProjects(
|
||||
{
|
||||
tokenName: 'token-1',
|
||||
type: ApiTokenType.CLIENT,
|
||||
environment: 'production',
|
||||
projects: ['*'],
|
||||
},
|
||||
auditUser,
|
||||
);
|
||||
|
||||
const environment =
|
||||
tokenType === ApiTokenType.ADMIN ? '*' : 'production';
|
||||
|
||||
await expect(
|
||||
service.createApiTokenWithProjects(
|
||||
{
|
||||
tokenName: 'exceeds-limit',
|
||||
type: tokenType,
|
||||
environment,
|
||||
projects: ['*'],
|
||||
},
|
||||
auditUser,
|
||||
),
|
||||
).rejects.toThrow(ExceedsLimitError);
|
||||
},
|
||||
);
|
@ -33,6 +33,8 @@ import type EventService from '../features/events/event-service';
|
||||
import { addMinutes, isPast } from 'date-fns';
|
||||
import metricsHelper from '../util/metrics-helper';
|
||||
import { FUNCTION_TIME } from '../metric-events';
|
||||
import type { ResourceLimitsSchema } from '../openapi';
|
||||
import { ExceedsLimitError } from '../error/exceeds-limit-error';
|
||||
|
||||
const resolveTokenPermissions = (tokenType: string) => {
|
||||
if (tokenType === ApiTokenType.ADMIN) {
|
||||
@ -69,6 +71,8 @@ export class ApiTokenService {
|
||||
|
||||
private timer: Function;
|
||||
|
||||
private resourceLimits: ResourceLimitsSchema;
|
||||
|
||||
constructor(
|
||||
{
|
||||
apiTokenStore,
|
||||
@ -76,7 +80,11 @@ export class ApiTokenService {
|
||||
}: Pick<IUnleashStores, 'apiTokenStore' | 'environmentStore'>,
|
||||
config: Pick<
|
||||
IUnleashConfig,
|
||||
'getLogger' | 'authentication' | 'flagResolver' | 'eventBus'
|
||||
| 'getLogger'
|
||||
| 'authentication'
|
||||
| 'flagResolver'
|
||||
| 'eventBus'
|
||||
| 'resourceLimits'
|
||||
>,
|
||||
eventService: EventService,
|
||||
) {
|
||||
@ -85,6 +93,7 @@ export class ApiTokenService {
|
||||
this.environmentStore = environmentStore;
|
||||
this.flagResolver = config.flagResolver;
|
||||
this.logger = config.getLogger('/services/api-token-service.ts');
|
||||
this.resourceLimits = config.resourceLimits;
|
||||
if (!this.flagResolver.isEnabled('useMemoizedActiveTokens')) {
|
||||
// This is probably not needed because the scheduler will run it
|
||||
this.fetchActiveTokens();
|
||||
@ -286,11 +295,23 @@ export class ApiTokenService {
|
||||
const environments = await this.environmentStore.getAll();
|
||||
validateApiTokenEnvironment(newToken, environments);
|
||||
|
||||
await this.validateApiTokenLimit();
|
||||
|
||||
const secret = this.generateSecretKey(newToken);
|
||||
const createNewToken = { ...newToken, secret };
|
||||
return this.insertNewApiToken(createNewToken, auditUser);
|
||||
}
|
||||
|
||||
private async validateApiTokenLimit() {
|
||||
if (this.flagResolver.isEnabled('resourceLimits')) {
|
||||
const currentTokenCount = await this.store.count();
|
||||
const limit = this.resourceLimits.apiTokens;
|
||||
if (currentTokenCount >= limit) {
|
||||
throw new ExceedsLimitError('api token', limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove this service method after embedded proxy has been released in
|
||||
// 4.16.0
|
||||
public async createMigratedProxyApiToken(
|
||||
|
Loading…
Reference in New Issue
Block a user