mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(core): Stop enforcing max numbers of API keys limit (no-changelog) (#13631)
This commit is contained in:
parent
e633e91f69
commit
1909b74350
|
@ -86,7 +86,6 @@ export interface FrontendSettings {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
publicApi: {
|
publicApi: {
|
||||||
apiKeysPerUserLimit: number;
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
latestVersion: number;
|
latestVersion: number;
|
||||||
path: string;
|
path: string;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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: '',
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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: '/',
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue