first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import { testDb, randomValidPassword, mockInstance } from '@n8n/backend-test-utils';
|
||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { User } from '@n8n/db';
|
||||
import { ApiKeyRepository, GLOBAL_MEMBER_ROLE } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { getApiKeyScopesForRole } from '@n8n/permissions';
|
||||
|
||||
import { License } from '@/license';
|
||||
|
||||
import { createUser } from './shared/db/users';
|
||||
import * as utils from './shared/utils/';
|
||||
import { LicenseMocker } from './shared/license';
|
||||
|
||||
const testServer = utils.setupTestServer({ endpointGroups: ['apiKeys'] });
|
||||
let licenseMocker: LicenseMocker;
|
||||
|
||||
beforeAll(() => {
|
||||
// Setup License mocker
|
||||
licenseMocker = new LicenseMocker();
|
||||
licenseMocker.mock(Container.get(License));
|
||||
licenseMocker.enable(LICENSE_FEATURES.API_KEY_SCOPES);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['User']);
|
||||
mockInstance(GlobalConfig, { publicApi: { disabled: false } });
|
||||
// Reset to licensed by default
|
||||
licenseMocker.reset();
|
||||
licenseMocker.enable(LICENSE_FEATURES.API_KEY_SCOPES);
|
||||
});
|
||||
|
||||
describe('API Key Scopes License Enforcement', () => {
|
||||
let member: User;
|
||||
|
||||
beforeEach(async () => {
|
||||
member = await createUser({ role: GLOBAL_MEMBER_ROLE, password: randomValidPassword() });
|
||||
});
|
||||
|
||||
describe('When feat:apiKeyScopes is NOT licensed', () => {
|
||||
test('POST /api-keys should ignore custom scopes and assign all available scopes', async () => {
|
||||
licenseMocker.disable(LICENSE_FEATURES.API_KEY_SCOPES);
|
||||
|
||||
const expiresAt = Date.now() + 1000;
|
||||
|
||||
const response = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'Test API Key', expiresAt, scopes: ['workflow:create'] });
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const storedApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
||||
userId: member.id,
|
||||
});
|
||||
|
||||
// Should have all available scopes for member role, not just the requested one
|
||||
const expectedScopes = getApiKeyScopesForRole(member);
|
||||
expect(storedApiKey.scopes.sort()).toEqual(expectedScopes.sort());
|
||||
expect(storedApiKey.scopes).not.toEqual(['workflow:create']);
|
||||
expect(storedApiKey.scopes.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
test('PATCH /api-keys/:id should ignore custom scopes and assign all available scopes', async () => {
|
||||
licenseMocker.disable(LICENSE_FEATURES.API_KEY_SCOPES);
|
||||
|
||||
const createResponse = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'Original Label', expiresAt: null, scopes: ['workflow:create'] });
|
||||
|
||||
const apiKeyId = createResponse.body.data.id;
|
||||
|
||||
const updateResponse = await testServer
|
||||
.authAgentFor(member)
|
||||
.patch(`/api-keys/${apiKeyId}`)
|
||||
.send({ label: 'Updated Label', scopes: ['workflow:create'] });
|
||||
|
||||
expect(updateResponse.statusCode).toBe(200);
|
||||
|
||||
const updatedApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
||||
id: apiKeyId,
|
||||
});
|
||||
|
||||
// Should still have all available scopes, not the custom one
|
||||
const expectedScopes = getApiKeyScopesForRole(member);
|
||||
expect(updatedApiKey.scopes.sort()).toEqual(expectedScopes.sort());
|
||||
expect(updatedApiKey.scopes).not.toEqual(['workflow:create']);
|
||||
expect(updatedApiKey.label).toBe('Updated Label');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When feat:apiKeyScopes IS licensed', () => {
|
||||
test('POST /api-keys should honor custom scopes', async () => {
|
||||
const expiresAt = Date.now() + 1000;
|
||||
|
||||
const response = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'Test API Key', expiresAt, scopes: ['workflow:create'] });
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const storedApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
||||
userId: member.id,
|
||||
});
|
||||
|
||||
// Should have only the requested scope
|
||||
expect(storedApiKey.scopes).toEqual(['workflow:create']);
|
||||
});
|
||||
|
||||
test('PATCH /api-keys/:id should honor custom scopes', async () => {
|
||||
const createResponse = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'Original Label', expiresAt: null, scopes: ['workflow:create'] });
|
||||
|
||||
const apiKeyId = createResponse.body.data.id;
|
||||
|
||||
const updateResponse = await testServer
|
||||
.authAgentFor(member)
|
||||
.patch(`/api-keys/${apiKeyId}`)
|
||||
.send({ label: 'Updated Label', scopes: ['workflow:list'] });
|
||||
|
||||
expect(updateResponse.statusCode).toBe(200);
|
||||
|
||||
const updatedApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
||||
id: apiKeyId,
|
||||
});
|
||||
|
||||
// Should have the new custom scope
|
||||
expect(updatedApiKey.scopes).toEqual(['workflow:list']);
|
||||
expect(updatedApiKey.label).toBe('Updated Label');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user