refactor: Move API keys into their own table (no-changelog) (#10629)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Ricardo Espinoza
2024-09-26 08:58:49 -04:00
committed by GitHub
parent 7e79a46750
commit a13a4f7442
35 changed files with 630 additions and 312 deletions

View File

@@ -10,7 +10,6 @@ export function assertReturnedUserProps(user: User) {
expect(user.personalizationAnswers).toBeNull();
expect(user.password).toBeUndefined();
expect(user.isPending).toBe(false);
expect(user.apiKey).not.toBeDefined();
expect(user.globalScopes).toBeDefined();
expect(user.globalScopes).not.toHaveLength(0);
}

View File

@@ -1,22 +1,29 @@
import { GlobalConfig } from '@n8n/config';
import { IsNull } from '@n8n/typeorm';
import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow';
import { Container } from 'typedi';
import validator from 'validator';
import type { ApiKey } from '@/databases/entities/api-key';
import type { User } from '@/databases/entities/user';
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
import { ProjectRepository } from '@/databases/repositories/project.repository';
import { UserRepository } from '@/databases/repositories/user.repository';
import { PublicApiKeyService } from '@/services/public-api-key.service';
import { mockInstance } from '@test/mocking';
import { SUCCESS_RESPONSE_BODY } from './shared/constants';
import { addApiKey, createOwner, createUser, createUserShell } from './shared/db/users';
import { randomApiKey, randomEmail, randomName, randomValidPassword } from './shared/random';
import { createOwnerWithApiKey, createUser, createUserShell } from './shared/db/users';
import { randomEmail, randomName, randomValidPassword } from './shared/random';
import * as testDb from './shared/test-db';
import type { SuperAgentTest } from './shared/types';
import * as utils from './shared/utils/';
const testServer = utils.setupTestServer({ endpointGroups: ['me'] });
let publicApiKeyService: PublicApiKeyService;
beforeAll(() => {
publicApiKeyService = Container.get(PublicApiKeyService);
});
beforeEach(async () => {
await testDb.truncate(['User']);
@@ -28,22 +35,22 @@ describe('When public API is disabled', () => {
let authAgent: SuperAgentTest;
beforeEach(async () => {
owner = await createOwner();
await addApiKey(owner);
owner = await createOwnerWithApiKey();
authAgent = testServer.authAgentFor(owner);
mockInstance(GlobalConfig, { publicApi: { disabled: true } });
});
test('POST /me/api-key should 404', async () => {
await authAgent.post('/me/api-key').expect(404);
test('POST /me/api-keys should 404', async () => {
await authAgent.post('/me/api-keys').expect(404);
});
test('GET /me/api-key should 404', async () => {
await authAgent.get('/me/api-key').expect(404);
test('GET /me/api-keys should 404', async () => {
await authAgent.get('/me/api-keys').expect(404);
});
test('DELETE /me/api-key should 404', async () => {
await authAgent.delete('/me/api-key').expect(404);
test('DELETE /me/api-key/:id should 404', async () => {
await authAgent.delete(`/me/api-keys/${1}`).expect(404);
});
});
@@ -53,7 +60,6 @@ describe('Owner shell', () => {
beforeEach(async () => {
ownerShell = await createUserShell('global:owner');
await addApiKey(ownerShell);
authOwnerShellAgent = testServer.authAgentFor(ownerShell);
});
@@ -63,17 +69,8 @@ describe('Owner shell', () => {
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
personalizationAnswers,
role,
password,
isPending,
apiKey,
} = response.body.data;
const { id, email, firstName, lastName, personalizationAnswers, role, password, isPending } =
response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(validPayload.email.toLowerCase());
@@ -83,7 +80,6 @@ describe('Owner shell', () => {
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(role).toBe('global:owner');
expect(apiKey).toBeUndefined();
const storedOwnerShell = await Container.get(UserRepository).findOneByOrFail({ id });
@@ -161,37 +157,56 @@ describe('Owner shell', () => {
}
});
test('POST /me/api-key should create an api key', async () => {
const response = await authOwnerShellAgent.post('/me/api-key');
test('POST /me/api-keys should create an api key', async () => {
const newApiKeyResponse = await authOwnerShellAgent.post('/me/api-keys');
expect(response.statusCode).toBe(200);
expect(response.body.data.apiKey).toBeDefined();
expect(response.body.data.apiKey).not.toBeNull();
const newApiKey = newApiKeyResponse.body.data as ApiKey;
const storedShellOwner = await Container.get(UserRepository).findOneOrFail({
where: { email: IsNull() },
expect(newApiKeyResponse.statusCode).toBe(200);
expect(newApiKey).toBeDefined();
const newStoredApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
userId: ownerShell.id,
});
expect(storedShellOwner.apiKey).toEqual(response.body.data.apiKey);
});
test('GET /me/api-key should fetch the api key redacted', async () => {
const response = await authOwnerShellAgent.get('/me/api-key');
expect(response.statusCode).toBe(200);
expect(response.body.data.apiKey).not.toEqual(ownerShell.apiKey);
});
test('DELETE /me/api-key should delete the api key', async () => {
const response = await authOwnerShellAgent.delete('/me/api-key');
expect(response.statusCode).toBe(200);
const storedShellOwner = await Container.get(UserRepository).findOneOrFail({
where: { email: IsNull() },
expect(newStoredApiKey).toEqual({
id: expect.any(String),
label: 'My API Key',
userId: ownerShell.id,
apiKey: newApiKey.apiKey,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
});
});
expect(storedShellOwner.apiKey).toBeNull();
test('GET /me/api-keys should fetch the api key redacted', async () => {
const newApiKeyResponse = await authOwnerShellAgent.post('/me/api-keys');
const retrieveAllApiKeysResponse = await authOwnerShellAgent.get('/me/api-keys');
expect(retrieveAllApiKeysResponse.statusCode).toBe(200);
expect(retrieveAllApiKeysResponse.body.data[0]).toEqual({
id: newApiKeyResponse.body.data.id,
label: 'My API Key',
userId: ownerShell.id,
apiKey: publicApiKeyService.redactApiKey(newApiKeyResponse.body.data.apiKey),
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
});
test('DELETE /me/api-keys/:id should delete the api key', async () => {
const newApiKeyResponse = await authOwnerShellAgent.post('/me/api-keys');
const deleteApiKeyResponse = await authOwnerShellAgent.delete(
`/me/api-keys/${newApiKeyResponse.body.data.id}`,
);
const retrieveAllApiKeysResponse = await authOwnerShellAgent.get('/me/api-keys');
expect(deleteApiKeyResponse.body.data.success).toBe(true);
expect(retrieveAllApiKeysResponse.body.data.length).toBe(0);
});
});
@@ -204,10 +219,8 @@ describe('Member', () => {
member = await createUser({
password: memberPassword,
role: 'global:member',
apiKey: randomApiKey(),
});
authMemberAgent = testServer.authAgentFor(member);
await utils.setInstanceOwnerSetUp(true);
});
@@ -215,17 +228,8 @@ describe('Member', () => {
for (const validPayload of VALID_PATCH_ME_PAYLOADS) {
const response = await authMemberAgent.patch('/me').send(validPayload).expect(200);
const {
id,
email,
firstName,
lastName,
personalizationAnswers,
role,
password,
isPending,
apiKey,
} = response.body.data;
const { id, email, firstName, lastName, personalizationAnswers, role, password, isPending } =
response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(validPayload.email.toLowerCase());
@@ -235,7 +239,6 @@ describe('Member', () => {
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(role).toBe('global:member');
expect(apiKey).toBeUndefined();
const storedMember = await Container.get(UserRepository).findOneByOrFail({ id });
@@ -275,6 +278,7 @@ describe('Member', () => {
};
const response = await authMemberAgent.patch('/me/password').send(validPayload);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
@@ -315,33 +319,59 @@ describe('Member', () => {
}
});
test('POST /me/api-key should create an api key', async () => {
const response = await testServer.authAgentFor(member).post('/me/api-key');
test('POST /me/api-keys should create an api key', async () => {
const newApiKeyResponse = await testServer.authAgentFor(member).post('/me/api-keys');
expect(response.statusCode).toBe(200);
expect(response.body.data.apiKey).toBeDefined();
expect(response.body.data.apiKey).not.toBeNull();
expect(newApiKeyResponse.statusCode).toBe(200);
expect(newApiKeyResponse.body.data.apiKey).toBeDefined();
expect(newApiKeyResponse.body.data.apiKey).not.toBeNull();
const storedMember = await Container.get(UserRepository).findOneByOrFail({ id: member.id });
const newStoredApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
userId: member.id,
});
expect(storedMember.apiKey).toEqual(response.body.data.apiKey);
expect(newStoredApiKey).toEqual({
id: expect.any(String),
label: 'My API Key',
userId: member.id,
apiKey: newApiKeyResponse.body.data.apiKey,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
});
});
test('GET /me/api-key should fetch the api key redacted', async () => {
const response = await testServer.authAgentFor(member).get('/me/api-key');
test('GET /me/api-keys should fetch the api key redacted', async () => {
const newApiKeyResponse = await testServer.authAgentFor(member).post('/me/api-keys');
expect(response.statusCode).toBe(200);
expect(response.body.data.apiKey).not.toEqual(member.apiKey);
const retrieveAllApiKeysResponse = await testServer.authAgentFor(member).get('/me/api-keys');
expect(retrieveAllApiKeysResponse.statusCode).toBe(200);
expect(retrieveAllApiKeysResponse.body.data[0]).toEqual({
id: newApiKeyResponse.body.data.id,
label: 'My API Key',
userId: member.id,
apiKey: publicApiKeyService.redactApiKey(newApiKeyResponse.body.data.apiKey),
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
expect(newApiKeyResponse.body.data.apiKey).not.toEqual(
retrieveAllApiKeysResponse.body.data[0].apiKey,
);
});
test('DELETE /me/api-key should delete the api key', async () => {
const response = await testServer.authAgentFor(member).delete('/me/api-key');
test('DELETE /me/api-keys/:id should delete the api key', async () => {
const newApiKeyResponse = await testServer.authAgentFor(member).post('/me/api-keys');
expect(response.statusCode).toBe(200);
const deleteApiKeyResponse = await testServer
.authAgentFor(member)
.delete(`/me/api-keys/${newApiKeyResponse.body.data.id}`);
const storedMember = await Container.get(UserRepository).findOneByOrFail({ id: member.id });
const retrieveAllApiKeysResponse = await testServer.authAgentFor(member).get('/me/api-keys');
expect(storedMember.apiKey).toBeNull();
expect(deleteApiKeyResponse.body.data.success).toBe(true);
expect(retrieveAllApiKeysResponse.body.data.length).toBe(0);
});
});

View File

@@ -7,8 +7,8 @@ import { SharedCredentialsRepository } from '@/databases/repositories/shared-cre
import { createTeamProject } from '@test-integration/db/projects';
import { affixRoleToSaveCredential, createCredentials } from '../shared/db/credentials';
import { addApiKey, createUser, createUserShell } from '../shared/db/users';
import { randomApiKey, randomName } from '../shared/random';
import { createMemberWithApiKey, createOwnerWithApiKey } from '../shared/db/users';
import { randomName } from '../shared/random';
import * as testDb from '../shared/test-db';
import type { CredentialPayload, SaveCredentialFunction } from '../shared/types';
import type { SuperAgentTest } from '../shared/types';
@@ -24,8 +24,8 @@ let saveCredential: SaveCredentialFunction;
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
beforeAll(async () => {
owner = await addApiKey(await createUserShell('global:owner'));
member = await createUser({ role: 'global:member', apiKey: randomApiKey() });
owner = await createOwnerWithApiKey();
member = await createMemberWithApiKey();
authOwnerAgent = testServer.publicApiAgentFor(owner);
authMemberAgent = testServer.publicApiAgentFor(member);
@@ -156,10 +156,7 @@ describe('DELETE /credentials/:id', () => {
});
test('should delete owned cred for member but leave others untouched', async () => {
const anotherMember = await createUser({
role: 'global:member',
apiKey: randomApiKey(),
});
const anotherMember = await createMemberWithApiKey();
const savedCredential = await saveCredential(dbCredential(), { user: member });
const notToBeChangedCredential = await saveCredential(dbCredential(), { user: member });

View File

@@ -12,13 +12,12 @@ import {
createSuccessfulExecution,
createWaitingExecution,
} from '../shared/db/executions';
import { createUser } from '../shared/db/users';
import { createMemberWithApiKey, createOwnerWithApiKey } from '../shared/db/users';
import {
createManyWorkflows,
createWorkflow,
shareWorkflowWithUsers,
} from '../shared/db/workflows';
import { randomApiKey } from '../shared/random';
import * as testDb from '../shared/test-db';
import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils/';
@@ -36,9 +35,9 @@ mockInstance(Telemetry);
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
beforeAll(async () => {
owner = await createUser({ role: 'global:owner', apiKey: randomApiKey() });
user1 = await createUser({ role: 'global:member', apiKey: randomApiKey() });
user2 = await createUser({ role: 'global:member', apiKey: randomApiKey() });
owner = await createOwnerWithApiKey();
user1 = await createMemberWithApiKey();
user2 = await createMemberWithApiKey();
// TODO: mock BinaryDataService instead
await utils.initBinaryDataService();

View File

@@ -2,7 +2,7 @@ import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
import { Telemetry } from '@/telemetry';
import { mockInstance } from '@test/mocking';
import { createTeamProject, getProjectByNameOrFail } from '@test-integration/db/projects';
import { createMember, createOwner } from '@test-integration/db/users';
import { createMemberWithApiKey, createOwnerWithApiKey } from '@test-integration/db/users';
import { setupTestServer } from '@test-integration/utils';
import * as testDb from '../shared/test-db';
@@ -26,7 +26,7 @@ describe('Projects in Public API', () => {
*/
testServer.license.setQuota('quota:maxTeamProjects', -1);
testServer.license.enable('feat:projectRole:admin');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const projects = await Promise.all([
createTeamProject(),
createTeamProject(),
@@ -53,15 +53,10 @@ describe('Projects in Public API', () => {
});
it('if not authenticated, should reject', async () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: false });
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).get('/projects');
const response = await testServer.publicApiAgentWithoutApiKey().get('/projects');
/**
* Assert
@@ -74,7 +69,7 @@ describe('Projects in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
/**
* Act
@@ -97,12 +92,12 @@ describe('Projects in Public API', () => {
*/
testServer.license.setQuota('quota:maxTeamProjects', -1);
testServer.license.enable('feat:projectRole:admin');
const owner = await createMember({ withApiKey: true });
const member = await createMemberWithApiKey();
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).get('/projects');
const response = await testServer.publicApiAgentFor(member).get('/projects');
/**
* Assert
@@ -119,7 +114,7 @@ describe('Projects in Public API', () => {
*/
testServer.license.setQuota('quota:maxTeamProjects', -1);
testServer.license.enable('feat:projectRole:admin');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const projectPayload = { name: 'some-project' };
/**
@@ -150,14 +145,13 @@ describe('Projects in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: false });
const projectPayload = { name: 'some-project' };
/**
* Act
*/
const response = await testServer
.publicApiAgentFor(owner)
.publicApiAgentWithoutApiKey()
.post('/projects')
.send(projectPayload);
@@ -172,7 +166,7 @@ describe('Projects in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const projectPayload = { name: 'some-project' };
/**
@@ -199,7 +193,7 @@ describe('Projects in Public API', () => {
*/
testServer.license.setQuota('quota:maxTeamProjects', -1);
testServer.license.enable('feat:projectRole:admin');
const member = await createMember({ withApiKey: true });
const member = await createMemberWithApiKey();
const projectPayload = { name: 'some-project' };
/**
@@ -225,7 +219,7 @@ describe('Projects in Public API', () => {
*/
testServer.license.setQuota('quota:maxTeamProjects', -1);
testServer.license.enable('feat:projectRole:admin');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const project = await createTeamProject();
/**
@@ -244,13 +238,14 @@ describe('Projects in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: false });
const project = await createTeamProject();
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).delete(`/projects/${project.id}`);
const response = await testServer
.publicApiAgentWithoutApiKey()
.delete(`/projects/${project.id}`);
/**
* Assert
@@ -263,7 +258,7 @@ describe('Projects in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const project = await createTeamProject();
/**
@@ -287,13 +282,13 @@ describe('Projects in Public API', () => {
*/
testServer.license.setQuota('quota:maxTeamProjects', -1);
testServer.license.enable('feat:projectRole:admin');
const member = await createMember({ withApiKey: true });
const owner = await createMemberWithApiKey();
const project = await createTeamProject();
/**
* Act
*/
const response = await testServer.publicApiAgentFor(member).delete(`/projects/${project.id}`);
const response = await testServer.publicApiAgentFor(owner).delete(`/projects/${project.id}`);
/**
* Assert
@@ -310,7 +305,7 @@ describe('Projects in Public API', () => {
*/
testServer.license.setQuota('quota:maxTeamProjects', -1);
testServer.license.enable('feat:projectRole:admin');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const project = await createTeamProject('old-name');
/**
@@ -332,14 +327,13 @@ describe('Projects in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: false });
const project = await createTeamProject();
/**
* Act
*/
const response = await testServer
.publicApiAgentFor(owner)
.publicApiAgentWithoutApiKey()
.put(`/projects/${project.id}`)
.send({ name: 'new-name' });
@@ -354,7 +348,7 @@ describe('Projects in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const project = await createTeamProject();
/**
@@ -381,7 +375,7 @@ describe('Projects in Public API', () => {
*/
testServer.license.setQuota('quota:maxTeamProjects', -1);
testServer.license.enable('feat:projectRole:admin');
const member = await createMember({ withApiKey: true });
const member = await createMemberWithApiKey();
const project = await createTeamProject();
/**

View File

@@ -4,8 +4,7 @@ import type { User } from '@/databases/entities/user';
import { TagRepository } from '@/databases/repositories/tag.repository';
import { createTag } from '../shared/db/tags';
import { createUser } from '../shared/db/users';
import { randomApiKey } from '../shared/random';
import { createMemberWithApiKey, createOwnerWithApiKey } from '../shared/db/users';
import * as testDb from '../shared/test-db';
import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils/';
@@ -18,15 +17,8 @@ let authMemberAgent: SuperAgentTest;
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
beforeAll(async () => {
owner = await createUser({
role: 'global:owner',
apiKey: randomApiKey(),
});
member = await createUser({
role: 'global:member',
apiKey: randomApiKey(),
});
owner = await createOwnerWithApiKey();
member = await createMemberWithApiKey();
});
beforeEach(async () => {

View File

@@ -6,8 +6,13 @@ import { License } from '@/license';
import { createTeamProject, linkUserToProject } from '@test-integration/db/projects';
import { mockInstance } from '../../shared/mocking';
import { createOwner, createUser, createUserShell } from '../shared/db/users';
import { randomApiKey } from '../shared/random';
import {
createMember,
createMemberWithApiKey,
createOwnerWithApiKey,
createUser,
createUserShell,
} from '../shared/db/users';
import * as testDb from '../shared/test-db';
import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils/';
@@ -25,32 +30,23 @@ beforeEach(async () => {
describe('With license unlimited quota:users', () => {
describe('GET /users', () => {
test('should fail due to missing API Key', async () => {
const owner = await createUser({ role: 'global:owner' });
const authOwnerAgent = testServer.publicApiAgentFor(owner);
const authOwnerAgent = testServer.publicApiAgentWithoutApiKey();
await authOwnerAgent.get('/users').expect(401);
});
test('should fail due to invalid API Key', async () => {
const owner = await createUser({
role: 'global:owner',
apiKey: randomApiKey(),
});
owner.apiKey = 'invalid-key';
const authOwnerAgent = testServer.publicApiAgentFor(owner);
const authOwnerAgent = testServer.publicApiAgentWithApiKey('invalid-key');
await authOwnerAgent.get('/users').expect(401);
});
test('should fail due to member trying to access owner only endpoint', async () => {
const member = await createUser({ apiKey: randomApiKey() });
const member = await createMemberWithApiKey();
const authMemberAgent = testServer.publicApiAgentFor(member);
await authMemberAgent.get('/users').expect(403);
});
test('should return all users', async () => {
const owner = await createUser({
role: 'global:owner',
apiKey: randomApiKey(),
});
const owner = await createOwnerWithApiKey();
const authOwnerAgent = testServer.publicApiAgentFor(owner);
@@ -92,10 +88,10 @@ describe('With license unlimited quota:users', () => {
* Arrange
*/
const [owner, firstMember, secondMember, thirdMember] = await Promise.all([
createOwner({ withApiKey: true }),
createUser({ role: 'global:member' }),
createUser({ role: 'global:member' }),
createUser({ role: 'global:member' }),
createOwnerWithApiKey(),
createMember(),
createMember(),
createMember(),
]);
const [firstProject, secondProject] = await Promise.all([
@@ -130,40 +126,30 @@ describe('With license unlimited quota:users', () => {
describe('GET /users/:id', () => {
test('should fail due to missing API Key', async () => {
const owner = await createUser({ role: 'global:owner' });
const authOwnerAgent = testServer.publicApiAgentFor(owner);
const owner = await createOwnerWithApiKey();
const authOwnerAgent = testServer.publicApiAgentWithoutApiKey();
await authOwnerAgent.get(`/users/${owner.id}`).expect(401);
});
test('should fail due to invalid API Key', async () => {
const owner = await createUser({
role: 'global:owner',
apiKey: randomApiKey(),
});
owner.apiKey = 'invalid-key';
const authOwnerAgent = testServer.publicApiAgentFor(owner);
const owner = await createOwnerWithApiKey();
const authOwnerAgent = testServer.publicApiAgentWithApiKey('invalid-key');
await authOwnerAgent.get(`/users/${owner.id}`).expect(401);
});
test('should fail due to member trying to access owner only endpoint', async () => {
const member = await createUser({ apiKey: randomApiKey() });
const member = await createMemberWithApiKey();
const authMemberAgent = testServer.publicApiAgentFor(member);
await authMemberAgent.get(`/users/${member.id}`).expect(403);
});
test('should return 404 for non-existing id ', async () => {
const owner = await createUser({
role: 'global:owner',
apiKey: randomApiKey(),
});
const owner = await createOwnerWithApiKey();
const authOwnerAgent = testServer.publicApiAgentFor(owner);
await authOwnerAgent.get(`/users/${uuid()}`).expect(404);
});
test('should return a pending user', async () => {
const owner = await createUser({
role: 'global:owner',
apiKey: randomApiKey(),
});
const owner = await createOwnerWithApiKey();
const { id: memberId } = await createUserShell('global:member');
@@ -199,20 +185,13 @@ describe('With license unlimited quota:users', () => {
describe('GET /users/:email', () => {
test('with non-existing email should return 404', async () => {
const owner = await createUser({
role: 'global:owner',
apiKey: randomApiKey(),
});
const owner = await createOwnerWithApiKey();
const authOwnerAgent = testServer.publicApiAgentFor(owner);
await authOwnerAgent.get('/users/jhondoe@gmail.com').expect(404);
});
test('should return a user', async () => {
const owner = await createUser({
role: 'global:owner',
apiKey: randomApiKey(),
});
const owner = await createOwnerWithApiKey();
const authOwnerAgent = testServer.publicApiAgentFor(owner);
const response = await authOwnerAgent.get(`/users/${owner.email}`).expect(200);
@@ -249,10 +228,7 @@ describe('With license without quota:users', () => {
beforeEach(async () => {
mockInstance(License, { getUsersLimit: jest.fn().mockReturnValue(null) });
const owner = await createUser({
role: 'global:owner',
apiKey: randomApiKey(),
});
const owner = await createOwnerWithApiKey();
authOwnerAgent = testServer.publicApiAgentFor(owner);
});

View File

@@ -1,7 +1,12 @@
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
import { Telemetry } from '@/telemetry';
import { mockInstance } from '@test/mocking';
import { createMember, createOwner, getUserById } from '@test-integration/db/users';
import {
createMember,
createMemberWithApiKey,
createOwnerWithApiKey,
getUserById,
} from '@test-integration/db/users';
import { setupTestServer } from '@test-integration/utils';
import * as testDb from '../shared/test-db';
@@ -23,13 +28,12 @@ describe('Users in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: false });
const payload = { email: 'test@test.com', role: 'global:admin' };
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).post('/users').send(payload);
const response = await testServer.publicApiAgentWithApiKey('').post('/users').send(payload);
/**
* Assert
@@ -42,7 +46,7 @@ describe('Users in Public API', () => {
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const member = await createMember({ withApiKey: true });
const member = await createMemberWithApiKey();
const payload = [{ email: 'test@test.com', role: 'global:admin' }];
/**
@@ -62,7 +66,8 @@ describe('Users in Public API', () => {
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
await createOwnerWithApiKey();
const payload = [{ email: 'test@test.com', role: 'global:admin' }];
/**
@@ -99,13 +104,12 @@ describe('Users in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: false });
const member = await createMember();
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).delete(`/users/${member.id}`);
const response = await testServer.publicApiAgentWithApiKey('').delete(`/users/${member.id}`);
/**
* Assert
@@ -118,14 +122,14 @@ describe('Users in Public API', () => {
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const firstMember = await createMember({ withApiKey: true });
const member = await createMemberWithApiKey();
const secondMember = await createMember();
/**
* Act
*/
const response = await testServer
.publicApiAgentFor(firstMember)
.publicApiAgentFor(member)
.delete(`/users/${secondMember.id}`);
/**
@@ -140,7 +144,7 @@ describe('Users in Public API', () => {
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const member = await createMember();
/**
@@ -161,13 +165,14 @@ describe('Users in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: false });
const member = await createMember();
/**
* Act
*/
const response = await testServer.publicApiAgentFor(owner).patch(`/users/${member.id}/role`);
const response = await testServer
.publicApiAgentWithApiKey('')
.patch(`/users/${member.id}/role`);
/**
* Assert
@@ -179,7 +184,7 @@ describe('Users in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const member = await createMember();
const payload = { newRoleName: 'global:admin' };
@@ -206,7 +211,7 @@ describe('Users in Public API', () => {
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const firstMember = await createMember({ withApiKey: true });
const member = await createMemberWithApiKey();
const secondMember = await createMember();
const payload = { newRoleName: 'global:admin' };
@@ -214,7 +219,7 @@ describe('Users in Public API', () => {
* Act
*/
const response = await testServer
.publicApiAgentFor(firstMember)
.publicApiAgentFor(member)
.patch(`/users/${secondMember.id}/role`)
.send(payload);
@@ -230,7 +235,7 @@ describe('Users in Public API', () => {
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const member = await createMember();
const payload = { newRoleName: 'invalid' };
@@ -253,7 +258,7 @@ describe('Users in Public API', () => {
* Arrange
*/
testServer.license.enable('feat:advancedPermissions');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const member = await createMember();
const payload = { newRoleName: 'global:admin' };

View File

@@ -1,5 +1,5 @@
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
import { createOwner } from '@test-integration/db/users';
import { createOwnerWithApiKey } from '@test-integration/db/users';
import { createVariable, getVariableOrFail } from '@test-integration/db/variables';
import { setupTestServer } from '@test-integration/utils';
@@ -22,7 +22,7 @@ describe('Variables in Public API', () => {
* Arrange
*/
testServer.license.enable('feat:variables');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const variables = await Promise.all([createVariable(), createVariable(), createVariable()]);
/**
@@ -48,7 +48,8 @@ describe('Variables in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
/**
* Act
@@ -72,7 +73,7 @@ describe('Variables in Public API', () => {
* Arrange
*/
testServer.license.enable('feat:variables');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const variablePayload = { key: 'key', value: 'value' };
/**
@@ -96,7 +97,7 @@ describe('Variables in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const variablePayload = { key: 'key', value: 'value' };
/**
@@ -124,7 +125,7 @@ describe('Variables in Public API', () => {
* Arrange
*/
testServer.license.enable('feat:variables');
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const variable = await createVariable();
/**
@@ -145,7 +146,7 @@ describe('Variables in Public API', () => {
/**
* Arrange
*/
const owner = await createOwner({ withApiKey: true });
const owner = await createOwnerWithApiKey();
const variable = await createVariable();
/**

View File

@@ -17,9 +17,8 @@ import { createTeamProject } from '@test-integration/db/projects';
import { mockInstance } from '../../shared/mocking';
import { createTag } from '../shared/db/tags';
import { createUser } from '../shared/db/users';
import { createMemberWithApiKey, createOwnerWithApiKey } from '../shared/db/users';
import { createWorkflow, createWorkflowWithTrigger } from '../shared/db/workflows';
import { randomApiKey } from '../shared/random';
import * as testDb from '../shared/test-db';
import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils/';
@@ -40,18 +39,13 @@ const license = testServer.license;
mockInstance(ExecutionService);
beforeAll(async () => {
owner = await createUser({
role: 'global:owner',
apiKey: randomApiKey(),
});
owner = await createOwnerWithApiKey();
ownerPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
owner.id,
);
member = await createUser({
role: 'global:member',
apiKey: randomApiKey(),
});
member = await createMemberWithApiKey();
memberPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
member.id,
);

View File

@@ -1,8 +1,10 @@
import { hash } from 'bcryptjs';
import { randomString } from 'n8n-workflow';
import Container from 'typedi';
import { AuthIdentity } from '@/databases/entities/auth-identity';
import { type GlobalRole, type User } from '@/databases/entities/user';
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
import { AuthIdentityRepository } from '@/databases/repositories/auth-identity.repository';
import { AuthUserRepository } from '@/databases/repositories/auth-user.repository';
import { UserRepository } from '@/databases/repositories/user.repository';
@@ -79,19 +81,38 @@ export async function createUserWithMfaEnabled(
};
}
export async function createOwner({ withApiKey } = { withApiKey: false }) {
if (withApiKey) {
return await addApiKey(await createUser({ role: 'global:owner' }));
}
const createApiKeyEntity = (user: User) => {
const apiKey = randomApiKey();
return Container.get(ApiKeyRepository).create({
userId: user.id,
label: randomString(10),
apiKey,
});
};
export const addApiKey = async (user: User) => {
return await Container.get(ApiKeyRepository).save(createApiKeyEntity(user));
};
export async function createOwnerWithApiKey() {
const owner = await createOwner();
const apiKey = await addApiKey(owner);
owner.apiKeys = [apiKey];
return owner;
}
export async function createMemberWithApiKey() {
const member = await createMember();
const apiKey = await addApiKey(member);
member.apiKeys = [apiKey];
return member;
}
export async function createOwner() {
return await createUser({ role: 'global:owner' });
}
export async function createMember({ withApiKey } = { withApiKey: false }) {
if (withApiKey) {
return await addApiKey(await createUser({ role: 'global:member' }));
}
export async function createMember() {
return await createUser({ role: 'global:member' });
}
@@ -128,11 +149,6 @@ export async function createManyUsers(
return result.map((result) => result.user);
}
export async function addApiKey(user: User): Promise<User> {
user.apiKey = randomApiKey();
return await Container.get(UserRepository).save(user);
}
export const getAllUsers = async () =>
await Container.get(UserRepository).find({
relations: ['authIdentities'],

View File

@@ -80,6 +80,7 @@ const repositories = [
'WorkflowHistory',
'WorkflowStatistics',
'WorkflowTagMapping',
'ApiKey',
] as const;
/**

View File

@@ -55,6 +55,8 @@ export interface TestServer {
httpServer: Server;
authAgentFor: (user: User) => TestAgent;
publicApiAgentFor: (user: User) => TestAgent;
publicApiAgentWithApiKey: (apiKey: string) => TestAgent;
publicApiAgentWithoutApiKey: () => TestAgent;
authlessAgent: TestAgent;
restlessAgent: TestAgent;
license: LicenseMocker;

View File

@@ -62,17 +62,30 @@ function createAgent(
return agent;
}
function publicApiAgent(
const userDoesNotHaveApiKey = (user: User) => {
return !user.apiKeys || !Array.from(user.apiKeys) || user.apiKeys.length === 0;
};
const publicApiAgent = (
app: express.Application,
{ user, version = 1 }: { user: User; version?: number },
) {
{ user, apiKey, version = 1 }: { user?: User; apiKey?: string; version?: number },
) => {
if (user && apiKey) {
throw new Error('Cannot provide both user and API key');
}
if (user && userDoesNotHaveApiKey(user)) {
throw new Error('User does not have an API key');
}
const agentApiKey = apiKey ?? user?.apiKeys[0].apiKey;
const agent = request.agent(app);
void agent.use(prefix(`${PUBLIC_API_REST_PATH_SEGMENT}/v${version}`));
if (user.apiKey) {
void agent.set({ 'X-N8N-API-KEY': user.apiKey });
}
if (!user && !apiKey) return agent;
void agent.set({ 'X-N8N-API-KEY': agentApiKey });
return agent;
}
};
export const setupTestServer = ({
endpointGroups,
@@ -100,6 +113,8 @@ export const setupTestServer = ({
authlessAgent: createAgent(app),
restlessAgent: createAgent(app, { auth: false, noRest: true }),
publicApiAgentFor: (user) => publicApiAgent(app, { user }),
publicApiAgentWithApiKey: (apiKey) => publicApiAgent(app, { apiKey }),
publicApiAgentWithoutApiKey: () => publicApiAgent(app, {}),
license: new LicenseMocker(),
};