mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(core): Add scopes to API Keys (#14176)
Co-authored-by: Charlie Kolb <charlie@n8n.io> Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
import type { ApiKeyWithRawValue } from '@n8n/api-types';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { ApiKeyScope } from '@n8n/permissions';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
|
||||
import type { License } from '@/license';
|
||||
import { getApiKeyScopesForRole, getOwnerOnlyApiKeyScopes } from '@/public-api/permissions.ee';
|
||||
import { PublicApiKeyService } from '@/services/public-api-key.service';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
@@ -15,6 +19,7 @@ import * as utils from './shared/utils/';
|
||||
|
||||
const testServer = utils.setupTestServer({ endpointGroups: ['apiKeys'] });
|
||||
let publicApiKeyService: PublicApiKeyService;
|
||||
const license = mock<License>();
|
||||
|
||||
beforeAll(() => {
|
||||
publicApiKeyService = Container.get(PublicApiKeyService);
|
||||
@@ -60,7 +65,7 @@ describe('Owner shell', () => {
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt: null });
|
||||
.send({ label: 'My API Key', expiresAt: null, scopes: ['workflow:create'] });
|
||||
|
||||
const newApiKey = newApiKeyResponse.body.data as ApiKeyWithRawValue;
|
||||
|
||||
@@ -78,19 +83,28 @@ describe('Owner shell', () => {
|
||||
apiKey: newApiKey.rawApiKey,
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
|
||||
expect(newApiKey.expiresAt).toBeNull();
|
||||
expect(newApiKey.rawApiKey).toBeDefined();
|
||||
});
|
||||
|
||||
test('POST /api-keys should fail to create api key with invalid scope', async () => {
|
||||
await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt: null, scopes: ['wrong'] })
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test('POST /api-keys should create an api key with expiration', async () => {
|
||||
const expiresAt = Date.now() + 1000;
|
||||
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt });
|
||||
.send({ label: 'My API Key', expiresAt, scopes: ['workflow:create'] });
|
||||
|
||||
const newApiKey = newApiKeyResponse.body.data as ApiKeyWithRawValue;
|
||||
|
||||
@@ -108,24 +122,138 @@ describe('Owner shell', () => {
|
||||
apiKey: newApiKey.rawApiKey,
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
|
||||
expect(newApiKey.expiresAt).toBe(expiresAt);
|
||||
expect(newApiKey.rawApiKey).toBeDefined();
|
||||
});
|
||||
|
||||
test("POST /api-keys should create an api key with scopes allow in the user's role", async () => {
|
||||
const expiresAt = Date.now() + 1000;
|
||||
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt, scopes: ['user:create'] });
|
||||
|
||||
const newApiKey = newApiKeyResponse.body.data as ApiKeyWithRawValue;
|
||||
|
||||
expect(newApiKeyResponse.statusCode).toBe(200);
|
||||
expect(newApiKey).toBeDefined();
|
||||
|
||||
const newStoredApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
||||
userId: ownerShell.id,
|
||||
});
|
||||
|
||||
expect(newStoredApiKey).toEqual({
|
||||
id: expect.any(String),
|
||||
label: 'My API Key',
|
||||
userId: ownerShell.id,
|
||||
apiKey: newApiKey.rawApiKey,
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
scopes: ['user:create'],
|
||||
});
|
||||
|
||||
expect(newApiKey.expiresAt).toBe(expiresAt);
|
||||
expect(newApiKey.rawApiKey).toBeDefined();
|
||||
});
|
||||
|
||||
test('PATCH /api-keys should update API key label', async () => {
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt: null, scopes: ['user:create'] });
|
||||
|
||||
const newApiKey = newApiKeyResponse.body.data as ApiKeyWithRawValue;
|
||||
|
||||
await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.patch(`/api-keys/${newApiKey.id}`)
|
||||
.send({ label: 'updated label', scopes: ['user:create'] });
|
||||
|
||||
const newStoredApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
||||
userId: ownerShell.id,
|
||||
});
|
||||
|
||||
expect(newStoredApiKey).toEqual({
|
||||
id: expect.any(String),
|
||||
label: 'updated label',
|
||||
userId: ownerShell.id,
|
||||
apiKey: newApiKey.rawApiKey,
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
scopes: ['user:create'],
|
||||
});
|
||||
});
|
||||
|
||||
test('PATCH /api-keys should update API key scopes', async () => {
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt: null, scopes: ['user:create'] });
|
||||
|
||||
const newApiKey = newApiKeyResponse.body.data as ApiKeyWithRawValue;
|
||||
|
||||
await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.patch(`/api-keys/${newApiKey.id}`)
|
||||
.send({ label: 'updated label', scopes: ['user:create', 'workflow:create'] });
|
||||
|
||||
const newStoredApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
||||
userId: ownerShell.id,
|
||||
});
|
||||
|
||||
expect(newStoredApiKey).toEqual({
|
||||
id: expect.any(String),
|
||||
label: 'updated label',
|
||||
userId: ownerShell.id,
|
||||
apiKey: newApiKey.rawApiKey,
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
scopes: ['user:create', 'workflow:create'],
|
||||
});
|
||||
});
|
||||
|
||||
test('PATCH /api-keys should not modify API key expiration', async () => {
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt: null, scopes: ['user:create'] });
|
||||
|
||||
const newApiKey = newApiKeyResponse.body.data as ApiKeyWithRawValue;
|
||||
|
||||
await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.patch(`/api-keys/${newApiKey.id}`)
|
||||
.send({ label: 'updated label', expiresAt: 123, scopes: ['user:create'] });
|
||||
|
||||
const getApiKeysResponse = await testServer.authAgentFor(ownerShell).get('/api-keys');
|
||||
|
||||
const allApiKeys = getApiKeysResponse.body.data as ApiKeyWithRawValue[];
|
||||
|
||||
const updatedApiKey = allApiKeys.find((apiKey) => apiKey.id === newApiKey.id);
|
||||
|
||||
expect(updatedApiKey?.expiresAt).toBe(null);
|
||||
});
|
||||
|
||||
test('GET /api-keys should fetch the api key redacted', async () => {
|
||||
const expirationDateInTheFuture = Date.now() + 1000;
|
||||
|
||||
const apiKeyWithNoExpiration = await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt: null });
|
||||
.send({ label: 'My API Key', expiresAt: null, scopes: ['workflow:create'] });
|
||||
|
||||
const apiKeyWithExpiration = await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key 2', expiresAt: expirationDateInTheFuture });
|
||||
.send({
|
||||
label: 'My API Key 2',
|
||||
expiresAt: expirationDateInTheFuture,
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
|
||||
const retrieveAllApiKeysResponse = await testServer.authAgentFor(ownerShell).get('/api-keys');
|
||||
|
||||
@@ -139,6 +267,7 @@ describe('Owner shell', () => {
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
expiresAt: expirationDateInTheFuture,
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
|
||||
expect(retrieveAllApiKeysResponse.body.data[0]).toEqual({
|
||||
@@ -149,6 +278,7 @@ describe('Owner shell', () => {
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
expiresAt: null,
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,7 +286,7 @@ describe('Owner shell', () => {
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt: null });
|
||||
.send({ label: 'My API Key', expiresAt: null, scopes: ['workflow:create'] });
|
||||
|
||||
const deleteApiKeyResponse = await testServer
|
||||
.authAgentFor(ownerShell)
|
||||
@@ -167,6 +297,16 @@ describe('Owner shell', () => {
|
||||
expect(deleteApiKeyResponse.body.data.success).toBe(true);
|
||||
expect(retrieveAllApiKeysResponse.body.data.length).toBe(0);
|
||||
});
|
||||
|
||||
test('GET /api-keys/scopes should return scopes for the role', async () => {
|
||||
const apiKeyScopesResponse = await testServer.authAgentFor(ownerShell).get('/api-keys/scopes');
|
||||
|
||||
const scopes = apiKeyScopesResponse.body.data as ApiKeyScope[];
|
||||
|
||||
const scopesForRole = getApiKeyScopesForRole(ownerShell.role);
|
||||
|
||||
expect(scopes).toEqual(scopesForRole);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Member', () => {
|
||||
@@ -185,7 +325,7 @@ describe('Member', () => {
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt: null });
|
||||
.send({ label: 'My API Key', expiresAt: null, scopes: ['workflow:create'] });
|
||||
|
||||
expect(newApiKeyResponse.statusCode).toBe(200);
|
||||
expect(newApiKeyResponse.body.data.apiKey).toBeDefined();
|
||||
@@ -202,6 +342,7 @@ describe('Member', () => {
|
||||
apiKey: newApiKeyResponse.body.data.rawApiKey,
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
|
||||
expect(newApiKeyResponse.body.data.expiresAt).toBeNull();
|
||||
@@ -214,7 +355,7 @@ describe('Member', () => {
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt });
|
||||
.send({ label: 'My API Key', expiresAt, scopes: ['workflow:create'] });
|
||||
|
||||
const newApiKey = newApiKeyResponse.body.data as ApiKeyWithRawValue;
|
||||
|
||||
@@ -232,21 +373,57 @@ describe('Member', () => {
|
||||
apiKey: newApiKey.rawApiKey,
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
|
||||
expect(newApiKey.expiresAt).toBe(expiresAt);
|
||||
expect(newApiKey.rawApiKey).toBeDefined();
|
||||
});
|
||||
|
||||
test('POST /api-keys should fail if max number of API keys reached', async () => {
|
||||
await testServer.authAgentFor(member).post('/api-keys').send({ label: 'My API Key' });
|
||||
test("POST /api-keys should create an api key with scopes allowed in the user's role", async () => {
|
||||
const expiresAt = Date.now() + 1000;
|
||||
license.isApiKeyScopesEnabled.mockReturnValue(true);
|
||||
|
||||
const secondApiKey = await testServer
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key' });
|
||||
.send({ label: 'My API Key', expiresAt, scopes: ['workflow:create'] });
|
||||
|
||||
expect(secondApiKey.statusCode).toBe(400);
|
||||
const newApiKey = newApiKeyResponse.body.data as ApiKeyWithRawValue;
|
||||
|
||||
expect(newApiKeyResponse.statusCode).toBe(200);
|
||||
expect(newApiKey).toBeDefined();
|
||||
|
||||
const newStoredApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
||||
userId: member.id,
|
||||
});
|
||||
|
||||
expect(newStoredApiKey).toEqual({
|
||||
id: expect.any(String),
|
||||
label: 'My API Key',
|
||||
userId: member.id,
|
||||
apiKey: newApiKey.rawApiKey,
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
|
||||
expect(newApiKey.expiresAt).toBe(expiresAt);
|
||||
expect(newApiKey.rawApiKey).toBeDefined();
|
||||
});
|
||||
|
||||
test("POST /api-keys should fail to create api key with scopes not allowed in the user's role", async () => {
|
||||
const expiresAt = Date.now() + 1000;
|
||||
license.isApiKeyScopesEnabled.mockReturnValue(true);
|
||||
|
||||
const notAllowedScope = getOwnerOnlyApiKeyScopes()[0];
|
||||
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt, scopes: [notAllowedScope] });
|
||||
|
||||
expect(newApiKeyResponse.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('GET /api-keys should fetch the api key redacted', async () => {
|
||||
@@ -255,12 +432,16 @@ describe('Member', () => {
|
||||
const apiKeyWithNoExpiration = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt: null });
|
||||
.send({ label: 'My API Key', expiresAt: null, scopes: ['workflow:create'] });
|
||||
|
||||
const apiKeyWithExpiration = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key 2', expiresAt: expirationDateInTheFuture });
|
||||
.send({
|
||||
label: 'My API Key 2',
|
||||
expiresAt: expirationDateInTheFuture,
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
|
||||
const retrieveAllApiKeysResponse = await testServer.authAgentFor(member).get('/api-keys');
|
||||
|
||||
@@ -274,6 +455,7 @@ describe('Member', () => {
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
expiresAt: expirationDateInTheFuture,
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
|
||||
expect(retrieveAllApiKeysResponse.body.data[0]).toEqual({
|
||||
@@ -284,6 +466,7 @@ describe('Member', () => {
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
expiresAt: null,
|
||||
scopes: ['workflow:create'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -291,7 +474,7 @@ describe('Member', () => {
|
||||
const newApiKeyResponse = await testServer
|
||||
.authAgentFor(member)
|
||||
.post('/api-keys')
|
||||
.send({ label: 'My API Key', expiresAt: null });
|
||||
.send({ label: 'My API Key', expiresAt: null, scopes: ['workflow:create'] });
|
||||
|
||||
const deleteApiKeyResponse = await testServer
|
||||
.authAgentFor(member)
|
||||
@@ -302,4 +485,14 @@ describe('Member', () => {
|
||||
expect(deleteApiKeyResponse.body.data.success).toBe(true);
|
||||
expect(retrieveAllApiKeysResponse.body.data.length).toBe(0);
|
||||
});
|
||||
|
||||
test('GET /api-keys/scopes should return scopes for the role', async () => {
|
||||
const apiKeyScopesResponse = await testServer.authAgentFor(member).get('/api-keys/scopes');
|
||||
|
||||
const scopes = apiKeyScopesResponse.body.data as ApiKeyScope[];
|
||||
|
||||
const scopesForRole = getApiKeyScopesForRole(member.role);
|
||||
|
||||
expect(scopes).toEqual(scopesForRole);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user