feat(core): Add LDAP support (#3835)

This commit is contained in:
Ricardo Espinoza
2023-01-24 20:18:39 -05:00
committed by GitHub
parent 259296c5c9
commit 0c70a40317
77 changed files with 3686 additions and 192 deletions

View File

@@ -9,13 +9,3 @@ declare module 'supertest' {
extends superagent.SuperAgent<T>,
Record<string, any> {}
}
/**
* Prevent `repository.delete({})` (non-criteria) from triggering the type error
* `Expression produces a union type that is too complex to represent.ts(2590)`
*/
declare module 'typeorm' {
interface Repository<Entity extends ObjectLiteral> {
delete(criteria: {}): Promise<void>;
}
}

View File

@@ -1,6 +1,7 @@
import { randomBytes } from 'crypto';
import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '@db/entities/User';
import type { CredentialPayload } from './types';
import { v4 as uuid } from 'uuid';
/**
* Create a random alphanumeric string of random length between two limits, both inclusive.
@@ -59,3 +60,5 @@ export const randomCredentialPayload = (): CredentialPayload => ({
nodesAccess: [{ nodeType: randomName() }],
data: { accessToken: randomString(6, 16) },
});
export const uniqueId = () => uuid();

View File

@@ -10,17 +10,7 @@ import { mysqlMigrations } from '@db/migrations/mysqldb';
import { postgresMigrations } from '@db/migrations/postgresdb';
import { sqliteMigrations } from '@db/migrations/sqlite';
import { hashPassword } from '@/UserManagement/UserManagementHelper';
import { DB_INITIALIZATION_TIMEOUT, MAPPING_TABLES, MAPPING_TABLES_TO_CLEAR } from './constants';
import {
randomApiKey,
randomCredentialPayload,
randomEmail,
randomName,
randomString,
randomValidPassword,
} from './random';
import { categorize, getPostgresSchemaSection } from './utils';
import { AuthIdentity } from '@/databases/entities/AuthIdentity';
import { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { InstalledNodes } from '@db/entities/InstalledNodes';
import { InstalledPackages } from '@db/entities/InstalledPackages';
@@ -35,6 +25,10 @@ import type {
InstalledPackagePayload,
MappingName,
} from './types';
import { DB_INITIALIZATION_TIMEOUT, MAPPING_TABLES, MAPPING_TABLES_TO_CLEAR } from './constants';
import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random';
import { categorize, getPostgresSchemaSection } from './utils';
import type { DatabaseType, ICredentialsDb } from '@/Interfaces';
export type TestDBType = 'postgres' | 'mysql';
@@ -103,7 +97,7 @@ export async function terminate() {
async function truncateMappingTables(
dbType: DatabaseType,
collections: Array<CollectionName>,
collections: CollectionName[],
testDb: Connection,
) {
const mappingTables = collections.reduce<string[]>((acc, collection) => {
@@ -115,7 +109,7 @@ async function truncateMappingTables(
}, []);
if (dbType === 'sqlite') {
const promises = mappingTables.map((tableName) =>
const promises = mappingTables.map(async (tableName) =>
testDb.query(
`DELETE FROM ${tableName}; DELETE FROM sqlite_sequence WHERE name=${tableName};`,
),
@@ -152,16 +146,16 @@ async function truncateMappingTables(
* @param collections Array of entity names whose tables to truncate.
* @param testDbName Name of the test DB to truncate tables in.
*/
export async function truncate(collections: Array<CollectionName>) {
export async function truncate(collections: CollectionName[]) {
const dbType = config.getEnv('database.type');
const testDb = Db.getConnection();
if (dbType === 'sqlite') {
await testDb.query('PRAGMA foreign_keys=OFF');
const truncationPromises = collections.map((collection) => {
const truncationPromises = collections.map(async (collection) => {
const tableName = toTableName(collection);
Db.collections[collection].clear();
// Db.collections[collection].clear();
return testDb.query(
`DELETE FROM ${tableName}; DELETE FROM sqlite_sequence WHERE name=${tableName};`,
);
@@ -200,7 +194,7 @@ export async function truncate(collections: Array<CollectionName>) {
const hasIdColumn = await testDb
.query(`SHOW COLUMNS FROM ${tableName}`)
.then((columns: { Field: string }[]) => columns.find((c) => c.Field === 'id'));
.then((columns: Array<{ Field: string }>) => columns.find((c) => c.Field === 'id'));
if (!hasIdColumn) continue;
@@ -218,18 +212,20 @@ function toTableName(sourceName: CollectionName | MappingName) {
if (isMapping(sourceName)) return MAPPING_TABLES[sourceName];
return {
AuthIdentity: 'auth_identity',
AuthProviderSyncHistory: 'auth_provider_sync_history',
Credentials: 'credentials_entity',
Workflow: 'workflow_entity',
Execution: 'execution_entity',
Tag: 'tag_entity',
Webhook: 'webhook_entity',
InstalledNodes: 'installed_nodes',
InstalledPackages: 'installed_packages',
Role: 'role',
User: 'user',
Settings: 'settings',
SharedCredentials: 'shared_credentials',
SharedWorkflow: 'shared_workflow',
Settings: 'settings',
InstalledPackages: 'installed_packages',
InstalledNodes: 'installed_nodes',
Tag: 'tag_entity',
User: 'user',
Webhook: 'webhook_entity',
Workflow: 'workflow_entity',
WorkflowStatistics: 'workflow_statistics',
EventDestinations: 'event_destinations',
}[sourceName];
@@ -243,7 +239,7 @@ function toTableName(sourceName: CollectionName | MappingName) {
* Save a credential to the test DB, sharing it with a user.
*/
export async function saveCredential(
credentialPayload: CredentialPayload = randomCredentialPayload(),
credentialPayload: CredentialPayload,
{ user, role }: { user: User; role: Role },
) {
const newCredential = new CredentialsEntity();
@@ -280,7 +276,7 @@ export async function shareCredentialWithUsers(credential: CredentialsEntity, us
}
export function affixRoleToSaveCredential(role: Role) {
return (credentialPayload: CredentialPayload, { user }: { user: User }) =>
return async (credentialPayload: CredentialPayload, { user }: { user: User }) =>
saveCredential(credentialPayload, { user, role });
}
@@ -293,7 +289,7 @@ export function affixRoleToSaveCredential(role: Role) {
*/
export async function createUser(attributes: Partial<User> = {}): Promise<User> {
const { email, password, firstName, lastName, globalRole, ...rest } = attributes;
const user = {
const user: Partial<User> = {
email: email ?? randomEmail(),
password: await hashPassword(password ?? randomValidPassword()),
firstName: firstName ?? randomName(),
@@ -305,11 +301,17 @@ export async function createUser(attributes: Partial<User> = {}): Promise<User>
return Db.collections.User.save(user);
}
export async function createLdapUser(attributes: Partial<User>, ldapId: string): Promise<User> {
const user = await createUser(attributes);
await Db.collections.AuthIdentity.save(AuthIdentity.create(user, ldapId, 'ldap'));
return user;
}
export async function createOwner() {
return createUser({ globalRole: await getGlobalOwnerRole() });
}
export function createUserShell(globalRole: Role): Promise<User> {
export async function createUserShell(globalRole: Role): Promise<User> {
if (globalRole.scope !== 'global') {
throw new Error(`Invalid role received: ${JSON.stringify(globalRole)}`);
}
@@ -366,7 +368,7 @@ export async function saveInstalledPackage(
return savedInstalledPackage;
}
export function saveInstalledNode(
export async function saveInstalledNode(
installedNodePayload: InstalledNodePayload,
): Promise<InstalledNodes> {
const newInstalledNode = new InstalledNodes();
@@ -376,7 +378,7 @@ export function saveInstalledNode(
return Db.collections.InstalledNodes.save(newInstalledNode);
}
export function addApiKey(user: User): Promise<User> {
export async function addApiKey(user: User): Promise<User> {
user.apiKey = randomApiKey();
return Db.collections.User.save(user);
}
@@ -385,42 +387,42 @@ export function addApiKey(user: User): Promise<User> {
// role fetchers
// ----------------------------------
export function getGlobalOwnerRole() {
export async function getGlobalOwnerRole() {
return Db.collections.Role.findOneByOrFail({
name: 'owner',
scope: 'global',
});
}
export function getGlobalMemberRole() {
export async function getGlobalMemberRole() {
return Db.collections.Role.findOneByOrFail({
name: 'member',
scope: 'global',
});
}
export function getWorkflowOwnerRole() {
export async function getWorkflowOwnerRole() {
return Db.collections.Role.findOneByOrFail({
name: 'owner',
scope: 'workflow',
});
}
export function getWorkflowEditorRole() {
export async function getWorkflowEditorRole() {
return Db.collections.Role.findOneByOrFail({
name: 'editor',
scope: 'workflow',
});
}
export function getCredentialOwnerRole() {
export async function getCredentialOwnerRole() {
return Db.collections.Role.findOneByOrFail({
name: 'owner',
scope: 'credential',
});
}
export function getAllRoles() {
export async function getAllRoles() {
return Promise.all([
getGlobalOwnerRole(),
getGlobalMemberRole(),
@@ -429,6 +431,17 @@ export function getAllRoles() {
]);
}
export const getAllUsers = async () =>
Db.collections.User.find({
relations: ['globalRole', 'authIdentities'],
});
export const getLdapIdentities = async () =>
Db.collections.AuthIdentity.find({
where: { providerType: 'ldap' },
relations: ['user'],
});
// ----------------------------------
// Execution helpers
// ----------------------------------
@@ -438,17 +451,14 @@ export async function createManyExecutions(
workflow: WorkflowEntity,
callback: (workflow: WorkflowEntity) => Promise<ExecutionEntity>,
) {
const executionsRequests = [...Array(amount)].map((_) => callback(workflow));
const executionsRequests = [...Array(amount)].map(async (_) => callback(workflow));
return Promise.all(executionsRequests);
}
/**
* Store a execution in the DB and assign it to a workflow.
*/
export async function createExecution(
attributes: Partial<ExecutionEntity> = {},
workflow: WorkflowEntity,
) {
async function createExecution(attributes: Partial<ExecutionEntity>, workflow: WorkflowEntity) {
const { data, finished, mode, startedAt, stoppedAt, waitTill } = attributes;
const execution = await Db.collections.Execution.save({
@@ -468,38 +478,21 @@ export async function createExecution(
* Store a successful execution in the DB and assign it to a workflow.
*/
export async function createSuccessfulExecution(workflow: WorkflowEntity) {
return await createExecution(
{
finished: true,
},
workflow,
);
return createExecution({ finished: true }, workflow);
}
/**
* Store an error execution in the DB and assign it to a workflow.
*/
export async function createErrorExecution(workflow: WorkflowEntity) {
return await createExecution(
{
finished: false,
stoppedAt: new Date(),
},
workflow,
);
return createExecution({ finished: false, stoppedAt: new Date() }, workflow);
}
/**
* Store a waiting execution in the DB and assign it to a workflow.
*/
export async function createWaitingExecution(workflow: WorkflowEntity) {
return await createExecution(
{
finished: false,
waitTill: new Date(),
},
workflow,
);
return createExecution({ finished: false, waitTill: new Date() }, workflow);
}
// ----------------------------------
@@ -509,7 +502,7 @@ export async function createWaitingExecution(workflow: WorkflowEntity) {
export async function createTag(attributes: Partial<TagEntity> = {}) {
const { name } = attributes;
return await Db.collections.Tag.save({
return Db.collections.Tag.save({
name: name ?? randomName(),
...attributes,
});
@@ -524,7 +517,7 @@ export async function createManyWorkflows(
attributes: Partial<WorkflowEntity> = {},
user?: User,
) {
const workflowRequests = [...Array(amount)].map((_) => createWorkflow(attributes, user));
const workflowRequests = [...Array(amount)].map(async (_) => createWorkflow(attributes, user));
return Promise.all(workflowRequests);
}
@@ -653,7 +646,7 @@ const baseOptions = (type: TestDBType) => ({
port: config.getEnv(`database.${type}db.port`),
username: config.getEnv(`database.${type}db.user`),
password: config.getEnv(`database.${type}db.password`),
schema: type === 'postgres' ? config.getEnv(`database.postgresdb.schema`) : undefined,
schema: type === 'postgres' ? config.getEnv('database.postgresdb.schema') : undefined,
});
/**

View File

@@ -24,6 +24,7 @@ type EndpointGroup =
| 'workflows'
| 'publicApi'
| 'nodes'
| 'ldap'
| 'eventBus'
| 'license';

View File

@@ -26,8 +26,6 @@ import type { N8nApp } from '@/UserManagement/Interfaces';
import superagent from 'superagent';
import request from 'supertest';
import { URL } from 'url';
import { v4 as uuid } from 'uuid';
import config from '@/config';
import * as Db from '@/Db';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
@@ -69,6 +67,10 @@ import type {
import { licenseController } from '@/license/license.controller';
import { eventBusRouter } from '@/eventbus/eventBusRoutes';
import { v4 as uuid } from 'uuid';
import { handleLdapInit } from '../../../src/Ldap/helpers';
import { ldapController } from '@/Ldap/routes/ldap.controller.ee';
const loadNodesAndCredentials: INodesAndCredentials = {
loaded: { nodes: {}, credentials: {} },
known: { nodes: {}, credentials: {} },
@@ -130,6 +132,7 @@ export async function initTestServer({
license: { controller: licenseController, path: 'license' },
eventBus: { controller: eventBusRouter, path: 'eventbus' },
publicApi: apiRouters,
ldap: { controller: ldapController, path: 'ldap' },
};
for (const group of routerEndpoints) {
@@ -173,7 +176,15 @@ const classifyEndpointGroups = (endpointGroups: string[]) => {
const routerEndpoints: string[] = [];
const functionEndpoints: string[] = [];
const ROUTER_GROUP = ['credentials', 'nodes', 'workflows', 'publicApi', 'license', 'eventBus'];
const ROUTER_GROUP = [
'credentials',
'nodes',
'workflows',
'publicApi',
'ldap',
'eventBus',
'license',
];
endpointGroups.forEach((group) =>
(ROUTER_GROUP.includes(group) ? routerEndpoints : functionEndpoints).push(group),
@@ -239,6 +250,13 @@ export async function initCredentialsTypes(): Promise<void> {
};
}
/**
* Initialize LDAP manager.
*/
export async function initLdapManager(): Promise<void> {
await handleLdapInit();
}
/**
* Initialize node types.
*/