fix(core): Stop enforcing max numbers of API keys limit (no-changelog) (#13631)

This commit is contained in:
Ricardo Espinoza 2025-03-03 14:32:50 +01:00 committed by GitHub
parent e633e91f69
commit 1909b74350
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 2 additions and 55 deletions

View file

@ -86,7 +86,6 @@ export interface FrontendSettings {
}; };
}; };
publicApi: { publicApi: {
apiKeysPerUserLimit: number;
enabled: boolean; enabled: boolean;
latestVersion: number; latestVersion: number;
path: string; path: string;

View file

@ -104,7 +104,6 @@ export const LICENSE_QUOTAS = {
WORKFLOW_HISTORY_PRUNE_LIMIT: 'quota:workflowHistoryPrune', WORKFLOW_HISTORY_PRUNE_LIMIT: 'quota:workflowHistoryPrune',
TEAM_PROJECT_LIMIT: 'quota:maxTeamProjects', TEAM_PROJECT_LIMIT: 'quota:maxTeamProjects',
AI_CREDITS: 'quota:aiCredits', AI_CREDITS: 'quota:aiCredits',
API_KEYS_PER_USER_LIMIT: 'quota:apiKeysPerUserLimit',
} as const; } as const;
export const UNLIMITED_LICENSE_QUOTA = -1; export const UNLIMITED_LICENSE_QUOTA = -1;

View file

@ -3,9 +3,7 @@ import { mock } from 'jest-mock-extended';
import type { ApiKey } from '@/databases/entities/api-key'; import type { ApiKey } from '@/databases/entities/api-key';
import type { User } from '@/databases/entities/user'; import type { User } from '@/databases/entities/user';
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { License } from '@/license';
import type { AuthenticatedRequest } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import { PublicApiKeyService } from '@/services/public-api-key.service'; import { PublicApiKeyService } from '@/services/public-api-key.service';
import { mockInstance } from '@test/mocking'; import { mockInstance } from '@test/mocking';
@ -15,8 +13,7 @@ import { ApiKeysController } from '../api-keys.controller';
describe('ApiKeysController', () => { describe('ApiKeysController', () => {
const publicApiKeyService = mockInstance(PublicApiKeyService); const publicApiKeyService = mockInstance(PublicApiKeyService);
const eventService = mockInstance(EventService); const eventService = mockInstance(EventService);
mockInstance(ApiKeyRepository);
mockInstance(License);
const controller = Container.get(ApiKeysController); const controller = Container.get(ApiKeysController);
let req: AuthenticatedRequest; let req: AuthenticatedRequest;

View file

@ -1,11 +1,8 @@
import { CreateApiKeyRequestDto, UpdateApiKeyRequestDto } from '@n8n/api-types'; import { CreateApiKeyRequestDto, UpdateApiKeyRequestDto } from '@n8n/api-types';
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
import { Body, Delete, Get, Param, Patch, Post, RestController } from '@/decorators'; import { Body, Delete, Get, Param, Patch, Post, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { License } from '@/license';
import { isApiEnabled } from '@/public-api'; import { isApiEnabled } from '@/public-api';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { PublicApiKeyService } from '@/services/public-api-key.service'; import { PublicApiKeyService } from '@/services/public-api-key.service';
@ -23,8 +20,6 @@ export class ApiKeysController {
constructor( constructor(
private readonly eventService: EventService, private readonly eventService: EventService,
private readonly publicApiKeyService: PublicApiKeyService, private readonly publicApiKeyService: PublicApiKeyService,
private readonly apiKeysRepository: ApiKeyRepository,
private readonly license: License,
) {} ) {}
/** /**
@ -36,12 +31,6 @@ export class ApiKeysController {
_res: Response, _res: Response,
@Body { label, expiresAt }: CreateApiKeyRequestDto, @Body { label, expiresAt }: CreateApiKeyRequestDto,
) { ) {
const currentNumberOfApiKeys = await this.apiKeysRepository.countBy({ userId: req.user.id });
if (currentNumberOfApiKeys >= this.license.getApiKeysPerUserLimit()) {
throw new BadRequestError('You have reached the maximum number of API keys allowed.');
}
const newApiKey = await this.publicApiKeyService.createPublicApiKeyForUser(req.user, { const newApiKey = await this.publicApiKeyService.createPublicApiKeyForUser(req.user, {
label, label,
expiresAt, expiresAt,

View file

@ -110,7 +110,6 @@ export class E2EController {
[LICENSE_QUOTAS.WORKFLOW_HISTORY_PRUNE_LIMIT]: -1, [LICENSE_QUOTAS.WORKFLOW_HISTORY_PRUNE_LIMIT]: -1,
[LICENSE_QUOTAS.TEAM_PROJECT_LIMIT]: 0, [LICENSE_QUOTAS.TEAM_PROJECT_LIMIT]: 0,
[LICENSE_QUOTAS.AI_CREDITS]: 0, [LICENSE_QUOTAS.AI_CREDITS]: 0,
[LICENSE_QUOTAS.API_KEYS_PER_USER_LIMIT]: 1,
}; };
private numericFeatures: Record<NumericLicenseFeature, number> = { private numericFeatures: Record<NumericLicenseFeature, number> = {
@ -124,8 +123,6 @@ export class E2EController {
[LICENSE_QUOTAS.TEAM_PROJECT_LIMIT]: [LICENSE_QUOTAS.TEAM_PROJECT_LIMIT]:
E2EController.numericFeaturesDefaults[LICENSE_QUOTAS.TEAM_PROJECT_LIMIT], E2EController.numericFeaturesDefaults[LICENSE_QUOTAS.TEAM_PROJECT_LIMIT],
[LICENSE_QUOTAS.AI_CREDITS]: E2EController.numericFeaturesDefaults[LICENSE_QUOTAS.AI_CREDITS], [LICENSE_QUOTAS.AI_CREDITS]: E2EController.numericFeaturesDefaults[LICENSE_QUOTAS.AI_CREDITS],
[LICENSE_QUOTAS.API_KEYS_PER_USER_LIMIT]:
E2EController.numericFeaturesDefaults[LICENSE_QUOTAS.API_KEYS_PER_USER_LIMIT],
}; };
constructor( constructor(

View file

@ -337,10 +337,6 @@ export class License {
return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA; return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
} }
getApiKeysPerUserLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.API_KEYS_PER_USER_LIMIT) ?? 1;
}
getTriggerLimit() { getTriggerLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? UNLIMITED_LICENSE_QUOTA; return this.getFeatureValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
} }

View file

@ -145,7 +145,6 @@ export class FrontendService {
}, },
}, },
publicApi: { publicApi: {
apiKeysPerUserLimit: this.license.getApiKeysPerUserLimit(),
enabled: isApiEnabled(), enabled: isApiEnabled(),
latestVersion: 1, latestVersion: 1,
path: this.globalConfig.publicApi.path, path: this.globalConfig.publicApi.path,

View file

@ -4,7 +4,6 @@ import { Container } from '@n8n/di';
import type { User } from '@/databases/entities/user'; import type { User } from '@/databases/entities/user';
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository'; import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
import { License } from '@/license';
import { PublicApiKeyService } from '@/services/public-api-key.service'; import { PublicApiKeyService } from '@/services/public-api-key.service';
import { mockInstance } from '@test/mocking'; import { mockInstance } from '@test/mocking';
@ -14,10 +13,6 @@ import * as testDb from './shared/test-db';
import type { SuperAgentTest } from './shared/types'; import type { SuperAgentTest } from './shared/types';
import * as utils from './shared/utils/'; import * as utils from './shared/utils/';
const license = mockInstance(License);
license.getApiKeysPerUserLimit.mockImplementation(() => 2);
const testServer = utils.setupTestServer({ endpointGroups: ['apiKeys'] }); const testServer = utils.setupTestServer({ endpointGroups: ['apiKeys'] });
let publicApiKeyService: PublicApiKeyService; let publicApiKeyService: PublicApiKeyService;
@ -119,17 +114,6 @@ describe('Owner shell', () => {
expect(newApiKey.rawApiKey).toBeDefined(); expect(newApiKey.rawApiKey).toBeDefined();
}); });
test('POST /api-keys should fail if max number of API keys reached', async () => {
await testServer.authAgentFor(ownerShell).post('/api-keys').send({ label: 'My API Key' });
const secondApiKey = await testServer
.authAgentFor(ownerShell)
.post('/api-keys')
.send({ label: 'My API Key' });
expect(secondApiKey.statusCode).toBe(400);
});
test('GET /api-keys should fetch the api key redacted', async () => { test('GET /api-keys should fetch the api key redacted', async () => {
const expirationDateInTheFuture = Date.now() + 1000; const expirationDateInTheFuture = Date.now() + 1000;

View file

@ -63,7 +63,6 @@ export const defaultSettings: FrontendSettings = {
enabled: false, enabled: false,
}, },
publicApi: { publicApi: {
apiKeysPerUserLimit: 0,
enabled: false, enabled: false,
latestVersion: 0, latestVersion: 0,
path: '', path: '',

View file

@ -4,14 +4,12 @@ import { useRootStore } from '@/stores/root.store';
import * as publicApiApi from '@/api/api-keys'; import * as publicApiApi from '@/api/api-keys';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useSettingsStore } from './settings.store';
import type { ApiKey, CreateApiKeyRequestDto, UpdateApiKeyRequestDto } from '@n8n/api-types'; import type { ApiKey, CreateApiKeyRequestDto, UpdateApiKeyRequestDto } from '@n8n/api-types';
export const useApiKeysStore = defineStore(STORES.API_KEYS, () => { export const useApiKeysStore = defineStore(STORES.API_KEYS, () => {
const apiKeys = ref<ApiKey[]>([]); const apiKeys = ref<ApiKey[]>([]);
const rootStore = useRootStore(); const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const apiKeysSortByCreationDate = computed(() => const apiKeysSortByCreationDate = computed(() =>
apiKeys.value.sort((a, b) => b.createdAt.localeCompare(a.createdAt)), apiKeys.value.sort((a, b) => b.createdAt.localeCompare(a.createdAt)),
@ -27,10 +25,6 @@ export const useApiKeysStore = defineStore(STORES.API_KEYS, () => {
); );
}); });
const canAddMoreApiKeys = computed(
() => apiKeys.value.length < settingsStore.api.apiKeysPerUserLimit,
);
const getAndCacheApiKeys = async () => { const getAndCacheApiKeys = async () => {
if (apiKeys.value.length) return apiKeys.value; if (apiKeys.value.length) return apiKeys.value;
apiKeys.value = await publicApiApi.getApiKeys(rootStore.restApiContext); apiKeys.value = await publicApiApi.getApiKeys(rootStore.restApiContext);
@ -62,6 +56,5 @@ export const useApiKeysStore = defineStore(STORES.API_KEYS, () => {
apiKeysSortByCreationDate, apiKeysSortByCreationDate,
apiKeysById, apiKeysById,
apiKeys, apiKeys,
canAddMoreApiKeys,
}; };
}); });

View file

@ -32,7 +32,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
}); });
const templatesEndpointHealthy = ref(false); const templatesEndpointHealthy = ref(false);
const api = ref({ const api = ref({
apiKeysPerUserLimit: 0,
enabled: false, enabled: false,
latestVersion: 0, latestVersion: 0,
path: '/', path: '/',

View file

@ -179,11 +179,7 @@ function onEdit(id: string) {
</n8n-link> </n8n-link>
</div> </div>
<div class="mt-m text-right"> <div class="mt-m text-right">
<n8n-button <n8n-button size="large" @click="onCreateApiKey">
size="large"
:disabled="!apiKeysStore.canAddMoreApiKeys"
@click="onCreateApiKey"
>
{{ i18n.baseText('settings.api.create.button') }} {{ i18n.baseText('settings.api.create.button') }}
</n8n-button> </n8n-button>
</div> </div>