mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
feat(core): Add LDAP support (#3835)
This commit is contained in:
parent
259296c5c9
commit
0c70a40317
|
@ -5,7 +5,7 @@ const tsJestOptions = {
|
||||||
tsconfig: {
|
tsconfig: {
|
||||||
...compilerOptions,
|
...compilerOptions,
|
||||||
declaration: false,
|
declaration: false,
|
||||||
sourceMap: false,
|
sourceMap: true,
|
||||||
skipLibCheck: true,
|
skipLibCheck: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -147,6 +147,7 @@
|
||||||
"jsonschema": "^1.4.1",
|
"jsonschema": "^1.4.1",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"jwks-rsa": "~1.12.1",
|
"jwks-rsa": "~1.12.1",
|
||||||
|
"ldapts": "^4.2.2",
|
||||||
"localtunnel": "^2.0.0",
|
"localtunnel": "^2.0.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.intersection": "^4.4.0",
|
"lodash.intersection": "^4.4.0",
|
||||||
|
|
|
@ -172,6 +172,8 @@ export async function init(
|
||||||
collections.Tag = linkRepository(entities.TagEntity);
|
collections.Tag = linkRepository(entities.TagEntity);
|
||||||
collections.Role = linkRepository(entities.Role);
|
collections.Role = linkRepository(entities.Role);
|
||||||
collections.User = linkRepository(entities.User);
|
collections.User = linkRepository(entities.User);
|
||||||
|
collections.AuthIdentity = linkRepository(entities.AuthIdentity);
|
||||||
|
collections.AuthProviderSyncHistory = linkRepository(entities.AuthProviderSyncHistory);
|
||||||
collections.SharedCredentials = linkRepository(entities.SharedCredentials);
|
collections.SharedCredentials = linkRepository(entities.SharedCredentials);
|
||||||
collections.SharedWorkflow = linkRepository(entities.SharedWorkflow);
|
collections.SharedWorkflow = linkRepository(entities.SharedWorkflow);
|
||||||
collections.Settings = linkRepository(entities.Settings);
|
collections.Settings = linkRepository(entities.Settings);
|
||||||
|
|
|
@ -28,6 +28,8 @@ import type { FindOperator, Repository } from 'typeorm';
|
||||||
|
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
|
|
||||||
|
import type { AuthIdentity, AuthProviderType } from '@db/entities/AuthIdentity';
|
||||||
|
import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory';
|
||||||
import type { InstalledNodes } from '@db/entities/InstalledNodes';
|
import type { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
import type { Role } from '@db/entities/Role';
|
import type { Role } from '@db/entities/Role';
|
||||||
|
@ -64,6 +66,8 @@ export interface ICredentialsOverwrite {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDatabaseCollections {
|
export interface IDatabaseCollections {
|
||||||
|
AuthIdentity: Repository<AuthIdentity>;
|
||||||
|
AuthProviderSyncHistory: Repository<AuthProviderSyncHistory>;
|
||||||
Credentials: Repository<ICredentialsDb>;
|
Credentials: Repository<ICredentialsDb>;
|
||||||
Execution: Repository<IExecutionFlattedDb>;
|
Execution: Repository<IExecutionFlattedDb>;
|
||||||
Workflow: Repository<WorkflowEntity>;
|
Workflow: Repository<WorkflowEntity>;
|
||||||
|
@ -318,6 +322,7 @@ export interface IDiagnosticInfo {
|
||||||
binaryDataMode: string;
|
binaryDataMode: string;
|
||||||
n8n_multi_user_allowed: boolean;
|
n8n_multi_user_allowed: boolean;
|
||||||
smtp_set_up: boolean;
|
smtp_set_up: boolean;
|
||||||
|
ldap_allowed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITelemetryUserDeletionData {
|
export interface ITelemetryUserDeletionData {
|
||||||
|
@ -400,7 +405,13 @@ export interface IInternalHooksClass {
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise<void>;
|
onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise<void>;
|
||||||
onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }, user?: User): Promise<void>;
|
onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }, user?: User): Promise<void>;
|
||||||
onUserSignup(userSignupData: { user: User }): Promise<void>;
|
onUserSignup(
|
||||||
|
user: User,
|
||||||
|
userSignupData: {
|
||||||
|
user_type: AuthProviderType;
|
||||||
|
was_disabled_ldap_user: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void>;
|
||||||
onCommunityPackageInstallFinished(installationData: {
|
onCommunityPackageInstallFinished(installationData: {
|
||||||
user: User;
|
user: User;
|
||||||
input_string: string;
|
input_string: string;
|
||||||
|
@ -519,6 +530,10 @@ export interface IN8nUISettings {
|
||||||
personalizationSurveyEnabled: boolean;
|
personalizationSurveyEnabled: boolean;
|
||||||
defaultLocale: string;
|
defaultLocale: string;
|
||||||
userManagement: IUserManagementSettings;
|
userManagement: IUserManagementSettings;
|
||||||
|
ldap: {
|
||||||
|
loginLabel: string;
|
||||||
|
loginEnabled: boolean;
|
||||||
|
};
|
||||||
publicApi: IPublicApiSettings;
|
publicApi: IPublicApiSettings;
|
||||||
workflowTagsDisabled: boolean;
|
workflowTagsDisabled: boolean;
|
||||||
logLevel: 'info' | 'debug' | 'warn' | 'error' | 'verbose' | 'silent';
|
logLevel: 'info' | 'debug' | 'warn' | 'error' | 'verbose' | 'silent';
|
||||||
|
@ -541,6 +556,7 @@ export interface IN8nUISettings {
|
||||||
};
|
};
|
||||||
enterprise: {
|
enterprise: {
|
||||||
sharing: boolean;
|
sharing: boolean;
|
||||||
|
ldap: boolean;
|
||||||
logStreaming: boolean;
|
logStreaming: boolean;
|
||||||
};
|
};
|
||||||
hideUsagePage: boolean;
|
hideUsagePage: boolean;
|
||||||
|
@ -567,6 +583,9 @@ export interface IUserManagementSettings {
|
||||||
showSetupOnFirstLoad?: boolean;
|
showSetupOnFirstLoad?: boolean;
|
||||||
smtpSetup: boolean;
|
smtpSetup: boolean;
|
||||||
}
|
}
|
||||||
|
export interface IActiveDirectorySettings {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
export interface IPublicApiSettings {
|
export interface IPublicApiSettings {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
latestVersion: number;
|
latestVersion: number;
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
|
import type { AuthProviderType } from '@db/entities/AuthIdentity';
|
||||||
import { RoleService } from './role/role.service';
|
import { RoleService } from './role/role.service';
|
||||||
import { eventBus } from './eventbus';
|
import { eventBus } from './eventbus';
|
||||||
import type { User } from '@db/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
|
@ -65,6 +66,7 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
n8n_binary_data_mode: diagnosticInfo.binaryDataMode,
|
n8n_binary_data_mode: diagnosticInfo.binaryDataMode,
|
||||||
n8n_multi_user_allowed: diagnosticInfo.n8n_multi_user_allowed,
|
n8n_multi_user_allowed: diagnosticInfo.n8n_multi_user_allowed,
|
||||||
smtp_set_up: diagnosticInfo.smtp_set_up,
|
smtp_set_up: diagnosticInfo.smtp_set_up,
|
||||||
|
ldap_allowed: diagnosticInfo.ldap_allowed,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
@ -642,16 +644,23 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
return this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
|
return this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserSignup(userSignupData: { user: User }): Promise<void> {
|
async onUserSignup(
|
||||||
|
user: User,
|
||||||
|
userSignupData: {
|
||||||
|
user_type: AuthProviderType;
|
||||||
|
was_disabled_ldap_user: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
void Promise.all([
|
void Promise.all([
|
||||||
eventBus.sendAuditEvent({
|
eventBus.sendAuditEvent({
|
||||||
eventName: 'n8n.audit.user.signedup',
|
eventName: 'n8n.audit.user.signedup',
|
||||||
payload: {
|
payload: {
|
||||||
...userToPayload(userSignupData.user),
|
...userToPayload(user),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.telemetry.track('User signed up', {
|
this.telemetry.track('User signed up', {
|
||||||
user_id: userSignupData.user.id,
|
user_id: user.id,
|
||||||
|
...userSignupData,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -848,7 +857,49 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async onLdapSyncFinished(data: {
|
||||||
|
type: string;
|
||||||
|
succeeded: boolean;
|
||||||
|
users_synced: number;
|
||||||
|
error: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
return this.telemetry.track('Ldap general sync finished', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onLdapUsersDisabled(data: {
|
||||||
|
reason: 'ldap_update' | 'ldap_feature_deactivated';
|
||||||
|
users: number;
|
||||||
|
user_ids: string[];
|
||||||
|
}): Promise<void> {
|
||||||
|
return this.telemetry.track('Ldap users disabled', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onUserUpdatedLdapSettings(data: {
|
||||||
|
user_id: string;
|
||||||
|
loginIdAttribute: string;
|
||||||
|
firstNameAttribute: string;
|
||||||
|
lastNameAttribute: string;
|
||||||
|
emailAttribute: string;
|
||||||
|
ldapIdAttribute: string;
|
||||||
|
searchPageSize: number;
|
||||||
|
searchTimeout: number;
|
||||||
|
synchronizationEnabled: boolean;
|
||||||
|
synchronizationInterval: number;
|
||||||
|
loginLabel: string;
|
||||||
|
loginEnabled: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
|
return this.telemetry.track('Ldap general sync finished', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onLdapLoginSyncFailed(data: { error: string }): Promise<void> {
|
||||||
|
return this.telemetry.track('Ldap login sync failed', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async userLoginFailedDueToLdapDisabled(data: { user_id: string }): Promise<void> {
|
||||||
|
return this.telemetry.track('User login failed since ldap disabled', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
* Execution Statistics
|
* Execution Statistics
|
||||||
*/
|
*/
|
||||||
async onFirstProductionWorkflowSuccess(data: {
|
async onFirstProductionWorkflowSuccess(data: {
|
||||||
|
|
38
packages/cli/src/Ldap/LdapManager.ee.ts
Normal file
38
packages/cli/src/Ldap/LdapManager.ee.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { LdapService } from './LdapService.ee';
|
||||||
|
import { LdapSync } from './LdapSync.ee';
|
||||||
|
import type { LdapConfig } from './types';
|
||||||
|
|
||||||
|
export class LdapManager {
|
||||||
|
private static ldap: {
|
||||||
|
service: LdapService;
|
||||||
|
sync: LdapSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
private static initialized: boolean;
|
||||||
|
|
||||||
|
static getInstance(): {
|
||||||
|
service: LdapService;
|
||||||
|
sync: LdapSync;
|
||||||
|
} {
|
||||||
|
if (!this.initialized) {
|
||||||
|
throw new Error('LDAP Manager has not been initialized');
|
||||||
|
}
|
||||||
|
return this.ldap;
|
||||||
|
}
|
||||||
|
|
||||||
|
static init(config: LdapConfig): void {
|
||||||
|
this.ldap = {
|
||||||
|
service: new LdapService(),
|
||||||
|
sync: new LdapSync(),
|
||||||
|
};
|
||||||
|
this.ldap.service.config = config;
|
||||||
|
this.ldap.sync.config = config;
|
||||||
|
this.ldap.sync.ldapService = this.ldap.service;
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateConfig(config: LdapConfig): void {
|
||||||
|
this.ldap.service.config = config;
|
||||||
|
this.ldap.sync.config = config;
|
||||||
|
}
|
||||||
|
}
|
104
packages/cli/src/Ldap/LdapService.ee.ts
Normal file
104
packages/cli/src/Ldap/LdapService.ee.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
import { Client, Entry as LdapUser, ClientOptions } from 'ldapts';
|
||||||
|
import type { LdapConfig } from './types';
|
||||||
|
import { formatUrl, getMappingAttributes } from './helpers';
|
||||||
|
import { BINARY_AD_ATTRIBUTES } from './constants';
|
||||||
|
import { ConnectionOptions } from 'tls';
|
||||||
|
|
||||||
|
export class LdapService {
|
||||||
|
private client: Client | undefined;
|
||||||
|
|
||||||
|
private _config: LdapConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the LDAP configuration and expire the current client
|
||||||
|
*/
|
||||||
|
set config(config: LdapConfig) {
|
||||||
|
this._config = config;
|
||||||
|
this.client = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get new/existing LDAP client,
|
||||||
|
* depending on whether the credentials
|
||||||
|
* were updated or not
|
||||||
|
*/
|
||||||
|
private async getClient() {
|
||||||
|
if (this._config === undefined) {
|
||||||
|
throw new Error('Service cannot be used without setting the property config');
|
||||||
|
}
|
||||||
|
if (this.client === undefined) {
|
||||||
|
const url = formatUrl(
|
||||||
|
this._config.connectionUrl,
|
||||||
|
this._config.connectionPort,
|
||||||
|
this._config.connectionSecurity,
|
||||||
|
);
|
||||||
|
const ldapOptions: ClientOptions = { url };
|
||||||
|
const tlsOptions: ConnectionOptions = {};
|
||||||
|
|
||||||
|
if (this._config.connectionSecurity !== 'none') {
|
||||||
|
Object.assign(tlsOptions, {
|
||||||
|
rejectUnauthorized: !this._config.allowUnauthorizedCerts,
|
||||||
|
});
|
||||||
|
if (this._config.connectionSecurity === 'tls') {
|
||||||
|
ldapOptions.tlsOptions = tlsOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = new Client(ldapOptions);
|
||||||
|
if (this._config.connectionSecurity === 'startTls') {
|
||||||
|
await this.client.startTLS(tlsOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt a binding with the admin credentials
|
||||||
|
*/
|
||||||
|
private async bindAdmin(): Promise<void> {
|
||||||
|
await this.getClient();
|
||||||
|
if (this.client) {
|
||||||
|
await this.client.bind(this._config.bindingAdminDn, this._config.bindingAdminPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search the LDAP server using the administrator binding
|
||||||
|
* (if any, else a anonymous binding will be attempted)
|
||||||
|
*/
|
||||||
|
async searchWithAdminBinding(filter: string): Promise<LdapUser[]> {
|
||||||
|
await this.bindAdmin();
|
||||||
|
if (this.client) {
|
||||||
|
const { searchEntries } = await this.client.search(this._config.baseDn, {
|
||||||
|
attributes: getMappingAttributes(this._config),
|
||||||
|
explicitBufferAttributes: BINARY_AD_ATTRIBUTES,
|
||||||
|
filter,
|
||||||
|
timeLimit: this._config.searchTimeout,
|
||||||
|
paged: { pageSize: this._config.searchPageSize },
|
||||||
|
...(this._config.searchPageSize === 0 && { paged: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.client.unbind();
|
||||||
|
return searchEntries;
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt binding with the user's credentials
|
||||||
|
*/
|
||||||
|
async validUser(dn: string, password: string): Promise<void> {
|
||||||
|
await this.getClient();
|
||||||
|
if (this.client) {
|
||||||
|
await this.client.bind(dn, password);
|
||||||
|
await this.client.unbind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt binding with the administrator credentials, to test the connection
|
||||||
|
*/
|
||||||
|
async testConnection(): Promise<void> {
|
||||||
|
await this.bindAdmin();
|
||||||
|
}
|
||||||
|
}
|
216
packages/cli/src/Ldap/LdapSync.ee.ts
Normal file
216
packages/cli/src/Ldap/LdapSync.ee.ts
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
import type { Entry as LdapUser } from 'ldapts';
|
||||||
|
import { LoggerProxy as Logger } from 'n8n-workflow';
|
||||||
|
import { QueryFailedError } from 'typeorm/error/QueryFailedError';
|
||||||
|
import { LdapService } from './LdapService.ee';
|
||||||
|
import type { LdapConfig } from './types';
|
||||||
|
import {
|
||||||
|
getLdapUserRole,
|
||||||
|
mapLdapUserToDbUser,
|
||||||
|
processUsers,
|
||||||
|
saveLdapSynchronization,
|
||||||
|
createFilter,
|
||||||
|
resolveBinaryAttributes,
|
||||||
|
getLdapIds,
|
||||||
|
} from './helpers';
|
||||||
|
import type { User } from '@db/entities/User';
|
||||||
|
import type { Role } from '@db/entities/Role';
|
||||||
|
import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHistory';
|
||||||
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
|
|
||||||
|
export class LdapSync {
|
||||||
|
private intervalId: NodeJS.Timeout | undefined = undefined;
|
||||||
|
|
||||||
|
private _config: LdapConfig;
|
||||||
|
|
||||||
|
private _ldapService: LdapService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the LDAP configuration
|
||||||
|
*/
|
||||||
|
set config(config: LdapConfig) {
|
||||||
|
this._config = config;
|
||||||
|
// If user disabled synchronization in the UI and there a job schedule,
|
||||||
|
// stop it
|
||||||
|
if (this.intervalId && !this._config.synchronizationEnabled) {
|
||||||
|
this.stop();
|
||||||
|
// If instance crashed with a job scheduled, once the server starts
|
||||||
|
// again, reschedule it.
|
||||||
|
} else if (!this.intervalId && this._config.synchronizationEnabled) {
|
||||||
|
this.scheduleRun();
|
||||||
|
// If job scheduled and the run interval got updated in the UI
|
||||||
|
// stop the current one and schedule a new one with the new internal
|
||||||
|
} else if (this.intervalId && this._config.synchronizationEnabled) {
|
||||||
|
this.stop();
|
||||||
|
this.scheduleRun();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the LDAP service instance
|
||||||
|
*/
|
||||||
|
set ldapService(service: LdapService) {
|
||||||
|
this._ldapService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a synchronization job based on the interval set in the LDAP config
|
||||||
|
*/
|
||||||
|
scheduleRun(): void {
|
||||||
|
if (!this._config.synchronizationInterval) {
|
||||||
|
throw new Error('Interval variable has to be defined');
|
||||||
|
}
|
||||||
|
this.intervalId = setInterval(async () => {
|
||||||
|
await this.run('live');
|
||||||
|
}, this._config.synchronizationInterval * 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the synchronization job.
|
||||||
|
* If the job runs in "live" mode, changes to LDAP users are persisted in the database,
|
||||||
|
* else the users are not modified
|
||||||
|
*/
|
||||||
|
async run(mode: RunningMode): Promise<void> {
|
||||||
|
Logger.debug(`LDAP - Starting a synchronization run in ${mode} mode`);
|
||||||
|
|
||||||
|
let adUsers: LdapUser[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
adUsers = await this._ldapService.searchWithAdminBinding(
|
||||||
|
createFilter(`(${this._config.loginIdAttribute}=*)`, this._config.userFilter),
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger.debug('LDAP - Users return by the query', {
|
||||||
|
users: adUsers,
|
||||||
|
});
|
||||||
|
|
||||||
|
resolveBinaryAttributes(adUsers);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
Logger.error(`LDAP - ${e.message}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startedAt = new Date();
|
||||||
|
|
||||||
|
const localAdUsers = await getLdapIds();
|
||||||
|
|
||||||
|
const role = await getLdapUserRole();
|
||||||
|
|
||||||
|
const { usersToCreate, usersToUpdate, usersToDisable } = this.getUsersToProcess(
|
||||||
|
adUsers,
|
||||||
|
localAdUsers,
|
||||||
|
role,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (usersToDisable.length) {
|
||||||
|
void InternalHooksManager.getInstance().onLdapUsersDisabled({
|
||||||
|
reason: 'ldap_update',
|
||||||
|
users: usersToDisable.length,
|
||||||
|
user_ids: usersToDisable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug('LDAP - Users processed', {
|
||||||
|
created: usersToCreate.length,
|
||||||
|
updated: usersToUpdate.length,
|
||||||
|
disabled: usersToDisable.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
const endedAt = new Date();
|
||||||
|
let status: SyncStatus = 'success';
|
||||||
|
let errorMessage = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mode === 'live') {
|
||||||
|
await processUsers(usersToCreate, usersToUpdate, usersToDisable);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof QueryFailedError) {
|
||||||
|
status = 'error';
|
||||||
|
errorMessage = `${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveLdapSynchronization({
|
||||||
|
startedAt,
|
||||||
|
endedAt,
|
||||||
|
created: usersToCreate.length,
|
||||||
|
updated: usersToUpdate.length,
|
||||||
|
disabled: usersToDisable.length,
|
||||||
|
scanned: adUsers.length,
|
||||||
|
runMode: mode,
|
||||||
|
status,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
void InternalHooksManager.getInstance().onLdapSyncFinished({
|
||||||
|
type: !this.intervalId ? 'scheduled' : `manual_${mode}`,
|
||||||
|
succeeded: true,
|
||||||
|
users_synced: usersToCreate.length + usersToUpdate.length + usersToDisable.length,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.debug('LDAP - Synchronization finished successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the current job scheduled, if any
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
this.intervalId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the user that will be changed (created, updated, disabled), in the database
|
||||||
|
*/
|
||||||
|
private getUsersToProcess(
|
||||||
|
adUsers: LdapUser[],
|
||||||
|
localAdUsers: string[],
|
||||||
|
role: Role,
|
||||||
|
): {
|
||||||
|
usersToCreate: Array<[string, User]>;
|
||||||
|
usersToUpdate: Array<[string, User]>;
|
||||||
|
usersToDisable: string[];
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
usersToCreate: this.getUsersToCreate(adUsers, localAdUsers, role),
|
||||||
|
usersToUpdate: this.getUsersToUpdate(adUsers, localAdUsers),
|
||||||
|
usersToDisable: this.getUsersToDisable(adUsers, localAdUsers),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get users in LDAP that are not in the database yet
|
||||||
|
*/
|
||||||
|
private getUsersToCreate(
|
||||||
|
remoteAdUsers: LdapUser[],
|
||||||
|
localLdapIds: string[],
|
||||||
|
role: Role,
|
||||||
|
): Array<[string, User]> {
|
||||||
|
return remoteAdUsers
|
||||||
|
.filter((adUser) => !localLdapIds.includes(adUser[this._config.ldapIdAttribute] as string))
|
||||||
|
.map((adUser) => mapLdapUserToDbUser(adUser, this._config, role));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get users in LDAP that are already in the database
|
||||||
|
*/
|
||||||
|
private getUsersToUpdate(
|
||||||
|
remoteAdUsers: LdapUser[],
|
||||||
|
localLdapIds: string[],
|
||||||
|
): Array<[string, User]> {
|
||||||
|
return remoteAdUsers
|
||||||
|
.filter((adUser) => localLdapIds.includes(adUser[this._config.ldapIdAttribute] as string))
|
||||||
|
.map((adUser) => mapLdapUserToDbUser(adUser, this._config));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get users that are in the database but not in the LDAP server
|
||||||
|
*/
|
||||||
|
private getUsersToDisable(remoteAdUsers: LdapUser[], localLdapIds: string[]): string[] {
|
||||||
|
const remoteAdUserIds = remoteAdUsers.map((adUser) => adUser[this._config.ldapIdAttribute]);
|
||||||
|
return localLdapIds.filter((user) => !remoteAdUserIds.includes(user));
|
||||||
|
}
|
||||||
|
}
|
133
packages/cli/src/Ldap/constants.ts
Normal file
133
packages/cli/src/Ldap/constants.ts
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import type { LdapConfig } from './types';
|
||||||
|
|
||||||
|
export const LDAP_FEATURE_NAME = 'features.ldap';
|
||||||
|
|
||||||
|
export const LDAP_ENABLED = 'enterprise.features.ldap';
|
||||||
|
|
||||||
|
export const LDAP_LOGIN_LABEL = 'ldap.loginLabel';
|
||||||
|
|
||||||
|
export const LDAP_LOGIN_ENABLED = 'ldap.loginEnabled';
|
||||||
|
|
||||||
|
export const BINARY_AD_ATTRIBUTES = ['objectGUID', 'objectSid'];
|
||||||
|
|
||||||
|
export const LDAP_DEFAULT_CONFIGURATION: LdapConfig = {
|
||||||
|
loginEnabled: false,
|
||||||
|
loginLabel: '',
|
||||||
|
connectionUrl: '',
|
||||||
|
allowUnauthorizedCerts: false,
|
||||||
|
connectionSecurity: 'none',
|
||||||
|
connectionPort: 389,
|
||||||
|
baseDn: '',
|
||||||
|
bindingAdminDn: '',
|
||||||
|
bindingAdminPassword: '',
|
||||||
|
firstNameAttribute: '',
|
||||||
|
lastNameAttribute: '',
|
||||||
|
emailAttribute: '',
|
||||||
|
loginIdAttribute: '',
|
||||||
|
ldapIdAttribute: '',
|
||||||
|
userFilter: '',
|
||||||
|
synchronizationEnabled: false,
|
||||||
|
synchronizationInterval: 60,
|
||||||
|
searchPageSize: 0,
|
||||||
|
searchTimeout: 60,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LDAP_CONFIG_SCHEMA = {
|
||||||
|
$schema: 'https://json-schema.org/draft/2019-09/schema',
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
emailAttribute: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
firstNameAttribute: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
lastNameAttribute: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
ldapIdAttribute: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
loginIdAttribute: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
bindingAdminDn: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
bindingAdminPassword: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
baseDn: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
connectionUrl: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
connectionSecurity: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
connectionPort: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
allowUnauthorizedCerts: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
userFilter: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
loginEnabled: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
loginLabel: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
synchronizationEnabled: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
synchronizationInterval: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
searchPageSize: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
searchTimeout: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'loginEnabled',
|
||||||
|
'loginLabel',
|
||||||
|
'connectionUrl',
|
||||||
|
'allowUnauthorizedCerts',
|
||||||
|
'connectionSecurity',
|
||||||
|
'connectionPort',
|
||||||
|
'baseDn',
|
||||||
|
'bindingAdminDn',
|
||||||
|
'bindingAdminPassword',
|
||||||
|
'firstNameAttribute',
|
||||||
|
'lastNameAttribute',
|
||||||
|
'emailAttribute',
|
||||||
|
'loginIdAttribute',
|
||||||
|
'ldapIdAttribute',
|
||||||
|
'userFilter',
|
||||||
|
'synchronizationEnabled',
|
||||||
|
'synchronizationInterval',
|
||||||
|
'searchPageSize',
|
||||||
|
'searchTimeout',
|
||||||
|
],
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NON_SENSIBLE_LDAP_CONFIG_PROPERTIES: Array<keyof LdapConfig> = [
|
||||||
|
'loginEnabled',
|
||||||
|
'emailAttribute',
|
||||||
|
'firstNameAttribute',
|
||||||
|
'lastNameAttribute',
|
||||||
|
'loginIdAttribute',
|
||||||
|
'ldapIdAttribute',
|
||||||
|
'synchronizationEnabled',
|
||||||
|
'synchronizationInterval',
|
||||||
|
'searchPageSize',
|
||||||
|
'searchTimeout',
|
||||||
|
'loginLabel',
|
||||||
|
];
|
474
packages/cli/src/Ldap/helpers.ts
Normal file
474
packages/cli/src/Ldap/helpers.ts
Normal file
|
@ -0,0 +1,474 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||||
|
import { AES, enc } from 'crypto-js';
|
||||||
|
import type { Entry as LdapUser } from 'ldapts';
|
||||||
|
import { Filter } from 'ldapts/filters/Filter';
|
||||||
|
import { UserSettings } from 'n8n-core';
|
||||||
|
import { validate } from 'jsonschema';
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
import config from '@/config';
|
||||||
|
import type { Role } from '@db/entities/Role';
|
||||||
|
import { User } from '@db/entities/User';
|
||||||
|
import { AuthIdentity } from '@/databases/entities/AuthIdentity';
|
||||||
|
import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory';
|
||||||
|
import { isUserManagementEnabled } from '@/UserManagement/UserManagementHelper';
|
||||||
|
import { LdapManager } from './LdapManager.ee';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BINARY_AD_ATTRIBUTES,
|
||||||
|
LDAP_CONFIG_SCHEMA,
|
||||||
|
LDAP_ENABLED,
|
||||||
|
LDAP_FEATURE_NAME,
|
||||||
|
LDAP_LOGIN_ENABLED,
|
||||||
|
LDAP_LOGIN_LABEL,
|
||||||
|
} from './constants';
|
||||||
|
import type { ConnectionSecurity, LdapConfig } from './types';
|
||||||
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
|
import { jsonParse, LoggerProxy as Logger } from 'n8n-workflow';
|
||||||
|
import { getLicense } from '@/License';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the LDAP feature is disabled in the instance
|
||||||
|
*/
|
||||||
|
export const isLdapEnabled = (): boolean => {
|
||||||
|
const license = getLicense();
|
||||||
|
return isUserManagementEnabled() && (config.getEnv(LDAP_ENABLED) || license.isLdapEnabled());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the LDAP feature is enabled in the instance
|
||||||
|
*/
|
||||||
|
export const isLdapDisabled = (): boolean => !isLdapEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the LDAP login label to the configuration object
|
||||||
|
*/
|
||||||
|
export const setLdapLoginLabel = (value: string): void => {
|
||||||
|
config.set(LDAP_LOGIN_LABEL, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the LDAP login enabled to the configuration object
|
||||||
|
*/
|
||||||
|
export const setLdapLoginEnabled = (value: boolean): void => {
|
||||||
|
config.set(LDAP_LOGIN_ENABLED, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the LDAP login label from the configuration object
|
||||||
|
*/
|
||||||
|
export const getLdapLoginLabel = (): string => config.getEnv(LDAP_LOGIN_LABEL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the LDAP login enabled from the configuration object
|
||||||
|
*/
|
||||||
|
export const isLdapLoginEnabled = (): boolean => config.getEnv(LDAP_LOGIN_ENABLED);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a random password to be assigned to the LDAP users
|
||||||
|
*/
|
||||||
|
export const randomPassword = (): string => {
|
||||||
|
return Math.random().toString(36).slice(-8);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the user role to be assigned to LDAP users
|
||||||
|
*/
|
||||||
|
export const getLdapUserRole = async (): Promise<Role> => {
|
||||||
|
return Db.collections.Role.findOneByOrFail({ scope: 'global', name: 'member' });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the structure of the LDAP configuration schema
|
||||||
|
*/
|
||||||
|
export const validateLdapConfigurationSchema = (
|
||||||
|
ldapConfig: LdapConfig,
|
||||||
|
): { valid: boolean; message: string } => {
|
||||||
|
const { valid, errors } = validate(ldapConfig, LDAP_CONFIG_SCHEMA, { nestedErrors: true });
|
||||||
|
|
||||||
|
let message = '';
|
||||||
|
if (!valid) {
|
||||||
|
message = errors.map((error) => `request.body.${error.path[0]} ${error.message}`).join(',');
|
||||||
|
}
|
||||||
|
return { valid, message };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt password using the instance's encryption key
|
||||||
|
*/
|
||||||
|
export const encryptPassword = async (password: string): Promise<string> => {
|
||||||
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
|
return AES.encrypt(password, encryptionKey).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt password using the instance's encryption key
|
||||||
|
*/
|
||||||
|
export const decryptPassword = async (password: string): Promise<string> => {
|
||||||
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
|
return AES.decrypt(password, encryptionKey).toString(enc.Utf8);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the LDAP configuration (decrypted) form the database
|
||||||
|
*/
|
||||||
|
export const getLdapConfig = async (): Promise<LdapConfig> => {
|
||||||
|
const configuration = await Db.collections.Settings.findOneByOrFail({
|
||||||
|
key: LDAP_FEATURE_NAME,
|
||||||
|
});
|
||||||
|
const configurationData = jsonParse<LdapConfig>(configuration.value);
|
||||||
|
configurationData.bindingAdminPassword = await decryptPassword(
|
||||||
|
configurationData.bindingAdminPassword,
|
||||||
|
);
|
||||||
|
return configurationData;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take the LDAP configuration and set login enabled and login label to the config object
|
||||||
|
*/
|
||||||
|
export const setGlobalLdapConfigVariables = (ldapConfig: LdapConfig): void => {
|
||||||
|
setLdapLoginEnabled(ldapConfig.loginEnabled);
|
||||||
|
setLdapLoginLabel(ldapConfig.loginLabel);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveEntryBinaryAttributes = (entry: LdapUser): LdapUser => {
|
||||||
|
Object.entries(entry)
|
||||||
|
.filter(([k]) => BINARY_AD_ATTRIBUTES.includes(k))
|
||||||
|
.forEach(([k]) => {
|
||||||
|
entry[k] = (entry[k] as Buffer).toString('hex');
|
||||||
|
});
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveBinaryAttributes = (entries: LdapUser[]): void => {
|
||||||
|
entries.forEach((entry) => resolveEntryBinaryAttributes(entry));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the LDAP configuration in the database
|
||||||
|
*/
|
||||||
|
export const updateLdapConfig = async (ldapConfig: LdapConfig): Promise<void> => {
|
||||||
|
const { valid, message } = validateLdapConfigurationSchema(ldapConfig);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
LdapManager.updateConfig({ ...ldapConfig });
|
||||||
|
|
||||||
|
ldapConfig.bindingAdminPassword = await encryptPassword(ldapConfig.bindingAdminPassword);
|
||||||
|
|
||||||
|
if (!ldapConfig.loginEnabled) {
|
||||||
|
ldapConfig.synchronizationEnabled = false;
|
||||||
|
const ldapUsers = await getLdapUsers();
|
||||||
|
if (ldapUsers.length) {
|
||||||
|
await deleteAllLdapIdentities();
|
||||||
|
void InternalHooksManager.getInstance().onLdapUsersDisabled({
|
||||||
|
reason: 'ldap_update',
|
||||||
|
users: ldapUsers.length,
|
||||||
|
user_ids: ldapUsers.map((user) => user.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Db.collections.Settings.update(
|
||||||
|
{ key: LDAP_FEATURE_NAME },
|
||||||
|
{ value: JSON.stringify(ldapConfig), loadOnStartup: true },
|
||||||
|
);
|
||||||
|
setGlobalLdapConfigVariables(ldapConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the LDAP initialization.
|
||||||
|
* If it's the first run of this feature, all the default data is created in the database
|
||||||
|
*/
|
||||||
|
export const handleLdapInit = async (): Promise<void> => {
|
||||||
|
if (!isLdapEnabled()) {
|
||||||
|
const ldapUsers = await getLdapUsers();
|
||||||
|
if (ldapUsers.length) {
|
||||||
|
void InternalHooksManager.getInstance().onLdapUsersDisabled({
|
||||||
|
reason: 'ldap_feature_deactivated',
|
||||||
|
users: ldapUsers.length,
|
||||||
|
user_ids: ldapUsers.map((user) => user.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ldapConfig = await getLdapConfig();
|
||||||
|
|
||||||
|
setGlobalLdapConfigVariables(ldapConfig);
|
||||||
|
|
||||||
|
// init LDAP manager with the current
|
||||||
|
// configuration
|
||||||
|
LdapManager.init(ldapConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFilter = (filter: string, userFilter: string) => {
|
||||||
|
let _filter = `(&(|(objectClass=person)(objectClass=user))${filter})`;
|
||||||
|
if (userFilter) {
|
||||||
|
_filter = `(&${userFilter}${filter}`;
|
||||||
|
}
|
||||||
|
return _filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const escapeFilter = (filter: string): string => {
|
||||||
|
//@ts-ignore
|
||||||
|
return new Filter().escape(filter); /* eslint-disable-line */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find and authenticate user in the LDAP server.
|
||||||
|
*/
|
||||||
|
export const findAndAuthenticateLdapUser = async (
|
||||||
|
loginId: string,
|
||||||
|
password: string,
|
||||||
|
loginIdAttribute: string,
|
||||||
|
userFilter: string,
|
||||||
|
): Promise<LdapUser | undefined> => {
|
||||||
|
const ldapService = LdapManager.getInstance().service;
|
||||||
|
|
||||||
|
// Search for the user with the administrator binding using the
|
||||||
|
// the Login ID attribute and whatever was inputted in the UI's
|
||||||
|
// email input.
|
||||||
|
let searchResult: LdapUser[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
searchResult = await ldapService.searchWithAdminBinding(
|
||||||
|
createFilter(`(${loginIdAttribute}=${escapeFilter(loginId)})`, userFilter),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
void InternalHooksManager.getInstance().onLdapLoginSyncFailed({
|
||||||
|
error: e.message,
|
||||||
|
});
|
||||||
|
Logger.error('LDAP - Error during search', { message: e.message });
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchResult.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the unlikely scenario that more than one user is found (
|
||||||
|
// can happen depending on how the LDAP database is structured
|
||||||
|
// and the LDAP configuration), return the last one found as it
|
||||||
|
// should be the less important in the hierarchy.
|
||||||
|
let user = searchResult.pop();
|
||||||
|
|
||||||
|
if (user === undefined) {
|
||||||
|
user = { dn: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Now with the user distinguished name (unique identifier
|
||||||
|
// for the user) and the password, attempt to validate the
|
||||||
|
// user by binding
|
||||||
|
await ldapService.validUser(user.dn, password);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
Logger.error('LDAP - Error validating user against LDAP server', { message: e.message });
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveEntryBinaryAttributes(user);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve auth identity by LDAP ID from database
|
||||||
|
*/
|
||||||
|
export const getAuthIdentityByLdapId = async (
|
||||||
|
idAttributeValue: string,
|
||||||
|
): Promise<AuthIdentity | null> => {
|
||||||
|
return Db.collections.AuthIdentity.findOne({
|
||||||
|
relations: ['user', 'user.globalRole'],
|
||||||
|
where: {
|
||||||
|
providerId: idAttributeValue,
|
||||||
|
providerType: 'ldap',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserByEmail = async (email: string): Promise<User | null> => {
|
||||||
|
return Db.collections.User.findOne({
|
||||||
|
where: { email },
|
||||||
|
relations: ['globalRole'],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map attributes from the LDAP server to the proper columns in the database
|
||||||
|
* e.g. mail => email | uid => ldapId
|
||||||
|
*/
|
||||||
|
export const mapLdapAttributesToUser = (
|
||||||
|
ldapUser: LdapUser,
|
||||||
|
ldapConfig: LdapConfig,
|
||||||
|
): [AuthIdentity['providerId'], Pick<User, 'email' | 'firstName' | 'lastName'>] => {
|
||||||
|
return [
|
||||||
|
ldapUser[ldapConfig.ldapIdAttribute] as string,
|
||||||
|
{
|
||||||
|
email: ldapUser[ldapConfig.emailAttribute] as string,
|
||||||
|
firstName: ldapUser[ldapConfig.firstNameAttribute] as string,
|
||||||
|
lastName: ldapUser[ldapConfig.lastNameAttribute] as string,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve LDAP ID of all LDAP users in the database
|
||||||
|
*/
|
||||||
|
export const getLdapIds = async (): Promise<string[]> => {
|
||||||
|
const identities = await Db.collections.AuthIdentity.find({
|
||||||
|
select: ['providerId'],
|
||||||
|
where: {
|
||||||
|
providerType: 'ldap',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return identities.map((i) => i.providerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLdapUsers = async (): Promise<User[]> => {
|
||||||
|
const identities = await Db.collections.AuthIdentity.find({
|
||||||
|
relations: ['user'],
|
||||||
|
where: {
|
||||||
|
providerType: 'ldap',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return identities.map((i) => i.user);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a LDAP user to database user
|
||||||
|
*/
|
||||||
|
export const mapLdapUserToDbUser = (
|
||||||
|
ldapUser: LdapUser,
|
||||||
|
ldapConfig: LdapConfig,
|
||||||
|
role?: Role,
|
||||||
|
): [string, User] => {
|
||||||
|
const user = new User();
|
||||||
|
const [ldapId, data] = mapLdapAttributesToUser(ldapUser, ldapConfig);
|
||||||
|
Object.assign(user, data);
|
||||||
|
if (role) {
|
||||||
|
user.globalRole = role;
|
||||||
|
user.password = randomPassword();
|
||||||
|
user.disabled = false;
|
||||||
|
} else {
|
||||||
|
user.disabled = true;
|
||||||
|
}
|
||||||
|
return [ldapId, user];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save "toCreateUsers" in the database
|
||||||
|
* Update "toUpdateUsers" in the database
|
||||||
|
* Update "ToDisableUsers" in the database
|
||||||
|
*/
|
||||||
|
export const processUsers = async (
|
||||||
|
toCreateUsers: Array<[string, User]>,
|
||||||
|
toUpdateUsers: Array<[string, User]>,
|
||||||
|
toDisableUsers: string[],
|
||||||
|
): Promise<void> => {
|
||||||
|
await Db.transaction(async (transactionManager) => {
|
||||||
|
return Promise.all([
|
||||||
|
...toCreateUsers.map(async ([ldapId, user]) => {
|
||||||
|
const authIdentity = AuthIdentity.create(await transactionManager.save(user), ldapId);
|
||||||
|
return transactionManager.save(authIdentity);
|
||||||
|
}),
|
||||||
|
...toUpdateUsers.map(async ([ldapId, user]) => {
|
||||||
|
const authIdentity = await transactionManager.findOneBy(AuthIdentity, {
|
||||||
|
providerId: ldapId,
|
||||||
|
});
|
||||||
|
if (authIdentity?.userId) {
|
||||||
|
await transactionManager.update(
|
||||||
|
User,
|
||||||
|
{ id: authIdentity.userId },
|
||||||
|
{ email: user.email, firstName: user.firstName, lastName: user.lastName },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...toDisableUsers.map(async (ldapId) => {
|
||||||
|
const authIdentity = await transactionManager.findOneBy(AuthIdentity, {
|
||||||
|
providerId: ldapId,
|
||||||
|
});
|
||||||
|
if (authIdentity?.userId) {
|
||||||
|
await transactionManager.update(User, { id: authIdentity?.userId }, { disabled: true });
|
||||||
|
await transactionManager.delete(AuthIdentity, { userId: authIdentity?.userId });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a LDAP synchronization data to the database
|
||||||
|
*/
|
||||||
|
export const saveLdapSynchronization = async (
|
||||||
|
data: Omit<AuthProviderSyncHistory, 'id' | 'providerType'>,
|
||||||
|
): Promise<void> => {
|
||||||
|
await Db.collections.AuthProviderSyncHistory.save({
|
||||||
|
...data,
|
||||||
|
providerType: 'ldap',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all LDAP synchronizations in the database
|
||||||
|
*/
|
||||||
|
export const getLdapSynchronizations = async (
|
||||||
|
page: number,
|
||||||
|
perPage: number,
|
||||||
|
): Promise<AuthProviderSyncHistory[]> => {
|
||||||
|
const _page = Math.abs(page);
|
||||||
|
return Db.collections.AuthProviderSyncHistory.find({
|
||||||
|
where: { providerType: 'ldap' },
|
||||||
|
order: { id: 'DESC' },
|
||||||
|
take: perPage,
|
||||||
|
skip: _page * perPage,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the LDAP connection URL to conform with LDAP client library
|
||||||
|
*/
|
||||||
|
export const formatUrl = (url: string, port: number, security: ConnectionSecurity) => {
|
||||||
|
const protocol = ['tls'].includes(security) ? 'ldaps' : 'ldap';
|
||||||
|
return `${protocol}://${url}:${port}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMappingAttributes = (ldapConfig: LdapConfig): string[] => {
|
||||||
|
return [
|
||||||
|
ldapConfig.emailAttribute,
|
||||||
|
ldapConfig.ldapIdAttribute,
|
||||||
|
ldapConfig.firstNameAttribute,
|
||||||
|
ldapConfig.lastNameAttribute,
|
||||||
|
ldapConfig.emailAttribute,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createLdapAuthIdentity = async (user: User, ldapId: string) => {
|
||||||
|
return Db.collections.AuthIdentity.save(AuthIdentity.create(user, ldapId));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createLdapUserOnLocalDb = async (role: Role, data: Partial<User>, ldapId: string) => {
|
||||||
|
const user = await Db.collections.User.save({
|
||||||
|
password: randomPassword(),
|
||||||
|
globalRole: role,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
await createLdapAuthIdentity(user, ldapId);
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateLdapUserOnLocalDb = async (identity: AuthIdentity, data: Partial<User>) => {
|
||||||
|
const userId = identity?.user?.id;
|
||||||
|
if (userId) {
|
||||||
|
await Db.collections.User.update({ id: userId }, data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAllLdapIdentities = async () => {
|
||||||
|
return Db.collections.AuthIdentity.delete({ providerType: 'ldap' });
|
||||||
|
};
|
76
packages/cli/src/Ldap/routes/ldap.controller.ee.ts
Normal file
76
packages/cli/src/Ldap/routes/ldap.controller.ee.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { LdapManager } from '../LdapManager.ee';
|
||||||
|
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '../helpers';
|
||||||
|
import type { LdapConfiguration } from '../types';
|
||||||
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
|
import pick from 'lodash.pick';
|
||||||
|
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '../constants';
|
||||||
|
|
||||||
|
export const ldapController = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /ldap/config
|
||||||
|
*/
|
||||||
|
ldapController.get('/config', async (req: express.Request, res: express.Response) => {
|
||||||
|
const data = await getLdapConfig();
|
||||||
|
return res.status(200).json({ data });
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* POST /ldap/test-connection
|
||||||
|
*/
|
||||||
|
ldapController.post('/test-connection', async (req: express.Request, res: express.Response) => {
|
||||||
|
try {
|
||||||
|
await LdapManager.getInstance().service.testConnection();
|
||||||
|
} catch (error) {
|
||||||
|
const errorObject = error as { message: string };
|
||||||
|
return res.status(400).json({ message: errorObject.message });
|
||||||
|
}
|
||||||
|
return res.status(200).json();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /ldap/config
|
||||||
|
*/
|
||||||
|
ldapController.put('/config', async (req: LdapConfiguration.Update, res: express.Response) => {
|
||||||
|
try {
|
||||||
|
await updateLdapConfig(req.body);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
return res.status(400).json({ message: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getLdapConfig();
|
||||||
|
|
||||||
|
void InternalHooksManager.getInstance().onUserUpdatedLdapSettings({
|
||||||
|
user_id: req.user.id,
|
||||||
|
...pick(data, NON_SENSIBLE_LDAP_CONFIG_PROPERTIES),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ data });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /ldap/sync
|
||||||
|
*/
|
||||||
|
ldapController.post('/sync', async (req: LdapConfiguration.Sync, res: express.Response) => {
|
||||||
|
const runType = req.body.type;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await LdapManager.getInstance().sync.run(runType);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
return res.status(400).json({ message: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.status(200).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /ldap/sync
|
||||||
|
*/
|
||||||
|
ldapController.get('/sync', async (req: LdapConfiguration.GetSync, res: express.Response) => {
|
||||||
|
const { page = '0', perPage = '20' } = req.query;
|
||||||
|
const data = await getLdapSynchronizations(parseInt(page, 10), parseInt(perPage, 10));
|
||||||
|
return res.status(200).json({ data });
|
||||||
|
});
|
32
packages/cli/src/Ldap/types.ts
Normal file
32
packages/cli/src/Ldap/types.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import type { RunningMode } from '@db/entities/AuthProviderSyncHistory';
|
||||||
|
import { AuthenticatedRequest } from '@/requests';
|
||||||
|
|
||||||
|
export type ConnectionSecurity = 'none' | 'tls' | 'startTls';
|
||||||
|
|
||||||
|
export interface LdapConfig {
|
||||||
|
loginEnabled: boolean;
|
||||||
|
loginLabel: string;
|
||||||
|
connectionUrl: string;
|
||||||
|
allowUnauthorizedCerts: boolean;
|
||||||
|
connectionSecurity: ConnectionSecurity;
|
||||||
|
connectionPort: number;
|
||||||
|
baseDn: string;
|
||||||
|
bindingAdminDn: string;
|
||||||
|
bindingAdminPassword: string;
|
||||||
|
firstNameAttribute: string;
|
||||||
|
lastNameAttribute: string;
|
||||||
|
emailAttribute: string;
|
||||||
|
loginIdAttribute: string;
|
||||||
|
ldapIdAttribute: string;
|
||||||
|
userFilter: string;
|
||||||
|
synchronizationEnabled: boolean;
|
||||||
|
synchronizationInterval: number; // minutes
|
||||||
|
searchPageSize: number;
|
||||||
|
searchTimeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare namespace LdapConfiguration {
|
||||||
|
type Update = AuthenticatedRequest<{}, {}, LdapConfig, {}>;
|
||||||
|
type Sync = AuthenticatedRequest<{}, {}, { type: RunningMode }, {}>;
|
||||||
|
type GetSync = AuthenticatedRequest<{}, {}, {}, { page?: string; perPage?: string }>;
|
||||||
|
}
|
|
@ -97,6 +97,10 @@ export class License {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.LOG_STREAMING);
|
return this.isFeatureEnabled(LICENSE_FEATURES.LOG_STREAMING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLdapEnabled() {
|
||||||
|
return this.isFeatureEnabled(LICENSE_FEATURES.LDAP);
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentEntitlements() {
|
getCurrentEntitlements() {
|
||||||
return this.manager?.getCurrentEntitlements() ?? [];
|
return this.manager?.getCurrentEntitlements() ?? [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,12 @@ export class ConflictError extends ResponseError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UnprocessableRequestError extends ResponseError {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message, 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class InternalServerError extends ResponseError {
|
export class InternalServerError extends ResponseError {
|
||||||
constructor(message: string, errorCode = 500) {
|
constructor(message: string, errorCode = 500) {
|
||||||
super(message, 500, errorCode);
|
super(message, 500, errorCode);
|
||||||
|
|
|
@ -152,6 +152,8 @@ import { getLicense } from '@/License';
|
||||||
import { licenseController } from './license/license.controller';
|
import { licenseController } from './license/license.controller';
|
||||||
import { corsMiddleware } from './middlewares/cors';
|
import { corsMiddleware } from './middlewares/cors';
|
||||||
import { initEvents } from './events';
|
import { initEvents } from './events';
|
||||||
|
import { ldapController } from './Ldap/routes/ldap.controller.ee';
|
||||||
|
import { getLdapLoginLabel, isLdapEnabled, isLdapLoginEnabled } from './Ldap/helpers';
|
||||||
import { AbstractServer } from './AbstractServer';
|
import { AbstractServer } from './AbstractServer';
|
||||||
import { configureMetrics } from './metrics';
|
import { configureMetrics } from './metrics';
|
||||||
|
|
||||||
|
@ -243,6 +245,10 @@ class Server extends AbstractServer {
|
||||||
config.getEnv('userManagement.skipInstanceOwnerSetup') === false,
|
config.getEnv('userManagement.skipInstanceOwnerSetup') === false,
|
||||||
smtpSetup: isEmailSetUp(),
|
smtpSetup: isEmailSetUp(),
|
||||||
},
|
},
|
||||||
|
ldap: {
|
||||||
|
loginEnabled: false,
|
||||||
|
loginLabel: '',
|
||||||
|
},
|
||||||
publicApi: {
|
publicApi: {
|
||||||
enabled: !config.getEnv('publicApi.disabled'),
|
enabled: !config.getEnv('publicApi.disabled'),
|
||||||
latestVersion: 1,
|
latestVersion: 1,
|
||||||
|
@ -271,6 +277,7 @@ class Server extends AbstractServer {
|
||||||
},
|
},
|
||||||
enterprise: {
|
enterprise: {
|
||||||
sharing: false,
|
sharing: false,
|
||||||
|
ldap: false,
|
||||||
logStreaming: config.getEnv('enterprise.features.logStreaming'),
|
logStreaming: config.getEnv('enterprise.features.logStreaming'),
|
||||||
},
|
},
|
||||||
hideUsagePage: config.getEnv('hideUsagePage'),
|
hideUsagePage: config.getEnv('hideUsagePage'),
|
||||||
|
@ -297,8 +304,16 @@ class Server extends AbstractServer {
|
||||||
Object.assign(this.frontendSettings.enterprise, {
|
Object.assign(this.frontendSettings.enterprise, {
|
||||||
sharing: isSharingEnabled(),
|
sharing: isSharingEnabled(),
|
||||||
logStreaming: isLogStreamingEnabled(),
|
logStreaming: isLogStreamingEnabled(),
|
||||||
|
ldap: isLdapEnabled(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isLdapEnabled()) {
|
||||||
|
Object.assign(this.frontendSettings.ldap, {
|
||||||
|
loginLabel: getLdapLoginLabel(),
|
||||||
|
loginEnabled: isLdapLoginEnabled(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (config.get('nodes.packagesMissing').length > 0) {
|
if (config.get('nodes.packagesMissing').length > 0) {
|
||||||
this.frontendSettings.missingPackages = true;
|
this.frontendSettings.missingPackages = true;
|
||||||
}
|
}
|
||||||
|
@ -602,6 +617,13 @@ class Server extends AbstractServer {
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
this.app.use(`/${this.restEndpoint}/tags`, tagsController);
|
this.app.use(`/${this.restEndpoint}/tags`, tagsController);
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// LDAP
|
||||||
|
// ----------------------------------------
|
||||||
|
if (isLdapEnabled()) {
|
||||||
|
this.app.use(`/${this.restEndpoint}/ldap`, ldapController);
|
||||||
|
}
|
||||||
|
|
||||||
// Returns parameter values which normally get loaded from an external API or
|
// Returns parameter values which normally get loaded from an external API or
|
||||||
// get generated dynamically
|
// get generated dynamically
|
||||||
this.app.get(
|
this.app.get(
|
||||||
|
@ -1428,6 +1450,7 @@ export async function start(): Promise<void> {
|
||||||
binaryDataMode: binaryDataConfig.mode,
|
binaryDataMode: binaryDataConfig.mode,
|
||||||
n8n_multi_user_allowed: isUserManagementEnabled(),
|
n8n_multi_user_allowed: isUserManagementEnabled(),
|
||||||
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
|
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
|
||||||
|
ldap_allowed: isLdapEnabled(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up event handling
|
// Set up event handling
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { Application } from 'express';
|
import type { Application } from 'express';
|
||||||
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||||
import type { IExternalHooksClass, IPersonalizationSurveyAnswers } from '@/Interfaces';
|
import type { IExternalHooksClass, IPersonalizationSurveyAnswers } from '@/Interfaces';
|
||||||
|
import type { AuthProviderType } from '@/databases/entities/AuthIdentity';
|
||||||
|
import type { Role } from '@/databases/entities/Role';
|
||||||
|
|
||||||
export interface JwtToken {
|
export interface JwtToken {
|
||||||
token: string;
|
token: string;
|
||||||
|
@ -23,6 +25,9 @@ export interface PublicUser {
|
||||||
passwordResetToken?: string;
|
passwordResetToken?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
|
globalRole?: Role;
|
||||||
|
signInType: AuthProviderType;
|
||||||
|
disabled: boolean;
|
||||||
inviteAcceptUrl?: string;
|
inviteAcceptUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -139,18 +139,27 @@ export function sanitizeUser(user: User, withoutKeys?: string[]): PublicUser {
|
||||||
resetPasswordTokenExpiration,
|
resetPasswordTokenExpiration,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
apiKey,
|
apiKey,
|
||||||
...sanitizedUser
|
authIdentities,
|
||||||
|
...rest
|
||||||
} = user;
|
} = user;
|
||||||
if (withoutKeys) {
|
if (withoutKeys) {
|
||||||
withoutKeys.forEach((key) => {
|
withoutKeys.forEach((key) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
delete sanitizedUser[key];
|
delete rest[key];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const sanitizedUser: PublicUser = {
|
||||||
|
...rest,
|
||||||
|
signInType: 'email',
|
||||||
|
};
|
||||||
|
const ldapIdentity = authIdentities?.find((i) => i.providerType === 'ldap');
|
||||||
|
if (ldapIdentity) {
|
||||||
|
sanitizedUser.signInType = 'ldap';
|
||||||
|
}
|
||||||
return sanitizedUser;
|
return sanitizedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addInviteLinktoUser(user: PublicUser, inviterId: string): PublicUser {
|
export function addInviteLinkToUser(user: PublicUser, inviterId: string): PublicUser {
|
||||||
if (user.isPending) {
|
if (user.isPending) {
|
||||||
user.inviteAcceptUrl = generateUserInviteUrl(inviterId, user.id);
|
user.inviteAcceptUrl = generateUserInviteUrl(inviterId, user.id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { AUTH_COOKIE_NAME } from '@/constants';
|
||||||
import { JwtPayload, JwtToken } from '../Interfaces';
|
import { JwtPayload, JwtToken } from '../Interfaces';
|
||||||
import { User } from '@db/entities/User';
|
import { User } from '@db/entities/User';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import * as ResponseHelper from '@/ResponseHelper';
|
||||||
|
|
||||||
export function issueJWT(user: User): JwtToken {
|
export function issueJWT(user: User): JwtToken {
|
||||||
const { id, email, password } = user;
|
const { id, email, password } = user;
|
||||||
|
@ -49,6 +50,12 @@ export async function resolveJwtContent(jwtPayload: JwtPayload): Promise<User> {
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// currently only LDAP users during synchronization
|
||||||
|
// can be set to disabled
|
||||||
|
if (user?.disabled) {
|
||||||
|
throw new ResponseHelper.AuthError('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
if (!user || jwtPayload.password !== passwordHash || user.email !== jwtPayload.email) {
|
if (!user || jwtPayload.password !== passwordHash || user.email !== jwtPayload.email) {
|
||||||
// When owner hasn't been set up, the default user
|
// When owner hasn't been set up, the default user
|
||||||
// won't have email nor password (both equals null)
|
// won't have email nor password (both equals null)
|
||||||
|
|
|
@ -72,7 +72,7 @@ export class UserManagementMailer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async passwordReset(passwordResetData: PasswordResetData): Promise<SendEmailResult> {
|
async passwordReset(passwordResetData: PasswordResetData): Promise<SendEmailResult> {
|
||||||
const template = await getTemplate('passwordReset');
|
const template = await getTemplate('passwordReset', 'passwordReset.html');
|
||||||
const result = await this.mailer?.sendMail({
|
const result = await this.mailer?.sendMail({
|
||||||
emailRecipients: passwordResetData.email,
|
emailRecipients: passwordResetData.email,
|
||||||
subject: 'n8n password reset',
|
subject: 'n8n password reset',
|
||||||
|
|
|
@ -7,10 +7,11 @@ import * as ResponseHelper from '@/ResponseHelper';
|
||||||
import { AUTH_COOKIE_NAME } from '@/constants';
|
import { AUTH_COOKIE_NAME } from '@/constants';
|
||||||
import { issueCookie, resolveJwt } from '../auth/jwt';
|
import { issueCookie, resolveJwt } from '../auth/jwt';
|
||||||
import { N8nApp, PublicUser } from '../Interfaces';
|
import { N8nApp, PublicUser } from '../Interfaces';
|
||||||
import { compareHash, sanitizeUser } from '../UserManagementHelper';
|
import { sanitizeUser } from '../UserManagementHelper';
|
||||||
import { User } from '@db/entities/User';
|
import { User } from '@db/entities/User';
|
||||||
import type { LoginRequest } from '@/requests';
|
import type { LoginRequest } from '@/requests';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
||||||
|
|
||||||
export function authenticationMethods(this: N8nApp): void {
|
export function authenticationMethods(this: N8nApp): void {
|
||||||
/**
|
/**
|
||||||
|
@ -22,6 +23,7 @@ export function authenticationMethods(this: N8nApp): void {
|
||||||
`/${this.restEndpoint}/login`,
|
`/${this.restEndpoint}/login`,
|
||||||
ResponseHelper.send(async (req: LoginRequest, res: Response): Promise<PublicUser> => {
|
ResponseHelper.send(async (req: LoginRequest, res: Response): Promise<PublicUser> => {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
throw new Error('Email is required to log in');
|
throw new Error('Email is required to log in');
|
||||||
}
|
}
|
||||||
|
@ -30,23 +32,23 @@ export function authenticationMethods(this: N8nApp): void {
|
||||||
throw new Error('Password is required to log in');
|
throw new Error('Password is required to log in');
|
||||||
}
|
}
|
||||||
|
|
||||||
let user: User | null;
|
const adUser = await handleLdapLogin(email, password);
|
||||||
try {
|
|
||||||
user = await Db.collections.User.findOne({
|
if (adUser) {
|
||||||
where: { email },
|
await issueCookie(res, adUser);
|
||||||
relations: ['globalRole'],
|
|
||||||
});
|
return sanitizeUser(adUser);
|
||||||
} catch (error) {
|
}
|
||||||
throw new Error('Unable to access database.');
|
|
||||||
|
const localUser = await handleEmailLogin(email, password);
|
||||||
|
|
||||||
|
if (localUser) {
|
||||||
|
await issueCookie(res, localUser);
|
||||||
|
|
||||||
|
return sanitizeUser(localUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user?.password || !(await compareHash(req.body.password, user.password))) {
|
|
||||||
throw new ResponseHelper.AuthError('Wrong username or password. Do you have caps lock on?');
|
throw new ResponseHelper.AuthError('Wrong username or password. Do you have caps lock on?');
|
||||||
}
|
|
||||||
|
|
||||||
await issueCookie(res, user);
|
|
||||||
|
|
||||||
return sanitizeUser(user);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -64,6 +66,11 @@ export function authenticationMethods(this: N8nApp): void {
|
||||||
// If logged in, return user
|
// If logged in, return user
|
||||||
try {
|
try {
|
||||||
user = await resolveJwt(cookieContents);
|
user = await resolveJwt(cookieContents);
|
||||||
|
|
||||||
|
if (!config.get('userManagement.isInstanceOwnerSetUp')) {
|
||||||
|
res.cookie(AUTH_COOKIE_NAME, cookieContents);
|
||||||
|
}
|
||||||
|
|
||||||
return sanitizeUser(user);
|
return sanitizeUser(user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.clearCookie(AUTH_COOKIE_NAME);
|
res.clearCookie(AUTH_COOKIE_NAME);
|
||||||
|
|
|
@ -72,12 +72,23 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Not owner and user exists. We now protect restricted urls.
|
// Not owner and user exists. We now protect restricted urls.
|
||||||
const postRestrictedUrls = [`/${this.restEndpoint}/users`, `/${this.restEndpoint}/owner`];
|
const postRestrictedUrls = [
|
||||||
const getRestrictedUrls: string[] = [];
|
`/${this.restEndpoint}/users`,
|
||||||
|
`/${this.restEndpoint}/owner`,
|
||||||
|
`/${this.restEndpoint}/ldap/sync`,
|
||||||
|
`/${this.restEndpoint}/ldap/test-connection`,
|
||||||
|
];
|
||||||
|
const getRestrictedUrls = [
|
||||||
|
`/${this.restEndpoint}/users`,
|
||||||
|
`/${this.restEndpoint}/ldap/sync`,
|
||||||
|
`/${this.restEndpoint}/ldap/config`,
|
||||||
|
];
|
||||||
|
const putRestrictedUrls = [`/${this.restEndpoint}/ldap/config`];
|
||||||
const trimmedUrl = req.url.endsWith('/') ? req.url.slice(0, -1) : req.url;
|
const trimmedUrl = req.url.endsWith('/') ? req.url.slice(0, -1) : req.url;
|
||||||
if (
|
if (
|
||||||
(req.method === 'POST' && postRestrictedUrls.includes(trimmedUrl)) ||
|
(req.method === 'POST' && postRestrictedUrls.includes(trimmedUrl)) ||
|
||||||
(req.method === 'GET' && getRestrictedUrls.includes(trimmedUrl)) ||
|
(req.method === 'GET' && getRestrictedUrls.includes(trimmedUrl)) ||
|
||||||
|
(req.method === 'PUT' && putRestrictedUrls.includes(trimmedUrl)) ||
|
||||||
(req.method === 'DELETE' &&
|
(req.method === 'DELETE' &&
|
||||||
new RegExp(`/${restEndpoint}/users/[^/]+`, 'gm').test(trimmedUrl)) ||
|
new RegExp(`/${restEndpoint}/users/[^/]+`, 'gm').test(trimmedUrl)) ||
|
||||||
(req.method === 'POST' &&
|
(req.method === 'POST' &&
|
||||||
|
|
|
@ -13,9 +13,10 @@ import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
import { N8nApp } from '../Interfaces';
|
import { N8nApp } from '../Interfaces';
|
||||||
import { getInstanceBaseUrl, hashPassword, validatePassword } from '../UserManagementHelper';
|
import { getInstanceBaseUrl, hashPassword, validatePassword } from '../UserManagementHelper';
|
||||||
import * as UserManagementMailer from '../email';
|
import * as UserManagementMailer from '../email';
|
||||||
import type { PasswordResetRequest } from '../../requests';
|
import type { PasswordResetRequest } from '@/requests';
|
||||||
import { issueCookie } from '../auth/jwt';
|
import { issueCookie } from '../auth/jwt';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import { isLdapEnabled } from '@/Ldap/helpers';
|
||||||
|
|
||||||
export function passwordResetNamespace(this: N8nApp): void {
|
export function passwordResetNamespace(this: N8nApp): void {
|
||||||
/**
|
/**
|
||||||
|
@ -52,9 +53,16 @@ export function passwordResetNamespace(this: N8nApp): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// User should just be able to reset password if one is already present
|
// User should just be able to reset password if one is already present
|
||||||
const user = await Db.collections.User.findOneBy({ email, password: Not(IsNull()) });
|
const user = await Db.collections.User.findOne({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
password: Not(IsNull()),
|
||||||
|
},
|
||||||
|
relations: ['authIdentities'],
|
||||||
|
});
|
||||||
|
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
|
||||||
|
|
||||||
if (!user?.password) {
|
if (!user?.password || (ldapIdentity && user.disabled)) {
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
'Request to send password reset email failed because no user was found for the provided email',
|
'Request to send password reset email failed because no user was found for the provided email',
|
||||||
{ invalidEmail: email },
|
{ invalidEmail: email },
|
||||||
|
@ -62,6 +70,12 @@ export function passwordResetNamespace(this: N8nApp): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLdapEnabled() && ldapIdentity) {
|
||||||
|
throw new ResponseHelper.UnprocessableRequestError(
|
||||||
|
'forgotPassword.ldapUserPasswordResetUnavailable',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
user.resetPasswordToken = uuid();
|
user.resetPasswordToken = uuid();
|
||||||
|
|
||||||
const { id, firstName, lastName, resetPasswordToken } = user;
|
const { id, firstName, lastName, resetPasswordToken } = user;
|
||||||
|
@ -184,10 +198,13 @@ export function passwordResetNamespace(this: N8nApp): void {
|
||||||
// Timestamp is saved in seconds
|
// Timestamp is saved in seconds
|
||||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
const user = await Db.collections.User.findOneBy({
|
const user = await Db.collections.User.findOne({
|
||||||
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
resetPasswordToken,
|
resetPasswordToken,
|
||||||
resetPasswordTokenExpiration: MoreThanOrEqual(currentTimestamp),
|
resetPasswordTokenExpiration: MoreThanOrEqual(currentTimestamp),
|
||||||
|
},
|
||||||
|
relations: ['authIdentities'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -216,6 +233,15 @@ export function passwordResetNamespace(this: N8nApp): void {
|
||||||
fields_changed: ['password'],
|
fields_changed: ['password'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// if this user used to be an LDAP users
|
||||||
|
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
|
||||||
|
if (ldapIdentity) {
|
||||||
|
void InternalHooksManager.getInstance().onUserSignup(user, {
|
||||||
|
user_type: 'email',
|
||||||
|
was_disabled_ldap_user: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await this.externalHooks.run('user.password.update', [user.email, password]);
|
await this.externalHooks.run('user.password.update', [user.email, password]);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { UserRequest } from '@/requests';
|
||||||
import * as UserManagementMailer from '../email/UserManagementMailer';
|
import * as UserManagementMailer from '../email/UserManagementMailer';
|
||||||
import { N8nApp, PublicUser } from '../Interfaces';
|
import { N8nApp, PublicUser } from '../Interfaces';
|
||||||
import {
|
import {
|
||||||
addInviteLinktoUser,
|
addInviteLinkToUser,
|
||||||
generateUserInviteUrl,
|
generateUserInviteUrl,
|
||||||
getInstanceBaseUrl,
|
getInstanceBaseUrl,
|
||||||
hashPassword,
|
hashPassword,
|
||||||
|
@ -27,6 +27,7 @@ import {
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { issueCookie } from '../auth/jwt';
|
import { issueCookie } from '../auth/jwt';
|
||||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
|
import { AuthIdentity } from '@/databases/entities/AuthIdentity';
|
||||||
import { RoleService } from '@/role/role.service';
|
import { RoleService } from '@/role/role.service';
|
||||||
|
|
||||||
export function usersNamespace(this: N8nApp): void {
|
export function usersNamespace(this: N8nApp): void {
|
||||||
|
@ -348,8 +349,9 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
|
|
||||||
await issueCookie(res, updatedUser);
|
await issueCookie(res, updatedUser);
|
||||||
|
|
||||||
void InternalHooksManager.getInstance().onUserSignup({
|
void InternalHooksManager.getInstance().onUserSignup(updatedUser, {
|
||||||
user: updatedUser,
|
user_type: 'email',
|
||||||
|
was_disabled_ldap_user: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.externalHooks.run('user.profile.update', [invitee.email, sanitizeUser(invitee)]);
|
await this.externalHooks.run('user.profile.update', [invitee.email, sanitizeUser(invitee)]);
|
||||||
|
@ -362,11 +364,11 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
this.app.get(
|
this.app.get(
|
||||||
`/${this.restEndpoint}/users`,
|
`/${this.restEndpoint}/users`,
|
||||||
ResponseHelper.send(async (req: UserRequest.List) => {
|
ResponseHelper.send(async (req: UserRequest.List) => {
|
||||||
const users = await Db.collections.User.find({ relations: ['globalRole'] });
|
const users = await Db.collections.User.find({ relations: ['globalRole', 'authIdentities'] });
|
||||||
|
|
||||||
return users.map(
|
return users.map(
|
||||||
(user): PublicUser =>
|
(user): PublicUser =>
|
||||||
addInviteLinktoUser(sanitizeUser(user, ['personalizationAnswers']), req.user.id),
|
addInviteLinkToUser(sanitizeUser(user, ['personalizationAnswers']), req.user.id),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -479,6 +481,8 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
{ user: transferee },
|
{ user: transferee },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await transactionManager.delete(AuthIdentity, { userId: userToDelete.id });
|
||||||
|
|
||||||
// This will remove all shared workflows and credentials not owned
|
// This will remove all shared workflows and credentials not owned
|
||||||
await transactionManager.delete(User, { id: userToDelete.id });
|
await transactionManager.delete(User, { id: userToDelete.id });
|
||||||
});
|
});
|
||||||
|
@ -517,6 +521,7 @@ export function usersNamespace(this: N8nApp): void {
|
||||||
await transactionManager.remove(
|
await transactionManager.remove(
|
||||||
ownedSharedCredentials.map(({ credentials }) => credentials),
|
ownedSharedCredentials.map(({ credentials }) => credentials),
|
||||||
);
|
);
|
||||||
|
await transactionManager.delete(AuthIdentity, { userId: userToDelete.id });
|
||||||
await transactionManager.delete(User, { id: userToDelete.id });
|
await transactionManager.delete(User, { id: userToDelete.id });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@ if (process.env.E2E_TESTS !== 'true') {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tablesToTruncate = [
|
const tablesToTruncate = [
|
||||||
|
'auth_identity',
|
||||||
|
'auth_provider_sync_history',
|
||||||
'event_destinations',
|
'event_destinations',
|
||||||
'shared_workflow',
|
'shared_workflow',
|
||||||
'shared_credentials',
|
'shared_credentials',
|
||||||
|
|
2
packages/cli/src/auth/index.ts
Normal file
2
packages/cli/src/auth/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './methods/email';
|
||||||
|
export * from './methods/ldap';
|
32
packages/cli/src/auth/methods/email.ts
Normal file
32
packages/cli/src/auth/methods/email.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
import type { User } from '@db/entities/User';
|
||||||
|
import { compareHash } from '@/UserManagement/UserManagementHelper';
|
||||||
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
|
import * as ResponseHelper from '@/ResponseHelper';
|
||||||
|
|
||||||
|
export const handleEmailLogin = async (
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<User | undefined> => {
|
||||||
|
const user = await Db.collections.User.findOne({
|
||||||
|
where: { email },
|
||||||
|
relations: ['globalRole', 'authIdentities'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user?.password && (await compareHash(password, user.password))) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point if the user has a LDAP ID, means it was previously an LDAP user,
|
||||||
|
// so suggest to reset the password to gain access to the instance.
|
||||||
|
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
|
||||||
|
if (user && ldapIdentity) {
|
||||||
|
void InternalHooksManager.getInstance().userLoginFailedDueToLdapDisabled({
|
||||||
|
user_id: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new ResponseHelper.AuthError('Reset your password to gain access to the instance.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
69
packages/cli/src/auth/methods/ldap.ts
Normal file
69
packages/cli/src/auth/methods/ldap.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
|
import {
|
||||||
|
createLdapUserOnLocalDb,
|
||||||
|
findAndAuthenticateLdapUser,
|
||||||
|
getLdapConfig,
|
||||||
|
getLdapUserRole,
|
||||||
|
getUserByEmail,
|
||||||
|
getAuthIdentityByLdapId,
|
||||||
|
isLdapDisabled,
|
||||||
|
mapLdapAttributesToUser,
|
||||||
|
createLdapAuthIdentity,
|
||||||
|
updateLdapUserOnLocalDb,
|
||||||
|
} from '@/Ldap/helpers';
|
||||||
|
import type { User } from '@db/entities/User';
|
||||||
|
|
||||||
|
export const handleLdapLogin = async (
|
||||||
|
loginId: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<User | undefined> => {
|
||||||
|
if (isLdapDisabled()) return undefined;
|
||||||
|
|
||||||
|
const ldapConfig = await getLdapConfig();
|
||||||
|
|
||||||
|
if (!ldapConfig.loginEnabled) return undefined;
|
||||||
|
|
||||||
|
const { loginIdAttribute, userFilter } = ldapConfig;
|
||||||
|
|
||||||
|
const ldapUser = await findAndAuthenticateLdapUser(
|
||||||
|
loginId,
|
||||||
|
password,
|
||||||
|
loginIdAttribute,
|
||||||
|
userFilter,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ldapUser) return undefined;
|
||||||
|
|
||||||
|
const [ldapId, ldapAttributesValues] = mapLdapAttributesToUser(ldapUser, ldapConfig);
|
||||||
|
|
||||||
|
const { email: emailAttributeValue } = ldapAttributesValues;
|
||||||
|
|
||||||
|
if (!ldapId || !emailAttributeValue) return undefined;
|
||||||
|
|
||||||
|
const ldapAuthIdentity = await getAuthIdentityByLdapId(ldapId);
|
||||||
|
if (!ldapAuthIdentity) {
|
||||||
|
const emailUser = await getUserByEmail(emailAttributeValue);
|
||||||
|
|
||||||
|
// check if there is an email user with the same email as the authenticated LDAP user trying to log-in
|
||||||
|
if (emailUser && emailUser.email === emailAttributeValue) {
|
||||||
|
const identity = await createLdapAuthIdentity(emailUser, ldapId);
|
||||||
|
await updateLdapUserOnLocalDb(identity, ldapAttributesValues);
|
||||||
|
} else {
|
||||||
|
const role = await getLdapUserRole();
|
||||||
|
const user = await createLdapUserOnLocalDb(role, ldapAttributesValues, ldapId);
|
||||||
|
void InternalHooksManager.getInstance().onUserSignup(user, {
|
||||||
|
user_type: 'ldap',
|
||||||
|
was_disabled_ldap_user: false,
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ldapAuthIdentity.user) {
|
||||||
|
if (ldapAuthIdentity.user.disabled) return undefined;
|
||||||
|
await updateLdapUserOnLocalDb(ldapAuthIdentity, ldapAttributesValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the user again as user's data might have been updated
|
||||||
|
return (await getAuthIdentityByLdapId(ldapId))?.user;
|
||||||
|
};
|
27
packages/cli/src/commands/ldap/reset.ts
Normal file
27
packages/cli/src/commands/ldap/reset.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
import { LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
||||||
|
import { In } from 'typeorm';
|
||||||
|
import { BaseCommand } from '../BaseCommand';
|
||||||
|
|
||||||
|
export class Reset extends BaseCommand {
|
||||||
|
static description = '\nResets the database to the default ldap state';
|
||||||
|
|
||||||
|
async run(): Promise<void> {
|
||||||
|
const ldapIdentities = await Db.collections.AuthIdentity.find({
|
||||||
|
where: { providerType: 'ldap' },
|
||||||
|
select: ['userId'],
|
||||||
|
});
|
||||||
|
await Db.collections.AuthProviderSyncHistory.delete({ providerType: 'ldap' });
|
||||||
|
await Db.collections.AuthIdentity.delete({ providerType: 'ldap' });
|
||||||
|
await Db.collections.User.delete({ id: In(ldapIdentities.map((i) => i.userId)) });
|
||||||
|
await Db.collections.Settings.delete({ key: LDAP_FEATURE_NAME });
|
||||||
|
|
||||||
|
this.logger.info('Successfully reset the database to default ldap state.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error: Error): Promise<void> {
|
||||||
|
this.logger.error('Error resetting database. See log messages for details.');
|
||||||
|
this.logger.error(error.message);
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,7 @@ import { WaitTracker } from '@/WaitTracker';
|
||||||
|
|
||||||
import { getLogger } from '@/Logger';
|
import { getLogger } from '@/Logger';
|
||||||
import { getAllInstalledPackages } from '@/CommunityNodes/packageModel';
|
import { getAllInstalledPackages } from '@/CommunityNodes/packageModel';
|
||||||
|
import { handleLdapInit } from '@/Ldap/helpers';
|
||||||
import { initErrorHandling } from '@/ErrorReporting';
|
import { initErrorHandling } from '@/ErrorReporting';
|
||||||
import * as CrashJournal from '@/CrashJournal';
|
import * as CrashJournal from '@/CrashJournal';
|
||||||
import { createPostHogLoadingScript } from '@/telemetry/scripts';
|
import { createPostHogLoadingScript } from '@/telemetry/scripts';
|
||||||
|
@ -407,6 +408,8 @@ export class Start extends Command {
|
||||||
|
|
||||||
WaitTracker();
|
WaitTracker();
|
||||||
|
|
||||||
|
await handleLdapInit();
|
||||||
|
|
||||||
const editorUrl = GenericHelpers.getBaseUrl();
|
const editorUrl = GenericHelpers.getBaseUrl();
|
||||||
this.log(`\nEditor is now accessible via:\n${editorUrl}`);
|
this.log(`\nEditor is now accessible via:\n${editorUrl}`);
|
||||||
|
|
||||||
|
|
|
@ -972,6 +972,10 @@ export const schema = {
|
||||||
format: Boolean,
|
format: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
ldap: {
|
||||||
|
format: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
logStreaming: {
|
logStreaming: {
|
||||||
format: Boolean,
|
format: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
|
2
packages/cli/src/config/types.d.ts
vendored
2
packages/cli/src/config/types.d.ts
vendored
|
@ -81,6 +81,8 @@ type ExceptionPaths = {
|
||||||
'nodes.include': string[] | undefined;
|
'nodes.include': string[] | undefined;
|
||||||
'userManagement.isInstanceOwnerSetUp': boolean;
|
'userManagement.isInstanceOwnerSetUp': boolean;
|
||||||
'userManagement.skipInstanceOwnerSetup': boolean;
|
'userManagement.skipInstanceOwnerSetup': boolean;
|
||||||
|
'ldap.loginLabel': string;
|
||||||
|
'ldap.loginEnabled': boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
|
|
|
@ -67,6 +67,7 @@ export const SETTINGS_LICENSE_CERT_KEY = 'license.cert';
|
||||||
|
|
||||||
export enum LICENSE_FEATURES {
|
export enum LICENSE_FEATURES {
|
||||||
SHARING = 'feat:sharing',
|
SHARING = 'feat:sharing',
|
||||||
|
LDAP = 'feat:ldap',
|
||||||
LOG_STREAMING = 'feat:logStreaming',
|
LOG_STREAMING = 'feat:logStreaming',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
packages/cli/src/databases/entities/AuthIdentity.ts
Normal file
34
packages/cli/src/databases/entities/AuthIdentity.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryColumn, Unique } from 'typeorm';
|
||||||
|
import { AbstractEntity } from './AbstractEntity';
|
||||||
|
import { User } from './User';
|
||||||
|
|
||||||
|
export type AuthProviderType = 'ldap' | 'email'; //| 'saml' | 'google';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Unique(['providerId', 'providerType'])
|
||||||
|
export class AuthIdentity extends AbstractEntity {
|
||||||
|
@Column()
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.authIdentities)
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@PrimaryColumn()
|
||||||
|
providerId: string;
|
||||||
|
|
||||||
|
@PrimaryColumn()
|
||||||
|
providerType: AuthProviderType;
|
||||||
|
|
||||||
|
static create(
|
||||||
|
user: User,
|
||||||
|
providerId: string,
|
||||||
|
providerType: AuthProviderType = 'ldap',
|
||||||
|
): AuthIdentity {
|
||||||
|
const identity = new AuthIdentity();
|
||||||
|
identity.user = user;
|
||||||
|
identity.userId = user.id;
|
||||||
|
identity.providerId = providerId;
|
||||||
|
identity.providerType = providerType;
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { datetimeColumnType } from './AbstractEntity';
|
||||||
|
import { AuthProviderType } from './AuthIdentity';
|
||||||
|
|
||||||
|
export type RunningMode = 'dry' | 'live';
|
||||||
|
export type SyncStatus = 'success' | 'error';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class AuthProviderSyncHistory {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
providerType: AuthProviderType;
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
runMode: RunningMode;
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
status: SyncStatus;
|
||||||
|
|
||||||
|
@Column(datetimeColumnType)
|
||||||
|
startedAt: Date;
|
||||||
|
|
||||||
|
@Column(datetimeColumnType)
|
||||||
|
endedAt: Date;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
scanned: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
created: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
updated: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
disabled: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
error: string;
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import { NoXss } from '../utils/customValidators';
|
||||||
import { objectRetriever, lowerCaser } from '../utils/transformers';
|
import { objectRetriever, lowerCaser } from '../utils/transformers';
|
||||||
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
|
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
|
||||||
import type { IPersonalizationSurveyAnswers, IUserSettings } from '@/Interfaces';
|
import type { IPersonalizationSurveyAnswers, IUserSettings } from '@/Interfaces';
|
||||||
|
import type { AuthIdentity } from './AuthIdentity';
|
||||||
|
|
||||||
export const MIN_PASSWORD_LENGTH = 8;
|
export const MIN_PASSWORD_LENGTH = 8;
|
||||||
|
|
||||||
|
@ -80,12 +81,18 @@ export class User extends AbstractEntity implements IUser {
|
||||||
@Column()
|
@Column()
|
||||||
globalRoleId: string;
|
globalRoleId: string;
|
||||||
|
|
||||||
|
@OneToMany('AuthIdentity', 'user')
|
||||||
|
authIdentities: AuthIdentity[];
|
||||||
|
|
||||||
@OneToMany('SharedWorkflow', 'user')
|
@OneToMany('SharedWorkflow', 'user')
|
||||||
sharedWorkflows: SharedWorkflow[];
|
sharedWorkflows: SharedWorkflow[];
|
||||||
|
|
||||||
@OneToMany('SharedCredentials', 'user')
|
@OneToMany('SharedCredentials', 'user')
|
||||||
sharedCredentials: SharedCredentials[];
|
sharedCredentials: SharedCredentials[];
|
||||||
|
|
||||||
|
@Column({ type: Boolean, default: false })
|
||||||
|
disabled: boolean;
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
@BeforeUpdate()
|
@BeforeUpdate()
|
||||||
preUpsertHook(): void {
|
preUpsertHook(): void {
|
||||||
|
|
|
@ -1,32 +1,36 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
import { AuthIdentity } from './AuthIdentity';
|
||||||
|
import { AuthProviderSyncHistory } from './AuthProviderSyncHistory';
|
||||||
import { CredentialsEntity } from './CredentialsEntity';
|
import { CredentialsEntity } from './CredentialsEntity';
|
||||||
|
import { EventDestinations } from './MessageEventBusDestinationEntity';
|
||||||
import { ExecutionEntity } from './ExecutionEntity';
|
import { ExecutionEntity } from './ExecutionEntity';
|
||||||
import { WorkflowEntity } from './WorkflowEntity';
|
import { InstalledNodes } from './InstalledNodes';
|
||||||
import { WebhookEntity } from './WebhookEntity';
|
import { InstalledPackages } from './InstalledPackages';
|
||||||
import { TagEntity } from './TagEntity';
|
|
||||||
import { User } from './User';
|
|
||||||
import { Role } from './Role';
|
import { Role } from './Role';
|
||||||
import { Settings } from './Settings';
|
import { Settings } from './Settings';
|
||||||
import { SharedWorkflow } from './SharedWorkflow';
|
|
||||||
import { SharedCredentials } from './SharedCredentials';
|
import { SharedCredentials } from './SharedCredentials';
|
||||||
import { InstalledPackages } from './InstalledPackages';
|
import { SharedWorkflow } from './SharedWorkflow';
|
||||||
import { InstalledNodes } from './InstalledNodes';
|
import { TagEntity } from './TagEntity';
|
||||||
|
import { User } from './User';
|
||||||
|
import { WebhookEntity } from './WebhookEntity';
|
||||||
|
import { WorkflowEntity } from './WorkflowEntity';
|
||||||
import { WorkflowStatistics } from './WorkflowStatistics';
|
import { WorkflowStatistics } from './WorkflowStatistics';
|
||||||
import { EventDestinations } from './MessageEventBusDestinationEntity';
|
|
||||||
|
|
||||||
export const entities = {
|
export const entities = {
|
||||||
|
AuthIdentity,
|
||||||
|
AuthProviderSyncHistory,
|
||||||
CredentialsEntity,
|
CredentialsEntity,
|
||||||
|
EventDestinations,
|
||||||
ExecutionEntity,
|
ExecutionEntity,
|
||||||
WorkflowEntity,
|
InstalledNodes,
|
||||||
WebhookEntity,
|
InstalledPackages,
|
||||||
TagEntity,
|
|
||||||
User,
|
|
||||||
Role,
|
Role,
|
||||||
Settings,
|
Settings,
|
||||||
SharedWorkflow,
|
|
||||||
SharedCredentials,
|
SharedCredentials,
|
||||||
InstalledPackages,
|
SharedWorkflow,
|
||||||
InstalledNodes,
|
TagEntity,
|
||||||
|
User,
|
||||||
|
WebhookEntity,
|
||||||
|
WorkflowEntity,
|
||||||
WorkflowStatistics,
|
WorkflowStatistics,
|
||||||
EventDestinations,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
||||||
|
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
|
||||||
|
|
||||||
|
export class CreateLdapEntities1674509946020 implements MigrationInterface {
|
||||||
|
name = 'CreateLdapEntities1674509946020';
|
||||||
|
|
||||||
|
async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`${tablePrefix}user\` ADD COLUMN disabled BOOLEAN NOT NULL DEFAULT false;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
INSERT INTO ${tablePrefix}settings(\`key\`, value, loadOnStartup)
|
||||||
|
VALUES ('${LDAP_FEATURE_NAME}', '${JSON.stringify(LDAP_DEFAULT_CONFIGURATION)}', 1);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE IF NOT EXISTS \`${tablePrefix}auth_identity\` (
|
||||||
|
\`userId\` VARCHAR(36) REFERENCES \`${tablePrefix}user\` (id),
|
||||||
|
\`providerId\` VARCHAR(64) NOT NULL,
|
||||||
|
\`providerType\` VARCHAR(32) NOT NULL,
|
||||||
|
\`createdAt\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
\`updatedAt\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY(\`providerId\`, \`providerType\`)
|
||||||
|
) ENGINE='InnoDB';`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE IF NOT EXISTS \`${tablePrefix}auth_provider_sync_history\` (
|
||||||
|
\`id\` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
\`providerType\` VARCHAR(32) NOT NULL,
|
||||||
|
\`runMode\` TEXT NOT NULL,
|
||||||
|
\`status\` TEXT NOT NULL,
|
||||||
|
\`startedAt\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
\`endedAt\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
\`scanned\` INTEGER NOT NULL,
|
||||||
|
\`created\` INTEGER NOT NULL,
|
||||||
|
\`updated\` INTEGER NOT NULL,
|
||||||
|
\`disabled\` INTEGER NOT NULL,
|
||||||
|
\`error\` TEXT,
|
||||||
|
PRIMARY KEY (\`id\`)
|
||||||
|
) ENGINE='InnoDB';`,
|
||||||
|
);
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
await queryRunner.query(`DROP TABLE \`${tablePrefix}auth_provider_sync_history\``);
|
||||||
|
await queryRunner.query(`DROP TABLE \`${tablePrefix}auth_identity\``);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`DELETE FROM ${tablePrefix}settings WHERE \`key\` = '${LDAP_FEATURE_NAME}'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`${tablePrefix}user\` DROP COLUMN disabled`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import { AddTriggerCountColumn1669823906994 } from './1669823906994-AddTriggerCo
|
||||||
import { RemoveWorkflowDataLoadedFlag1671726148420 } from './1671726148420-RemoveWorkflowDataLoadedFlag';
|
import { RemoveWorkflowDataLoadedFlag1671726148420 } from './1671726148420-RemoveWorkflowDataLoadedFlag';
|
||||||
import { MessageEventBusDestinations1671535397530 } from './1671535397530-MessageEventBusDestinations';
|
import { MessageEventBusDestinations1671535397530 } from './1671535397530-MessageEventBusDestinations';
|
||||||
import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-DeleteExecutionsWithWorkflows';
|
import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-DeleteExecutionsWithWorkflows';
|
||||||
|
import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntities';
|
||||||
|
|
||||||
export const mysqlMigrations = [
|
export const mysqlMigrations = [
|
||||||
InitialMigration1588157391238,
|
InitialMigration1588157391238,
|
||||||
|
@ -62,4 +63,5 @@ export const mysqlMigrations = [
|
||||||
RemoveWorkflowDataLoadedFlag1671726148420,
|
RemoveWorkflowDataLoadedFlag1671726148420,
|
||||||
MessageEventBusDestinations1671535397530,
|
MessageEventBusDestinations1671535397530,
|
||||||
DeleteExecutionsWithWorkflows1673268682475,
|
DeleteExecutionsWithWorkflows1673268682475,
|
||||||
|
CreateLdapEntities1674509946020,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
||||||
|
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
|
||||||
|
|
||||||
|
export class CreateLdapEntities1674509946020 implements MigrationInterface {
|
||||||
|
name = 'CreateLdapEntities1674509946020';
|
||||||
|
|
||||||
|
async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "${tablePrefix}user" ADD COLUMN disabled BOOLEAN NOT NULL DEFAULT false;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
INSERT INTO ${tablePrefix}settings (key, value, "loadOnStartup")
|
||||||
|
VALUES ('${LDAP_FEATURE_NAME}', '${JSON.stringify(LDAP_DEFAULT_CONFIGURATION)}', true)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE IF NOT EXISTS "${tablePrefix}auth_identity" (
|
||||||
|
"userId" uuid REFERENCES "${tablePrefix}user" (id),
|
||||||
|
"providerId" VARCHAR(64) NOT NULL,
|
||||||
|
"providerType" VARCHAR(32) NOT NULL,
|
||||||
|
"createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY("providerId", "providerType")
|
||||||
|
);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE IF NOT EXISTS "${tablePrefix}auth_provider_sync_history" (
|
||||||
|
"id" serial NOT NULL PRIMARY KEY,
|
||||||
|
"providerType" VARCHAR(32) NOT NULL,
|
||||||
|
"runMode" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL,
|
||||||
|
"startedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"endedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"scanned" INTEGER NOT NULL,
|
||||||
|
"created" INTEGER NOT NULL,
|
||||||
|
"updated" INTEGER NOT NULL,
|
||||||
|
"disabled" INTEGER NOT NULL,
|
||||||
|
"error" TEXT
|
||||||
|
);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
await queryRunner.query(`DROP TABLE "${tablePrefix}auth_provider_sync_history"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "${tablePrefix}auth_identity"`);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`DELETE FROM ${tablePrefix}settings WHERE key = '${LDAP_FEATURE_NAME}'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "${tablePrefix}user" DROP COLUMN disabled`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import { AddTriggerCountColumn1669823906995 } from './1669823906995-AddTriggerCo
|
||||||
import { RemoveWorkflowDataLoadedFlag1671726148421 } from './1671726148421-RemoveWorkflowDataLoadedFlag';
|
import { RemoveWorkflowDataLoadedFlag1671726148421 } from './1671726148421-RemoveWorkflowDataLoadedFlag';
|
||||||
import { MessageEventBusDestinations1671535397530 } from './1671535397530-MessageEventBusDestinations';
|
import { MessageEventBusDestinations1671535397530 } from './1671535397530-MessageEventBusDestinations';
|
||||||
import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-DeleteExecutionsWithWorkflows';
|
import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-DeleteExecutionsWithWorkflows';
|
||||||
|
import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntities';
|
||||||
|
|
||||||
export const postgresMigrations = [
|
export const postgresMigrations = [
|
||||||
InitialMigration1587669153312,
|
InitialMigration1587669153312,
|
||||||
|
@ -58,4 +59,5 @@ export const postgresMigrations = [
|
||||||
RemoveWorkflowDataLoadedFlag1671726148421,
|
RemoveWorkflowDataLoadedFlag1671726148421,
|
||||||
MessageEventBusDestinations1671535397530,
|
MessageEventBusDestinations1671535397530,
|
||||||
DeleteExecutionsWithWorkflows1673268682475,
|
DeleteExecutionsWithWorkflows1673268682475,
|
||||||
|
CreateLdapEntities1674509946020,
|
||||||
];
|
];
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
escapeQuery,
|
escapeQuery,
|
||||||
} from '@db/utils/migrationHelpers';
|
} from '@db/utils/migrationHelpers';
|
||||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
import { isJsonKeyObject, PinData } from '../../utils/migrations.types';
|
import { isJsonKeyObject, PinData } from '@db/utils/migrations.types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert TEXT-type `pinData` column in `workflow_entity` table from
|
* Convert TEXT-type `pinData` column in `workflow_entity` table from
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
||||||
|
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
|
||||||
|
export class CreateLdapEntities1674509946020 implements MigrationInterface {
|
||||||
|
name = 'CreateLdapEntities1674509946020';
|
||||||
|
|
||||||
|
async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE ${tablePrefix}user ADD COLUMN disabled BOOLEAN NOT NULL DEFAULT false;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
INSERT INTO "${tablePrefix}settings" (key, value, loadOnStartup)
|
||||||
|
VALUES ('${LDAP_FEATURE_NAME}', '${JSON.stringify(LDAP_DEFAULT_CONFIGURATION)}', true)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE IF NOT EXISTS "${tablePrefix}auth_identity" (
|
||||||
|
"userId" VARCHAR(36) REFERENCES "${tablePrefix}user" (id),
|
||||||
|
"providerId" VARCHAR(64) NOT NULL,
|
||||||
|
"providerType" VARCHAR(32) NOT NULL,
|
||||||
|
"createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY("providerId", "providerType")
|
||||||
|
);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE IF NOT EXISTS "${tablePrefix}auth_provider_sync_history" (
|
||||||
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"providerType" VARCHAR(32) NOT NULL,
|
||||||
|
"runMode" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL,
|
||||||
|
"startedAt" DATETIME NOT NULL,
|
||||||
|
"endedAt" DATETIME NOT NULL,
|
||||||
|
"scanned" INTEGER NOT NULL,
|
||||||
|
"created" INTEGER NOT NULL,
|
||||||
|
"updated" INTEGER NOT NULL,
|
||||||
|
"disabled" INTEGER NOT NULL,
|
||||||
|
"error" TEXT
|
||||||
|
);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = getTablePrefix();
|
||||||
|
await queryRunner.query(`DROP TABLE "${tablePrefix}auth_provider_sync_history"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "${tablePrefix}auth_identity"`);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`DELETE FROM "${tablePrefix}settings" WHERE key = '${LDAP_FEATURE_NAME}'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "${tablePrefix}user" DROP COLUMN disabled`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,15 +17,16 @@ import { IntroducePinData1654089251344 } from './1654089251344-IntroducePinData'
|
||||||
import { AddNodeIds1658930531669 } from './1658930531669-AddNodeIds';
|
import { AddNodeIds1658930531669 } from './1658930531669-AddNodeIds';
|
||||||
import { AddJsonKeyPinData1659888469333 } from './1659888469333-AddJsonKeyPinData';
|
import { AddJsonKeyPinData1659888469333 } from './1659888469333-AddJsonKeyPinData';
|
||||||
import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCredentialsUserRole';
|
import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCredentialsUserRole';
|
||||||
import { WorkflowStatistics1664196174000 } from './1664196174000-WorkflowStatistics';
|
|
||||||
import { CreateWorkflowsEditorRole1663755770892 } from './1663755770892-CreateWorkflowsUserRole';
|
import { CreateWorkflowsEditorRole1663755770892 } from './1663755770892-CreateWorkflowsUserRole';
|
||||||
import { CreateCredentialUsageTable1665484192211 } from './1665484192211-CreateCredentialUsageTable';
|
import { CreateCredentialUsageTable1665484192211 } from './1665484192211-CreateCredentialUsageTable';
|
||||||
import { RemoveCredentialUsageTable1665754637024 } from './1665754637024-RemoveCredentialUsageTable';
|
import { RemoveCredentialUsageTable1665754637024 } from './1665754637024-RemoveCredentialUsageTable';
|
||||||
import { AddWorkflowVersionIdColumn1669739707124 } from './1669739707124-AddWorkflowVersionIdColumn';
|
import { AddWorkflowVersionIdColumn1669739707124 } from './1669739707124-AddWorkflowVersionIdColumn';
|
||||||
|
import { WorkflowStatistics1664196174000 } from './1664196174000-WorkflowStatistics';
|
||||||
import { AddTriggerCountColumn1669823906993 } from './1669823906993-AddTriggerCountColumn';
|
import { AddTriggerCountColumn1669823906993 } from './1669823906993-AddTriggerCountColumn';
|
||||||
import { RemoveWorkflowDataLoadedFlag1671726148419 } from './1671726148419-RemoveWorkflowDataLoadedFlag';
|
import { RemoveWorkflowDataLoadedFlag1671726148419 } from './1671726148419-RemoveWorkflowDataLoadedFlag';
|
||||||
import { MessageEventBusDestinations1671535397530 } from './1671535397530-MessageEventBusDestinations';
|
import { MessageEventBusDestinations1671535397530 } from './1671535397530-MessageEventBusDestinations';
|
||||||
import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-DeleteExecutionsWithWorkflows';
|
import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-DeleteExecutionsWithWorkflows';
|
||||||
|
import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntities';
|
||||||
|
|
||||||
const sqliteMigrations = [
|
const sqliteMigrations = [
|
||||||
InitialMigration1588102412422,
|
InitialMigration1588102412422,
|
||||||
|
@ -51,11 +52,12 @@ const sqliteMigrations = [
|
||||||
CreateCredentialUsageTable1665484192211,
|
CreateCredentialUsageTable1665484192211,
|
||||||
RemoveCredentialUsageTable1665754637024,
|
RemoveCredentialUsageTable1665754637024,
|
||||||
AddWorkflowVersionIdColumn1669739707124,
|
AddWorkflowVersionIdColumn1669739707124,
|
||||||
AddTriggerCountColumn1669823906993,
|
|
||||||
WorkflowStatistics1664196174000,
|
WorkflowStatistics1664196174000,
|
||||||
|
AddTriggerCountColumn1669823906993,
|
||||||
RemoveWorkflowDataLoadedFlag1671726148419,
|
RemoveWorkflowDataLoadedFlag1671726148419,
|
||||||
MessageEventBusDestinations1671535397530,
|
MessageEventBusDestinations1671535397530,
|
||||||
DeleteExecutionsWithWorkflows1673268682475,
|
DeleteExecutionsWithWorkflows1673268682475,
|
||||||
|
CreateLdapEntities1674509946020,
|
||||||
];
|
];
|
||||||
|
|
||||||
export { sqliteMigrations };
|
export { sqliteMigrations };
|
||||||
|
|
|
@ -31,6 +31,8 @@ beforeAll(async () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await testDb.truncate(['User']);
|
await testDb.truncate(['User']);
|
||||||
|
|
||||||
|
config.set('ldap.disabled', true);
|
||||||
|
|
||||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||||
|
|
||||||
await Db.collections.Settings.update(
|
await Db.collections.Settings.update(
|
||||||
|
|
630
packages/cli/test/integration/ldap/ldap.api.test.ts
Normal file
630
packages/cli/test/integration/ldap/ldap.api.test.ts
Normal file
|
@ -0,0 +1,630 @@
|
||||||
|
import express from 'express';
|
||||||
|
import type { Entry as LdapUser } from 'ldapts';
|
||||||
|
import { jsonParse } from 'n8n-workflow';
|
||||||
|
import config from '@/config';
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
import type { Role } from '@db/entities/Role';
|
||||||
|
import type { User } from '@db/entities/User';
|
||||||
|
import { LDAP_DEFAULT_CONFIGURATION, LDAP_ENABLED, LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
||||||
|
import { LdapManager } from '@/Ldap/LdapManager.ee';
|
||||||
|
import { LdapService } from '@/Ldap/LdapService.ee';
|
||||||
|
import { encryptPassword, saveLdapSynchronization } from '@/Ldap/helpers';
|
||||||
|
import type { LdapConfig } from '@/Ldap/types';
|
||||||
|
import { sanitizeUser } from '@/UserManagement/UserManagementHelper';
|
||||||
|
import { randomEmail, randomName, uniqueId } from './../shared/random';
|
||||||
|
import * as testDb from './../shared/testDb';
|
||||||
|
import type { AuthAgent } from '../shared/types';
|
||||||
|
import * as utils from '../shared/utils';
|
||||||
|
|
||||||
|
jest.mock('@/telemetry');
|
||||||
|
jest.mock('@/UserManagement/email/NodeMailer');
|
||||||
|
|
||||||
|
let app: express.Application;
|
||||||
|
let globalMemberRole: Role;
|
||||||
|
let globalOwnerRole: Role;
|
||||||
|
let owner: User;
|
||||||
|
let authAgent: AuthAgent;
|
||||||
|
|
||||||
|
const defaultLdapConfig = {
|
||||||
|
...LDAP_DEFAULT_CONFIGURATION,
|
||||||
|
loginEnabled: true,
|
||||||
|
loginLabel: '',
|
||||||
|
ldapIdAttribute: 'uid',
|
||||||
|
firstNameAttribute: 'givenName',
|
||||||
|
lastNameAttribute: 'sn',
|
||||||
|
emailAttribute: 'mail',
|
||||||
|
loginIdAttribute: 'mail',
|
||||||
|
baseDn: 'baseDn',
|
||||||
|
bindingAdminDn: 'adminDn',
|
||||||
|
bindingAdminPassword: 'adminPassword',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await testDb.init();
|
||||||
|
app = await utils.initTestServer({ endpointGroups: ['auth', 'ldap'], applyAuth: true });
|
||||||
|
|
||||||
|
const [fetchedGlobalOwnerRole, fetchedGlobalMemberRole] = await testDb.getAllRoles();
|
||||||
|
|
||||||
|
globalOwnerRole = fetchedGlobalOwnerRole;
|
||||||
|
globalMemberRole = fetchedGlobalMemberRole;
|
||||||
|
|
||||||
|
authAgent = utils.createAuthAgent(app);
|
||||||
|
|
||||||
|
config.set(LDAP_ENABLED, true);
|
||||||
|
defaultLdapConfig.bindingAdminPassword = await encryptPassword(
|
||||||
|
defaultLdapConfig.bindingAdminPassword,
|
||||||
|
);
|
||||||
|
|
||||||
|
utils.initConfigFile();
|
||||||
|
utils.initTestLogger();
|
||||||
|
utils.initTestTelemetry();
|
||||||
|
await utils.initLdapManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testDb.truncate([
|
||||||
|
'AuthIdentity',
|
||||||
|
'AuthProviderSyncHistory',
|
||||||
|
'SharedCredentials',
|
||||||
|
'Credentials',
|
||||||
|
'SharedWorkflow',
|
||||||
|
'Workflow',
|
||||||
|
'User',
|
||||||
|
]);
|
||||||
|
|
||||||
|
owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||||
|
|
||||||
|
jest.mock('@/telemetry');
|
||||||
|
|
||||||
|
config.set('userManagement.disabled', false);
|
||||||
|
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||||
|
config.set('userManagement.emails.mode', '');
|
||||||
|
config.set('enterprise.features.ldap', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await testDb.terminate();
|
||||||
|
});
|
||||||
|
|
||||||
|
const createLdapConfig = async (attributes: Partial<LdapConfig> = {}): Promise<LdapConfig> => {
|
||||||
|
const { value: ldapConfig } = await Db.collections.Settings.save({
|
||||||
|
key: LDAP_FEATURE_NAME,
|
||||||
|
value: JSON.stringify({
|
||||||
|
...defaultLdapConfig,
|
||||||
|
...attributes,
|
||||||
|
}),
|
||||||
|
loadOnStartup: true,
|
||||||
|
});
|
||||||
|
return jsonParse(ldapConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Member role should not be able to access ldap routes', async () => {
|
||||||
|
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||||
|
|
||||||
|
let response = await authAgent(member).get('/ldap/config');
|
||||||
|
expect(response.statusCode).toBe(403);
|
||||||
|
|
||||||
|
response = await authAgent(member).put('/ldap/config');
|
||||||
|
expect(response.statusCode).toBe(403);
|
||||||
|
|
||||||
|
response = await authAgent(member).post('/ldap/test-connection');
|
||||||
|
expect(response.statusCode).toBe(403);
|
||||||
|
|
||||||
|
response = await authAgent(member).post('/ldap/sync');
|
||||||
|
expect(response.statusCode).toBe(403);
|
||||||
|
|
||||||
|
response = await authAgent(member).get('/ldap/sync');
|
||||||
|
expect(response.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /ldap/config', () => {
|
||||||
|
test('route should validate payload', async () => {
|
||||||
|
const invalidValuePayload = {
|
||||||
|
...LDAP_DEFAULT_CONFIGURATION,
|
||||||
|
loginEnabled: '', // enabled property only allows boolean
|
||||||
|
loginLabel: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidExtraPropertyPayload = {
|
||||||
|
...LDAP_DEFAULT_CONFIGURATION,
|
||||||
|
example: true, // property not defined in the validation schema
|
||||||
|
};
|
||||||
|
|
||||||
|
const missingPropertyPayload = {
|
||||||
|
loginEnabled: true,
|
||||||
|
loginLabel: '',
|
||||||
|
// missing all other properties defined in the schema
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidPayloads = [
|
||||||
|
invalidValuePayload,
|
||||||
|
invalidExtraPropertyPayload,
|
||||||
|
missingPropertyPayload,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const invalidPayload of invalidPayloads) {
|
||||||
|
const response = await authAgent(owner).put('/ldap/config').send(invalidPayload);
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
expect(response.body).toHaveProperty('message');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('route should update model', async () => {
|
||||||
|
const validPayload = {
|
||||||
|
...LDAP_DEFAULT_CONFIGURATION,
|
||||||
|
loginEnabled: true,
|
||||||
|
loginLabel: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await authAgent(owner).put('/ldap/config').send(validPayload);
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data.loginEnabled).toBe(true);
|
||||||
|
expect(response.body.data.loginLabel).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should apply "Convert all LDAP users to email users" strategy when LDAP login disabled', async () => {
|
||||||
|
const ldapConfig = await createLdapConfig();
|
||||||
|
LdapManager.updateConfig(ldapConfig);
|
||||||
|
|
||||||
|
const member = await testDb.createLdapUser({ globalRole: globalMemberRole }, uniqueId());
|
||||||
|
|
||||||
|
const configuration = ldapConfig;
|
||||||
|
|
||||||
|
// disable the login, so the strategy is applied
|
||||||
|
await authAgent(owner)
|
||||||
|
.put('/ldap/config')
|
||||||
|
.send({ ...configuration, loginEnabled: false });
|
||||||
|
|
||||||
|
const emailUser = await Db.collections.User.findOneByOrFail({ id: member.id });
|
||||||
|
const localLdapIdentities = await testDb.getLdapIdentities();
|
||||||
|
|
||||||
|
expect(emailUser.email).toBe(member.email);
|
||||||
|
expect(emailUser.lastName).toBe(member.lastName);
|
||||||
|
expect(emailUser.firstName).toBe(member.firstName);
|
||||||
|
expect(localLdapIdentities.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /ldap/config route should retrieve current configuration', async () => {
|
||||||
|
const validPayload = {
|
||||||
|
...LDAP_DEFAULT_CONFIGURATION,
|
||||||
|
loginEnabled: true,
|
||||||
|
loginLabel: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = await authAgent(owner).put('/ldap/config').send(validPayload);
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
response = await authAgent(owner).get('/ldap/config');
|
||||||
|
|
||||||
|
expect(response.body.data).toMatchObject(validPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /ldap/test-connection', () => {
|
||||||
|
test('route should success', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(LdapService.prototype, 'testConnection')
|
||||||
|
.mockImplementation(async () => Promise.resolve());
|
||||||
|
|
||||||
|
const response = await authAgent(owner).post('/ldap/test-connection');
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('route should fail', async () => {
|
||||||
|
const errorMessage = 'Invalid connection';
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(LdapService.prototype, 'testConnection')
|
||||||
|
.mockImplementation(async () => Promise.reject(new Error(errorMessage)));
|
||||||
|
|
||||||
|
const response = await authAgent(owner).post('/ldap/test-connection');
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
expect(response.body).toHaveProperty('message');
|
||||||
|
expect(response.body.message).toStrictEqual(errorMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /ldap/sync', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const ldapConfig = await createLdapConfig({
|
||||||
|
ldapIdAttribute: 'uid',
|
||||||
|
firstNameAttribute: 'givenName',
|
||||||
|
lastNameAttribute: 'sn',
|
||||||
|
emailAttribute: 'mail',
|
||||||
|
});
|
||||||
|
LdapManager.updateConfig(ldapConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dry mode', () => {
|
||||||
|
const runTest = async (ldapUsers: LdapUser[]) => {
|
||||||
|
jest
|
||||||
|
.spyOn(LdapService.prototype, 'searchWithAdminBinding')
|
||||||
|
.mockImplementation(async () => Promise.resolve(ldapUsers));
|
||||||
|
|
||||||
|
const response = await authAgent(owner).post('/ldap/sync').send({ type: 'dry' });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
const synchronization = await Db.collections.AuthProviderSyncHistory.findOneByOrFail({});
|
||||||
|
|
||||||
|
expect(synchronization.id).toBeDefined();
|
||||||
|
expect(synchronization.startedAt).toBeDefined();
|
||||||
|
expect(synchronization.endedAt).toBeDefined();
|
||||||
|
expect(synchronization.created).toBeDefined();
|
||||||
|
expect(synchronization.updated).toBeDefined();
|
||||||
|
expect(synchronization.disabled).toBeDefined();
|
||||||
|
expect(synchronization.status).toBeDefined();
|
||||||
|
expect(synchronization.scanned).toBeDefined();
|
||||||
|
expect(synchronization.error).toBeDefined();
|
||||||
|
expect(synchronization.runMode).toBeDefined();
|
||||||
|
expect(synchronization.runMode).toBe('dry');
|
||||||
|
expect(synchronization.scanned).toBe(ldapUsers.length);
|
||||||
|
return synchronization;
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should detect new user but not persist change in model', async () => {
|
||||||
|
const synchronization = await runTest([
|
||||||
|
{
|
||||||
|
dn: '',
|
||||||
|
mail: randomEmail(),
|
||||||
|
sn: randomName(),
|
||||||
|
givenName: randomName(),
|
||||||
|
uid: uniqueId(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(synchronization.created).toBe(1);
|
||||||
|
|
||||||
|
// Make sure only the instance owner is on the DB
|
||||||
|
const localDbUsers = await Db.collections.User.find();
|
||||||
|
expect(localDbUsers.length).toBe(1);
|
||||||
|
expect(localDbUsers[0].id).toBe(owner.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect updated user but not persist change in model', async () => {
|
||||||
|
const ldapUserEmail = randomEmail();
|
||||||
|
const ldapUserId = uniqueId();
|
||||||
|
|
||||||
|
const member = await testDb.createLdapUser(
|
||||||
|
{ globalRole: globalMemberRole, email: ldapUserEmail },
|
||||||
|
ldapUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const synchronization = await runTest([
|
||||||
|
{
|
||||||
|
dn: '',
|
||||||
|
mail: ldapUserEmail,
|
||||||
|
sn: randomName(),
|
||||||
|
givenName: 'updated',
|
||||||
|
uid: ldapUserId,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(synchronization.updated).toBe(1);
|
||||||
|
|
||||||
|
// Make sure the changes in the "LDAP server" were not persisted in the database
|
||||||
|
const localLdapIdentities = await testDb.getLdapIdentities();
|
||||||
|
const localLdapUsers = localLdapIdentities.map(({ user }) => user);
|
||||||
|
expect(localLdapUsers.length).toBe(1);
|
||||||
|
expect(localLdapUsers[0].id).toBe(member.id);
|
||||||
|
expect(localLdapUsers[0].lastName).toBe(member.lastName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect disabled user but not persist change in model', async () => {
|
||||||
|
const ldapUserEmail = randomEmail();
|
||||||
|
const ldapUserId = uniqueId();
|
||||||
|
|
||||||
|
const member = await testDb.createLdapUser(
|
||||||
|
{ globalRole: globalMemberRole, email: ldapUserEmail },
|
||||||
|
ldapUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const synchronization = await runTest([]);
|
||||||
|
|
||||||
|
expect(synchronization.disabled).toBe(1);
|
||||||
|
|
||||||
|
// Make sure the changes in the "LDAP server" were not persisted in the database
|
||||||
|
const localLdapIdentities = await testDb.getLdapIdentities();
|
||||||
|
const localLdapUsers = localLdapIdentities.map(({ user }) => user);
|
||||||
|
expect(localLdapUsers.length).toBe(1);
|
||||||
|
expect(localLdapUsers[0].id).toBe(member.id);
|
||||||
|
expect(localLdapUsers[0].disabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('live mode', () => {
|
||||||
|
const runTest = async (ldapUsers: LdapUser[]) => {
|
||||||
|
jest
|
||||||
|
.spyOn(LdapService.prototype, 'searchWithAdminBinding')
|
||||||
|
.mockImplementation(async () => Promise.resolve(ldapUsers));
|
||||||
|
|
||||||
|
const response = await authAgent(owner).post('/ldap/sync').send({ type: 'live' });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
const synchronization = await Db.collections.AuthProviderSyncHistory.findOneByOrFail({});
|
||||||
|
|
||||||
|
expect(synchronization.id).toBeDefined();
|
||||||
|
expect(synchronization.startedAt).toBeDefined();
|
||||||
|
expect(synchronization.endedAt).toBeDefined();
|
||||||
|
expect(synchronization.created).toBeDefined();
|
||||||
|
expect(synchronization.updated).toBeDefined();
|
||||||
|
expect(synchronization.disabled).toBeDefined();
|
||||||
|
expect(synchronization.status).toBeDefined();
|
||||||
|
expect(synchronization.scanned).toBeDefined();
|
||||||
|
expect(synchronization.error).toBeDefined();
|
||||||
|
expect(synchronization.runMode).toBeDefined();
|
||||||
|
expect(synchronization.runMode).toBe('live');
|
||||||
|
expect(synchronization.scanned).toBe(ldapUsers.length);
|
||||||
|
return synchronization;
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should detect new user and persist change in model', async () => {
|
||||||
|
const ldapUser = {
|
||||||
|
mail: randomEmail(),
|
||||||
|
dn: '',
|
||||||
|
sn: randomName(),
|
||||||
|
givenName: randomName(),
|
||||||
|
uid: uniqueId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const synchronization = await runTest([ldapUser]);
|
||||||
|
expect(synchronization.created).toBe(1);
|
||||||
|
|
||||||
|
// Make sure the changes in the "LDAP server" were persisted in the database
|
||||||
|
const allUsers = await testDb.getAllUsers();
|
||||||
|
expect(allUsers.length).toBe(2);
|
||||||
|
|
||||||
|
const ownerUser = allUsers.find((u) => u.email === owner.email)!;
|
||||||
|
expect(ownerUser.email).toBe(owner.email);
|
||||||
|
|
||||||
|
const memberUser = allUsers.find((u) => u.email !== owner.email)!;
|
||||||
|
expect(memberUser.email).toBe(ldapUser.mail);
|
||||||
|
expect(memberUser.lastName).toBe(ldapUser.sn);
|
||||||
|
expect(memberUser.firstName).toBe(ldapUser.givenName);
|
||||||
|
|
||||||
|
const authIdentities = await testDb.getLdapIdentities();
|
||||||
|
expect(authIdentities.length).toBe(1);
|
||||||
|
expect(authIdentities[0].providerId).toBe(ldapUser.uid);
|
||||||
|
expect(authIdentities[0].providerType).toBe('ldap');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect updated user and persist change in model', async () => {
|
||||||
|
const ldapUser = {
|
||||||
|
mail: randomEmail(),
|
||||||
|
dn: '',
|
||||||
|
sn: 'updated',
|
||||||
|
givenName: randomName(),
|
||||||
|
uid: uniqueId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await testDb.createLdapUser(
|
||||||
|
{
|
||||||
|
globalRole: globalMemberRole,
|
||||||
|
email: ldapUser.mail,
|
||||||
|
firstName: ldapUser.givenName,
|
||||||
|
lastName: randomName(),
|
||||||
|
},
|
||||||
|
ldapUser.uid,
|
||||||
|
);
|
||||||
|
|
||||||
|
const synchronization = await runTest([ldapUser]);
|
||||||
|
expect(synchronization.updated).toBe(1);
|
||||||
|
|
||||||
|
// Make sure the changes in the "LDAP server" were persisted in the database
|
||||||
|
const localLdapIdentities = await testDb.getLdapIdentities();
|
||||||
|
const localLdapUsers = localLdapIdentities.map(({ user }) => user);
|
||||||
|
|
||||||
|
expect(localLdapUsers.length).toBe(1);
|
||||||
|
expect(localLdapUsers[0].email).toBe(ldapUser.mail);
|
||||||
|
expect(localLdapUsers[0].lastName).toBe(ldapUser.sn);
|
||||||
|
expect(localLdapUsers[0].firstName).toBe(ldapUser.givenName);
|
||||||
|
expect(localLdapIdentities[0].providerId).toBe(ldapUser.uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect disabled user and persist change in model', async () => {
|
||||||
|
const ldapUser = {
|
||||||
|
mail: randomEmail(),
|
||||||
|
dn: '',
|
||||||
|
sn: 'updated',
|
||||||
|
givenName: randomName(),
|
||||||
|
uid: uniqueId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await testDb.createLdapUser(
|
||||||
|
{
|
||||||
|
globalRole: globalMemberRole,
|
||||||
|
email: ldapUser.mail,
|
||||||
|
firstName: ldapUser.givenName,
|
||||||
|
lastName: ldapUser.sn,
|
||||||
|
},
|
||||||
|
ldapUser.uid,
|
||||||
|
);
|
||||||
|
|
||||||
|
const synchronization = await runTest([]);
|
||||||
|
expect(synchronization.disabled).toBe(1);
|
||||||
|
|
||||||
|
// Make sure the changes in the "LDAP server" were persisted in the database
|
||||||
|
const allUsers = await testDb.getAllUsers();
|
||||||
|
expect(allUsers.length).toBe(2);
|
||||||
|
|
||||||
|
const ownerUser = allUsers.find((u) => u.email === owner.email)!;
|
||||||
|
expect(ownerUser.email).toBe(owner.email);
|
||||||
|
|
||||||
|
const memberUser = allUsers.find((u) => u.email !== owner.email)!;
|
||||||
|
expect(memberUser.email).toBe(ldapUser.mail);
|
||||||
|
expect(memberUser.lastName).toBe(ldapUser.sn);
|
||||||
|
expect(memberUser.firstName).toBe(ldapUser.givenName);
|
||||||
|
expect(memberUser.disabled).toBe(true);
|
||||||
|
|
||||||
|
const authIdentities = await testDb.getLdapIdentities();
|
||||||
|
expect(authIdentities.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove user instance access once the user is disabled during synchronization', async () => {
|
||||||
|
const member = await testDb.createLdapUser({ globalRole: globalMemberRole }, uniqueId());
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(LdapService.prototype, 'searchWithAdminBinding')
|
||||||
|
.mockImplementation(async () => Promise.resolve([]));
|
||||||
|
|
||||||
|
await authAgent(owner).post('/ldap/sync').send({ type: 'live' });
|
||||||
|
|
||||||
|
const response = await authAgent(member).get('/login');
|
||||||
|
expect(response.body.code).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /ldap/sync should return paginated synchronizations', async () => {
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
await saveLdapSynchronization({
|
||||||
|
created: 0,
|
||||||
|
scanned: 0,
|
||||||
|
updated: 0,
|
||||||
|
disabled: 0,
|
||||||
|
startedAt: new Date(),
|
||||||
|
endedAt: new Date(),
|
||||||
|
status: 'success',
|
||||||
|
error: '',
|
||||||
|
runMode: 'dry',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await authAgent(owner).get('/ldap/sync?perPage=1&page=0');
|
||||||
|
expect(response.body.data.length).toBe(1);
|
||||||
|
|
||||||
|
response = await authAgent(owner).get('/ldap/sync?perPage=1&page=1');
|
||||||
|
expect(response.body.data.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /login', () => {
|
||||||
|
const runTest = async (ldapUser: LdapUser) => {
|
||||||
|
const ldapConfig = await createLdapConfig();
|
||||||
|
LdapManager.updateConfig(ldapConfig);
|
||||||
|
|
||||||
|
const authlessAgent = utils.createAgent(app);
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(LdapService.prototype, 'searchWithAdminBinding')
|
||||||
|
.mockImplementation(async () => Promise.resolve([ldapUser]));
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(LdapService.prototype, 'validUser')
|
||||||
|
.mockImplementation(async () => Promise.resolve());
|
||||||
|
|
||||||
|
const response = await authlessAgent
|
||||||
|
.post('/login')
|
||||||
|
.send({ email: ldapUser.mail, password: 'password' });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.headers['set-cookie']).toBeDefined();
|
||||||
|
expect(response.headers['set-cookie'][0] as string).toContain('n8n-auth=');
|
||||||
|
|
||||||
|
// Make sure the changes in the "LDAP server" were persisted in the database
|
||||||
|
const localLdapIdentities = await testDb.getLdapIdentities();
|
||||||
|
const localLdapUsers = localLdapIdentities.map(({ user }) => user);
|
||||||
|
|
||||||
|
expect(localLdapUsers.length).toBe(1);
|
||||||
|
expect(localLdapUsers[0].email).toBe(ldapUser.mail);
|
||||||
|
expect(localLdapUsers[0].lastName).toBe(ldapUser.sn);
|
||||||
|
expect(localLdapUsers[0].firstName).toBe(ldapUser.givenName);
|
||||||
|
expect(localLdapIdentities[0].providerId).toBe(ldapUser.uid);
|
||||||
|
expect(localLdapUsers[0].disabled).toBe(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should allow new LDAP user to login and synchronize data', async () => {
|
||||||
|
const ldapUser = {
|
||||||
|
mail: randomEmail(),
|
||||||
|
dn: '',
|
||||||
|
sn: '',
|
||||||
|
givenName: randomName(),
|
||||||
|
uid: uniqueId(),
|
||||||
|
};
|
||||||
|
await runTest(ldapUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow existing LDAP user to login and synchronize data', async () => {
|
||||||
|
const ldapUser = {
|
||||||
|
mail: randomEmail(),
|
||||||
|
dn: '',
|
||||||
|
sn: 'updated',
|
||||||
|
givenName: 'updated',
|
||||||
|
uid: uniqueId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await testDb.createLdapUser(
|
||||||
|
{
|
||||||
|
globalRole: globalMemberRole,
|
||||||
|
email: ldapUser.mail,
|
||||||
|
firstName: 'firstname',
|
||||||
|
lastName: 'lastname',
|
||||||
|
},
|
||||||
|
ldapUser.uid,
|
||||||
|
);
|
||||||
|
|
||||||
|
await runTest(ldapUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should transform email user into LDAP user when match found', async () => {
|
||||||
|
const ldapUser = {
|
||||||
|
mail: randomEmail(),
|
||||||
|
dn: '',
|
||||||
|
sn: randomName(),
|
||||||
|
givenName: randomName(),
|
||||||
|
uid: uniqueId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await testDb.createUser({
|
||||||
|
globalRole: globalMemberRole,
|
||||||
|
email: ldapUser.mail,
|
||||||
|
firstName: ldapUser.givenName,
|
||||||
|
lastName: 'lastname',
|
||||||
|
});
|
||||||
|
|
||||||
|
await runTest(ldapUser);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Instance owner should able to delete LDAP users', () => {
|
||||||
|
test("don't transfer workflows", async () => {
|
||||||
|
const ldapConfig = await createLdapConfig();
|
||||||
|
LdapManager.updateConfig(ldapConfig);
|
||||||
|
|
||||||
|
const member = await testDb.createLdapUser({ globalRole: globalMemberRole }, uniqueId());
|
||||||
|
|
||||||
|
await authAgent(owner).post(`/users/${member.id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transfer workflows and credentials', async () => {
|
||||||
|
const ldapConfig = await createLdapConfig();
|
||||||
|
LdapManager.updateConfig(ldapConfig);
|
||||||
|
|
||||||
|
const member = await testDb.createLdapUser({ globalRole: globalMemberRole }, uniqueId());
|
||||||
|
|
||||||
|
// delete the LDAP member and transfer its workflows/credentials to instance owner
|
||||||
|
await authAgent(owner).post(`/users/${member.id}?transferId=${owner.id}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Sign-type should be returned when listing users', async () => {
|
||||||
|
const ldapConfig = await createLdapConfig();
|
||||||
|
LdapManager.updateConfig(ldapConfig);
|
||||||
|
|
||||||
|
await testDb.createLdapUser(
|
||||||
|
{
|
||||||
|
globalRole: globalMemberRole,
|
||||||
|
},
|
||||||
|
uniqueId(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allUsers = await testDb.getAllUsers();
|
||||||
|
expect(allUsers.length).toBe(2);
|
||||||
|
|
||||||
|
const ownerUser = allUsers.find((u) => u.email === owner.email)!;
|
||||||
|
expect(sanitizeUser(ownerUser).signInType).toStrictEqual('email');
|
||||||
|
|
||||||
|
const memberUser = allUsers.find((u) => u.email !== owner.email)!;
|
||||||
|
expect(sanitizeUser(memberUser).signInType).toStrictEqual('ldap');
|
||||||
|
});
|
|
@ -9,13 +9,3 @@ declare module 'supertest' {
|
||||||
extends superagent.SuperAgent<T>,
|
extends superagent.SuperAgent<T>,
|
||||||
Record<string, any> {}
|
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>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '@db/entities/User';
|
import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '@db/entities/User';
|
||||||
import type { CredentialPayload } from './types';
|
import type { CredentialPayload } from './types';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a random alphanumeric string of random length between two limits, both inclusive.
|
* Create a random alphanumeric string of random length between two limits, both inclusive.
|
||||||
|
@ -59,3 +60,5 @@ export const randomCredentialPayload = (): CredentialPayload => ({
|
||||||
nodesAccess: [{ nodeType: randomName() }],
|
nodesAccess: [{ nodeType: randomName() }],
|
||||||
data: { accessToken: randomString(6, 16) },
|
data: { accessToken: randomString(6, 16) },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const uniqueId = () => uuid();
|
||||||
|
|
|
@ -10,17 +10,7 @@ import { mysqlMigrations } from '@db/migrations/mysqldb';
|
||||||
import { postgresMigrations } from '@db/migrations/postgresdb';
|
import { postgresMigrations } from '@db/migrations/postgresdb';
|
||||||
import { sqliteMigrations } from '@db/migrations/sqlite';
|
import { sqliteMigrations } from '@db/migrations/sqlite';
|
||||||
import { hashPassword } from '@/UserManagement/UserManagementHelper';
|
import { hashPassword } from '@/UserManagement/UserManagementHelper';
|
||||||
import { DB_INITIALIZATION_TIMEOUT, MAPPING_TABLES, MAPPING_TABLES_TO_CLEAR } from './constants';
|
import { AuthIdentity } from '@/databases/entities/AuthIdentity';
|
||||||
import {
|
|
||||||
randomApiKey,
|
|
||||||
randomCredentialPayload,
|
|
||||||
randomEmail,
|
|
||||||
randomName,
|
|
||||||
randomString,
|
|
||||||
randomValidPassword,
|
|
||||||
} from './random';
|
|
||||||
import { categorize, getPostgresSchemaSection } from './utils';
|
|
||||||
|
|
||||||
import { ExecutionEntity } from '@db/entities/ExecutionEntity';
|
import { ExecutionEntity } from '@db/entities/ExecutionEntity';
|
||||||
import { InstalledNodes } from '@db/entities/InstalledNodes';
|
import { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||||
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
|
@ -35,6 +25,10 @@ import type {
|
||||||
InstalledPackagePayload,
|
InstalledPackagePayload,
|
||||||
MappingName,
|
MappingName,
|
||||||
} from './types';
|
} 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';
|
import type { DatabaseType, ICredentialsDb } from '@/Interfaces';
|
||||||
|
|
||||||
export type TestDBType = 'postgres' | 'mysql';
|
export type TestDBType = 'postgres' | 'mysql';
|
||||||
|
@ -103,7 +97,7 @@ export async function terminate() {
|
||||||
|
|
||||||
async function truncateMappingTables(
|
async function truncateMappingTables(
|
||||||
dbType: DatabaseType,
|
dbType: DatabaseType,
|
||||||
collections: Array<CollectionName>,
|
collections: CollectionName[],
|
||||||
testDb: Connection,
|
testDb: Connection,
|
||||||
) {
|
) {
|
||||||
const mappingTables = collections.reduce<string[]>((acc, collection) => {
|
const mappingTables = collections.reduce<string[]>((acc, collection) => {
|
||||||
|
@ -115,7 +109,7 @@ async function truncateMappingTables(
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (dbType === 'sqlite') {
|
if (dbType === 'sqlite') {
|
||||||
const promises = mappingTables.map((tableName) =>
|
const promises = mappingTables.map(async (tableName) =>
|
||||||
testDb.query(
|
testDb.query(
|
||||||
`DELETE FROM ${tableName}; DELETE FROM sqlite_sequence WHERE name=${tableName};`,
|
`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 collections Array of entity names whose tables to truncate.
|
||||||
* @param testDbName Name of the test DB to truncate tables in.
|
* @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 dbType = config.getEnv('database.type');
|
||||||
const testDb = Db.getConnection();
|
const testDb = Db.getConnection();
|
||||||
|
|
||||||
if (dbType === 'sqlite') {
|
if (dbType === 'sqlite') {
|
||||||
await testDb.query('PRAGMA foreign_keys=OFF');
|
await testDb.query('PRAGMA foreign_keys=OFF');
|
||||||
|
|
||||||
const truncationPromises = collections.map((collection) => {
|
const truncationPromises = collections.map(async (collection) => {
|
||||||
const tableName = toTableName(collection);
|
const tableName = toTableName(collection);
|
||||||
Db.collections[collection].clear();
|
// Db.collections[collection].clear();
|
||||||
return testDb.query(
|
return testDb.query(
|
||||||
`DELETE FROM ${tableName}; DELETE FROM sqlite_sequence WHERE name=${tableName};`,
|
`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
|
const hasIdColumn = await testDb
|
||||||
.query(`SHOW COLUMNS FROM ${tableName}`)
|
.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;
|
if (!hasIdColumn) continue;
|
||||||
|
|
||||||
|
@ -218,18 +212,20 @@ function toTableName(sourceName: CollectionName | MappingName) {
|
||||||
if (isMapping(sourceName)) return MAPPING_TABLES[sourceName];
|
if (isMapping(sourceName)) return MAPPING_TABLES[sourceName];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
AuthIdentity: 'auth_identity',
|
||||||
|
AuthProviderSyncHistory: 'auth_provider_sync_history',
|
||||||
Credentials: 'credentials_entity',
|
Credentials: 'credentials_entity',
|
||||||
Workflow: 'workflow_entity',
|
|
||||||
Execution: 'execution_entity',
|
Execution: 'execution_entity',
|
||||||
Tag: 'tag_entity',
|
InstalledNodes: 'installed_nodes',
|
||||||
Webhook: 'webhook_entity',
|
InstalledPackages: 'installed_packages',
|
||||||
Role: 'role',
|
Role: 'role',
|
||||||
User: 'user',
|
Settings: 'settings',
|
||||||
SharedCredentials: 'shared_credentials',
|
SharedCredentials: 'shared_credentials',
|
||||||
SharedWorkflow: 'shared_workflow',
|
SharedWorkflow: 'shared_workflow',
|
||||||
Settings: 'settings',
|
Tag: 'tag_entity',
|
||||||
InstalledPackages: 'installed_packages',
|
User: 'user',
|
||||||
InstalledNodes: 'installed_nodes',
|
Webhook: 'webhook_entity',
|
||||||
|
Workflow: 'workflow_entity',
|
||||||
WorkflowStatistics: 'workflow_statistics',
|
WorkflowStatistics: 'workflow_statistics',
|
||||||
EventDestinations: 'event_destinations',
|
EventDestinations: 'event_destinations',
|
||||||
}[sourceName];
|
}[sourceName];
|
||||||
|
@ -243,7 +239,7 @@ function toTableName(sourceName: CollectionName | MappingName) {
|
||||||
* Save a credential to the test DB, sharing it with a user.
|
* Save a credential to the test DB, sharing it with a user.
|
||||||
*/
|
*/
|
||||||
export async function saveCredential(
|
export async function saveCredential(
|
||||||
credentialPayload: CredentialPayload = randomCredentialPayload(),
|
credentialPayload: CredentialPayload,
|
||||||
{ user, role }: { user: User; role: Role },
|
{ user, role }: { user: User; role: Role },
|
||||||
) {
|
) {
|
||||||
const newCredential = new CredentialsEntity();
|
const newCredential = new CredentialsEntity();
|
||||||
|
@ -280,7 +276,7 @@ export async function shareCredentialWithUsers(credential: CredentialsEntity, us
|
||||||
}
|
}
|
||||||
|
|
||||||
export function affixRoleToSaveCredential(role: Role) {
|
export function affixRoleToSaveCredential(role: Role) {
|
||||||
return (credentialPayload: CredentialPayload, { user }: { user: User }) =>
|
return async (credentialPayload: CredentialPayload, { user }: { user: User }) =>
|
||||||
saveCredential(credentialPayload, { user, role });
|
saveCredential(credentialPayload, { user, role });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +289,7 @@ export function affixRoleToSaveCredential(role: Role) {
|
||||||
*/
|
*/
|
||||||
export async function createUser(attributes: Partial<User> = {}): Promise<User> {
|
export async function createUser(attributes: Partial<User> = {}): Promise<User> {
|
||||||
const { email, password, firstName, lastName, globalRole, ...rest } = attributes;
|
const { email, password, firstName, lastName, globalRole, ...rest } = attributes;
|
||||||
const user = {
|
const user: Partial<User> = {
|
||||||
email: email ?? randomEmail(),
|
email: email ?? randomEmail(),
|
||||||
password: await hashPassword(password ?? randomValidPassword()),
|
password: await hashPassword(password ?? randomValidPassword()),
|
||||||
firstName: firstName ?? randomName(),
|
firstName: firstName ?? randomName(),
|
||||||
|
@ -305,11 +301,17 @@ export async function createUser(attributes: Partial<User> = {}): Promise<User>
|
||||||
return Db.collections.User.save(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() {
|
export async function createOwner() {
|
||||||
return createUser({ globalRole: await getGlobalOwnerRole() });
|
return createUser({ globalRole: await getGlobalOwnerRole() });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createUserShell(globalRole: Role): Promise<User> {
|
export async function createUserShell(globalRole: Role): Promise<User> {
|
||||||
if (globalRole.scope !== 'global') {
|
if (globalRole.scope !== 'global') {
|
||||||
throw new Error(`Invalid role received: ${JSON.stringify(globalRole)}`);
|
throw new Error(`Invalid role received: ${JSON.stringify(globalRole)}`);
|
||||||
}
|
}
|
||||||
|
@ -366,7 +368,7 @@ export async function saveInstalledPackage(
|
||||||
return savedInstalledPackage;
|
return savedInstalledPackage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveInstalledNode(
|
export async function saveInstalledNode(
|
||||||
installedNodePayload: InstalledNodePayload,
|
installedNodePayload: InstalledNodePayload,
|
||||||
): Promise<InstalledNodes> {
|
): Promise<InstalledNodes> {
|
||||||
const newInstalledNode = new InstalledNodes();
|
const newInstalledNode = new InstalledNodes();
|
||||||
|
@ -376,7 +378,7 @@ export function saveInstalledNode(
|
||||||
return Db.collections.InstalledNodes.save(newInstalledNode);
|
return Db.collections.InstalledNodes.save(newInstalledNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addApiKey(user: User): Promise<User> {
|
export async function addApiKey(user: User): Promise<User> {
|
||||||
user.apiKey = randomApiKey();
|
user.apiKey = randomApiKey();
|
||||||
return Db.collections.User.save(user);
|
return Db.collections.User.save(user);
|
||||||
}
|
}
|
||||||
|
@ -385,42 +387,42 @@ export function addApiKey(user: User): Promise<User> {
|
||||||
// role fetchers
|
// role fetchers
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
||||||
export function getGlobalOwnerRole() {
|
export async function getGlobalOwnerRole() {
|
||||||
return Db.collections.Role.findOneByOrFail({
|
return Db.collections.Role.findOneByOrFail({
|
||||||
name: 'owner',
|
name: 'owner',
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGlobalMemberRole() {
|
export async function getGlobalMemberRole() {
|
||||||
return Db.collections.Role.findOneByOrFail({
|
return Db.collections.Role.findOneByOrFail({
|
||||||
name: 'member',
|
name: 'member',
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkflowOwnerRole() {
|
export async function getWorkflowOwnerRole() {
|
||||||
return Db.collections.Role.findOneByOrFail({
|
return Db.collections.Role.findOneByOrFail({
|
||||||
name: 'owner',
|
name: 'owner',
|
||||||
scope: 'workflow',
|
scope: 'workflow',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkflowEditorRole() {
|
export async function getWorkflowEditorRole() {
|
||||||
return Db.collections.Role.findOneByOrFail({
|
return Db.collections.Role.findOneByOrFail({
|
||||||
name: 'editor',
|
name: 'editor',
|
||||||
scope: 'workflow',
|
scope: 'workflow',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCredentialOwnerRole() {
|
export async function getCredentialOwnerRole() {
|
||||||
return Db.collections.Role.findOneByOrFail({
|
return Db.collections.Role.findOneByOrFail({
|
||||||
name: 'owner',
|
name: 'owner',
|
||||||
scope: 'credential',
|
scope: 'credential',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllRoles() {
|
export async function getAllRoles() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
getGlobalOwnerRole(),
|
getGlobalOwnerRole(),
|
||||||
getGlobalMemberRole(),
|
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
|
// Execution helpers
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -438,17 +451,14 @@ export async function createManyExecutions(
|
||||||
workflow: WorkflowEntity,
|
workflow: WorkflowEntity,
|
||||||
callback: (workflow: WorkflowEntity) => Promise<ExecutionEntity>,
|
callback: (workflow: WorkflowEntity) => Promise<ExecutionEntity>,
|
||||||
) {
|
) {
|
||||||
const executionsRequests = [...Array(amount)].map((_) => callback(workflow));
|
const executionsRequests = [...Array(amount)].map(async (_) => callback(workflow));
|
||||||
return Promise.all(executionsRequests);
|
return Promise.all(executionsRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a execution in the DB and assign it to a workflow.
|
* Store a execution in the DB and assign it to a workflow.
|
||||||
*/
|
*/
|
||||||
export async function createExecution(
|
async function createExecution(attributes: Partial<ExecutionEntity>, workflow: WorkflowEntity) {
|
||||||
attributes: Partial<ExecutionEntity> = {},
|
|
||||||
workflow: WorkflowEntity,
|
|
||||||
) {
|
|
||||||
const { data, finished, mode, startedAt, stoppedAt, waitTill } = attributes;
|
const { data, finished, mode, startedAt, stoppedAt, waitTill } = attributes;
|
||||||
|
|
||||||
const execution = await Db.collections.Execution.save({
|
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.
|
* Store a successful execution in the DB and assign it to a workflow.
|
||||||
*/
|
*/
|
||||||
export async function createSuccessfulExecution(workflow: WorkflowEntity) {
|
export async function createSuccessfulExecution(workflow: WorkflowEntity) {
|
||||||
return await createExecution(
|
return createExecution({ finished: true }, workflow);
|
||||||
{
|
|
||||||
finished: true,
|
|
||||||
},
|
|
||||||
workflow,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store an error execution in the DB and assign it to a workflow.
|
* Store an error execution in the DB and assign it to a workflow.
|
||||||
*/
|
*/
|
||||||
export async function createErrorExecution(workflow: WorkflowEntity) {
|
export async function createErrorExecution(workflow: WorkflowEntity) {
|
||||||
return await createExecution(
|
return createExecution({ finished: false, stoppedAt: new Date() }, workflow);
|
||||||
{
|
|
||||||
finished: false,
|
|
||||||
stoppedAt: new Date(),
|
|
||||||
},
|
|
||||||
workflow,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a waiting execution in the DB and assign it to a workflow.
|
* Store a waiting execution in the DB and assign it to a workflow.
|
||||||
*/
|
*/
|
||||||
export async function createWaitingExecution(workflow: WorkflowEntity) {
|
export async function createWaitingExecution(workflow: WorkflowEntity) {
|
||||||
return await createExecution(
|
return createExecution({ finished: false, waitTill: new Date() }, workflow);
|
||||||
{
|
|
||||||
finished: false,
|
|
||||||
waitTill: new Date(),
|
|
||||||
},
|
|
||||||
workflow,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -509,7 +502,7 @@ export async function createWaitingExecution(workflow: WorkflowEntity) {
|
||||||
export async function createTag(attributes: Partial<TagEntity> = {}) {
|
export async function createTag(attributes: Partial<TagEntity> = {}) {
|
||||||
const { name } = attributes;
|
const { name } = attributes;
|
||||||
|
|
||||||
return await Db.collections.Tag.save({
|
return Db.collections.Tag.save({
|
||||||
name: name ?? randomName(),
|
name: name ?? randomName(),
|
||||||
...attributes,
|
...attributes,
|
||||||
});
|
});
|
||||||
|
@ -524,7 +517,7 @@ export async function createManyWorkflows(
|
||||||
attributes: Partial<WorkflowEntity> = {},
|
attributes: Partial<WorkflowEntity> = {},
|
||||||
user?: User,
|
user?: User,
|
||||||
) {
|
) {
|
||||||
const workflowRequests = [...Array(amount)].map((_) => createWorkflow(attributes, user));
|
const workflowRequests = [...Array(amount)].map(async (_) => createWorkflow(attributes, user));
|
||||||
return Promise.all(workflowRequests);
|
return Promise.all(workflowRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -653,7 +646,7 @@ const baseOptions = (type: TestDBType) => ({
|
||||||
port: config.getEnv(`database.${type}db.port`),
|
port: config.getEnv(`database.${type}db.port`),
|
||||||
username: config.getEnv(`database.${type}db.user`),
|
username: config.getEnv(`database.${type}db.user`),
|
||||||
password: config.getEnv(`database.${type}db.password`),
|
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,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,6 +24,7 @@ type EndpointGroup =
|
||||||
| 'workflows'
|
| 'workflows'
|
||||||
| 'publicApi'
|
| 'publicApi'
|
||||||
| 'nodes'
|
| 'nodes'
|
||||||
|
| 'ldap'
|
||||||
| 'eventBus'
|
| 'eventBus'
|
||||||
| 'license';
|
| 'license';
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,6 @@ import type { N8nApp } from '@/UserManagement/Interfaces';
|
||||||
import superagent from 'superagent';
|
import superagent from 'superagent';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||||
|
@ -69,6 +67,10 @@ import type {
|
||||||
import { licenseController } from '@/license/license.controller';
|
import { licenseController } from '@/license/license.controller';
|
||||||
import { eventBusRouter } from '@/eventbus/eventBusRoutes';
|
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 = {
|
const loadNodesAndCredentials: INodesAndCredentials = {
|
||||||
loaded: { nodes: {}, credentials: {} },
|
loaded: { nodes: {}, credentials: {} },
|
||||||
known: { nodes: {}, credentials: {} },
|
known: { nodes: {}, credentials: {} },
|
||||||
|
@ -130,6 +132,7 @@ export async function initTestServer({
|
||||||
license: { controller: licenseController, path: 'license' },
|
license: { controller: licenseController, path: 'license' },
|
||||||
eventBus: { controller: eventBusRouter, path: 'eventbus' },
|
eventBus: { controller: eventBusRouter, path: 'eventbus' },
|
||||||
publicApi: apiRouters,
|
publicApi: apiRouters,
|
||||||
|
ldap: { controller: ldapController, path: 'ldap' },
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const group of routerEndpoints) {
|
for (const group of routerEndpoints) {
|
||||||
|
@ -173,7 +176,15 @@ const classifyEndpointGroups = (endpointGroups: string[]) => {
|
||||||
const routerEndpoints: string[] = [];
|
const routerEndpoints: string[] = [];
|
||||||
const functionEndpoints: 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) =>
|
endpointGroups.forEach((group) =>
|
||||||
(ROUTER_GROUP.includes(group) ? routerEndpoints : functionEndpoints).push(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.
|
* Initialize node types.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||||
import { compareHash } from '@/UserManagement/UserManagementHelper';
|
import { compareHash } from '@/UserManagement/UserManagementHelper';
|
||||||
import { SUCCESS_RESPONSE_BODY } from './shared/constants';
|
import { SUCCESS_RESPONSE_BODY } from './shared/constants';
|
||||||
import {
|
import {
|
||||||
|
randomCredentialPayload,
|
||||||
randomEmail,
|
randomEmail,
|
||||||
randomInvalidPassword,
|
randomInvalidPassword,
|
||||||
randomName,
|
randomName,
|
||||||
|
@ -208,7 +209,7 @@ test('DELETE /users/:id with transferId should perform transfer', async () => {
|
||||||
|
|
||||||
const savedWorkflow = await testDb.createWorkflow(undefined, userToDelete);
|
const savedWorkflow = await testDb.createWorkflow(undefined, userToDelete);
|
||||||
|
|
||||||
const savedCredential = await testDb.saveCredential(undefined, {
|
const savedCredential = await testDb.saveCredential(randomCredentialPayload(), {
|
||||||
user: userToDelete,
|
user: userToDelete,
|
||||||
role: credentialOwnerRole,
|
role: credentialOwnerRole,
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,23 @@
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
/>
|
/>
|
||||||
|
<n8n-input-label
|
||||||
|
v-else-if="type === 'toggle'"
|
||||||
|
:inputName="name"
|
||||||
|
:label="label"
|
||||||
|
:tooltipText="tooltipText"
|
||||||
|
:required="required && showRequiredAsterisk"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
{{ tooltipText }}
|
||||||
|
</template>
|
||||||
|
<el-switch
|
||||||
|
:value="value"
|
||||||
|
@change="onInput"
|
||||||
|
:active-color="activeColor"
|
||||||
|
:inactive-color="inactiveColor"
|
||||||
|
></el-switch>
|
||||||
|
</n8n-input-label>
|
||||||
<n8n-input-label
|
<n8n-input-label
|
||||||
v-else
|
v-else
|
||||||
:inputName="name"
|
:inputName="name"
|
||||||
|
@ -20,6 +37,7 @@
|
||||||
:value="value"
|
:value="value"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:multiple="type === 'multi-select'"
|
:multiple="type === 'multi-select'"
|
||||||
|
:disabled="disabled"
|
||||||
@change="onInput"
|
@change="onInput"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
|
@ -41,6 +59,7 @@
|
||||||
:value="value"
|
:value="value"
|
||||||
:maxlength="maxlength"
|
:maxlength="maxlength"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
|
:disabled="disabled"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
|
@ -73,6 +92,7 @@ import N8nSelect from '../N8nSelect';
|
||||||
import N8nOption from '../N8nOption';
|
import N8nOption from '../N8nOption';
|
||||||
import N8nInputLabel from '../N8nInputLabel';
|
import N8nInputLabel from '../N8nInputLabel';
|
||||||
import N8nCheckbox from '../N8nCheckbox';
|
import N8nCheckbox from '../N8nCheckbox';
|
||||||
|
import ElSwitch from 'element-ui/lib/switch';
|
||||||
|
|
||||||
import { getValidationError, VALIDATORS } from './validators';
|
import { getValidationError, VALIDATORS } from './validators';
|
||||||
import { Rule, RuleGroup, IValidator, Validatable, FormState } from '../../types';
|
import { Rule, RuleGroup, IValidator, Validatable, FormState } from '../../types';
|
||||||
|
@ -100,6 +120,11 @@ export interface Props {
|
||||||
name?: string;
|
name?: string;
|
||||||
focusInitially?: boolean;
|
focusInitially?: boolean;
|
||||||
labelSize?: 'small' | 'medium';
|
labelSize?: 'small' | 'medium';
|
||||||
|
disabled?: boolean;
|
||||||
|
activeLabel?: string;
|
||||||
|
activeColor?: string;
|
||||||
|
inactiveLabel?: string;
|
||||||
|
inactiveColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
|
@ -27,6 +27,7 @@ const Template: StoryFn = (args, { argTypes }) => ({
|
||||||
|
|
||||||
export const FormInputs = Template.bind({});
|
export const FormInputs = Template.bind({});
|
||||||
FormInputs.args = {
|
FormInputs.args = {
|
||||||
|
columnView: true,
|
||||||
inputs: [
|
inputs: [
|
||||||
{
|
{
|
||||||
name: 'email',
|
name: 'email',
|
||||||
|
@ -79,5 +80,15 @@ FormInputs.args = {
|
||||||
tooltipText: 'Check this if you agree to be contacted by our marketing team',
|
tooltipText: 'Check this if you agree to be contacted by our marketing team',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'activate',
|
||||||
|
properties: {
|
||||||
|
type: 'toggle',
|
||||||
|
label: 'Activated',
|
||||||
|
activeColor: '#13ce66',
|
||||||
|
inactiveColor: '#8899AA',
|
||||||
|
tooltipText: 'Check this if you agree to be contacted by our marketing team',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,12 +2,18 @@
|
||||||
<ResizeObserver :breakpoints="[{ bp: 'md', width: 500 }]">
|
<ResizeObserver :breakpoints="[{ bp: 'md', width: 500 }]">
|
||||||
<template #default="{ bp }">
|
<template #default="{ bp }">
|
||||||
<div :class="bp === 'md' || columnView ? $style.grid : $style.gridMulti">
|
<div :class="bp === 'md' || columnView ? $style.grid : $style.gridMulti">
|
||||||
<div v-for="input in filteredInputs" :key="input.name">
|
<div
|
||||||
|
v-for="(input, index) in filteredInputs"
|
||||||
|
:key="input.name"
|
||||||
|
:class="{ [`mt-${verticalSpacing}`]: index > 0 }"
|
||||||
|
>
|
||||||
<n8n-text
|
<n8n-text
|
||||||
color="text-base"
|
color="text-base"
|
||||||
v-if="input.properties.type === 'info'"
|
v-if="input.properties.type === 'info'"
|
||||||
tag="div"
|
tag="div"
|
||||||
align="center"
|
:size="input.properties.labelSize"
|
||||||
|
:align="input.properties.labelAlignment"
|
||||||
|
class="form-text"
|
||||||
>
|
>
|
||||||
{{ input.properties.label }}
|
{{ input.properties.label }}
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
|
@ -15,11 +21,13 @@
|
||||||
v-else
|
v-else
|
||||||
v-bind="input.properties"
|
v-bind="input.properties"
|
||||||
:name="input.name"
|
:name="input.name"
|
||||||
|
:label="input.properties.label || ''"
|
||||||
:value="values[input.name]"
|
:value="values[input.name]"
|
||||||
:data-test-id="input.name"
|
:data-test-id="input.name"
|
||||||
:showValidationWarnings="showValidationWarnings"
|
:showValidationWarnings="showValidationWarnings"
|
||||||
@input="(value) => onInput(input.name, value)"
|
@input="(value) => onInput(input.name, value)"
|
||||||
@validate="(value) => onValidate(input.name, value)"
|
@validate="(value) => onValidate(input.name, value)"
|
||||||
|
@change="(value) => onInput(input.name, value)"
|
||||||
@enter="onSubmit"
|
@enter="onSubmit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,6 +60,12 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
columnView: {
|
columnView: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
verticalSpacing: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
validator: (value: string): boolean => ['xs', 's', 'm', 'm', 'l', 'xl'].includes(value),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -10,14 +10,22 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="$style.infoContainer">
|
<div v-else :class="$style.infoContainer">
|
||||||
<div>
|
<div>
|
||||||
<n8n-text :bold="true" color="text-dark"
|
<n8n-text :bold="true" color="text-dark">
|
||||||
>{{ firstName }} {{ lastName }}
|
{{ firstName }} {{ lastName }}
|
||||||
{{ isCurrentUser ? this.t('nds.userInfo.you') : '' }}</n8n-text
|
{{ isCurrentUser ? this.t('nds.userInfo.you') : '' }}
|
||||||
>
|
</n8n-text>
|
||||||
|
<span v-if="disabled" :class="$style.pendingBadge">
|
||||||
|
<n8n-badge :bold="true">Disabled</n8n-badge>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<n8n-text size="small" color="text-light">{{ email }}</n8n-text>
|
<n8n-text size="small" color="text-light">{{ email }}</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!isOwner">
|
||||||
|
<n8n-text v-if="signInType" size="small" color="text-light">
|
||||||
|
Sign-in type: {{ signInType }}
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -47,6 +55,9 @@ export default mixins(Locale).extend({
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
isOwner: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
isPendingUser: {
|
isPendingUser: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
@ -55,7 +66,10 @@ export default mixins(Locale).extend({
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
},
|
||||||
|
signInType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -40,28 +40,16 @@ UserSelect.args = {
|
||||||
firstName: 'Sunny',
|
firstName: 'Sunny',
|
||||||
lastName: 'Side',
|
lastName: 'Side',
|
||||||
email: 'sunny@n8n.io',
|
email: 'sunny@n8n.io',
|
||||||
globalRole: {
|
|
||||||
name: 'owner',
|
|
||||||
id: '1',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
firstName: 'Kobi',
|
firstName: 'Kobi',
|
||||||
lastName: 'Dog',
|
lastName: 'Dog',
|
||||||
email: 'kobi@n8n.io',
|
email: 'kobi@n8n.io',
|
||||||
globalRole: {
|
|
||||||
name: 'member',
|
|
||||||
id: '2',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
email: 'invited@n8n.io',
|
email: 'invited@n8n.io',
|
||||||
globalRole: {
|
|
||||||
name: 'member',
|
|
||||||
id: '2',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
placeholder: 'Select user to transfer to',
|
placeholder: 'Select user to transfer to',
|
||||||
|
|
|
@ -50,10 +50,8 @@ UsersList.args = {
|
||||||
isDefaultUser: false,
|
isDefaultUser: false,
|
||||||
isPendingUser: false,
|
isPendingUser: false,
|
||||||
isOwner: true,
|
isOwner: true,
|
||||||
globalRole: {
|
signInType: 'email',
|
||||||
name: 'owner',
|
disabled: false,
|
||||||
id: 1,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
|
@ -64,10 +62,8 @@ UsersList.args = {
|
||||||
isDefaultUser: false,
|
isDefaultUser: false,
|
||||||
isPendingUser: false,
|
isPendingUser: false,
|
||||||
isOwner: false,
|
isOwner: false,
|
||||||
globalRole: {
|
signInType: 'ldap',
|
||||||
name: 'member',
|
disabled: true,
|
||||||
id: '2',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
|
@ -75,10 +71,6 @@ UsersList.args = {
|
||||||
isDefaultUser: false,
|
isDefaultUser: false,
|
||||||
isPendingUser: true,
|
isPendingUser: true,
|
||||||
isOwner: false,
|
isOwner: false,
|
||||||
globalRole: {
|
|
||||||
name: 'member',
|
|
||||||
id: '2',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
currentUserId: '1',
|
currentUserId: '1',
|
||||||
|
|
|
@ -13,7 +13,13 @@
|
||||||
</n8n-badge>
|
</n8n-badge>
|
||||||
<slot v-if="!user.isOwner && !readonly" name="actions" :user="user" />
|
<slot v-if="!user.isOwner && !readonly" name="actions" :user="user" />
|
||||||
<n8n-action-toggle
|
<n8n-action-toggle
|
||||||
v-if="!user.isOwner && !readonly && getActions(user).length > 0 && actions.length > 0"
|
v-if="
|
||||||
|
!user.isOwner &&
|
||||||
|
user.signInType !== 'ldap' &&
|
||||||
|
!readonly &&
|
||||||
|
getActions(user).length > 0 &&
|
||||||
|
actions.length > 0
|
||||||
|
"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:actions="getActions(user)"
|
:actions="getActions(user)"
|
||||||
theme="dark"
|
theme="dark"
|
||||||
|
|
|
@ -24,7 +24,16 @@ export type IFormInput = {
|
||||||
initialValue?: string | number | boolean | null;
|
initialValue?: string | number | boolean | null;
|
||||||
properties: {
|
properties: {
|
||||||
label?: string;
|
label?: string;
|
||||||
type?: 'text' | 'email' | 'password' | 'select' | 'multi-select' | 'info' | 'checkbox';
|
type?:
|
||||||
|
| 'text'
|
||||||
|
| 'email'
|
||||||
|
| 'password'
|
||||||
|
| 'select'
|
||||||
|
| 'multi-select'
|
||||||
|
| 'number'
|
||||||
|
| 'info'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'toggle';
|
||||||
maxlength?: number;
|
maxlength?: number;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
showRequiredAsterisk?: boolean;
|
showRequiredAsterisk?: boolean;
|
||||||
|
@ -45,6 +54,9 @@ export type IFormInput = {
|
||||||
| 'email'; // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
|
| 'email'; // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
|
||||||
capitalize?: boolean;
|
capitalize?: boolean;
|
||||||
focusInitially?: boolean;
|
focusInitially?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
labelSize?: 'small' | 'medium' | 'large';
|
||||||
|
labelAlignment?: 'left' | 'right' | 'center';
|
||||||
};
|
};
|
||||||
shouldDisplay?: (values: { [key: string]: unknown }) => boolean;
|
shouldDisplay?: (values: { [key: string]: unknown }) => boolean;
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,8 @@ export interface IUser {
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
isPendingUser: boolean;
|
isPendingUser: boolean;
|
||||||
inviteAcceptUrl?: string;
|
inviteAcceptUrl?: string;
|
||||||
|
disabled: boolean;
|
||||||
|
signInType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserListAction {
|
export interface IUserListAction {
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
"fast-json-stable-stringify": "^2.1.0",
|
"fast-json-stable-stringify": "^2.1.0",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"flatted": "^3.2.4",
|
"flatted": "^3.2.4",
|
||||||
|
"humanize-duration": "^3.27.2",
|
||||||
"jquery": "^3.4.1",
|
"jquery": "^3.4.1",
|
||||||
"jsonpath": "^1.1.1",
|
"jsonpath": "^1.1.1",
|
||||||
"jsplumb": "2.15.4",
|
"jsplumb": "2.15.4",
|
||||||
|
@ -69,6 +70,7 @@
|
||||||
"vue-agile": "^2.0.0",
|
"vue-agile": "^2.0.0",
|
||||||
"vue-fragment": "1.5.1",
|
"vue-fragment": "1.5.1",
|
||||||
"vue-i18n": "^8.26.7",
|
"vue-i18n": "^8.26.7",
|
||||||
|
"vue-infinite-loading": "^2.4.5",
|
||||||
"vue-json-pretty": "1.9.3",
|
"vue-json-pretty": "1.9.3",
|
||||||
"vue-prism-editor": "^0.3.0",
|
"vue-prism-editor": "^0.3.0",
|
||||||
"vue-router": "^3.6.5",
|
"vue-router": "^3.6.5",
|
||||||
|
@ -86,6 +88,7 @@
|
||||||
"@types/dateformat": "^3.0.0",
|
"@types/dateformat": "^3.0.0",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
|
"@types/humanize-duration": "^3.27.1",
|
||||||
"@types/jsonpath": "^0.2.0",
|
"@types/jsonpath": "^0.2.0",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/lodash.camelcase": "^4.3.6",
|
"@types/lodash.camelcase": "^4.3.6",
|
||||||
|
|
|
@ -40,6 +40,7 @@ import {
|
||||||
IAbstractEventMessage,
|
IAbstractEventMessage,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { FAKE_DOOR_FEATURES } from './constants';
|
import { FAKE_DOOR_FEATURES } from './constants';
|
||||||
|
import { SignInType } from './constants';
|
||||||
import { BulkCommand, Undoable } from '@/models/history';
|
import { BulkCommand, Undoable } from '@/models/history';
|
||||||
|
|
||||||
export * from 'n8n-design-system/types';
|
export * from 'n8n-design-system/types';
|
||||||
|
@ -642,6 +643,7 @@ export interface IUserResponse {
|
||||||
};
|
};
|
||||||
personalizationAnswers?: IPersonalizationSurveyVersions | null;
|
personalizationAnswers?: IPersonalizationSurveyVersions | null;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
|
signInType?: SignInType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUser extends IUserResponse {
|
export interface IUser extends IUserResponse {
|
||||||
|
@ -808,6 +810,10 @@ export interface IN8nUISettings {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
ldap: {
|
||||||
|
loginLabel: string;
|
||||||
|
loginEnabled: boolean;
|
||||||
|
};
|
||||||
onboardingCallPromptEnabled: boolean;
|
onboardingCallPromptEnabled: boolean;
|
||||||
allowedModules: {
|
allowedModules: {
|
||||||
builtIn?: string[];
|
builtIn?: string[];
|
||||||
|
@ -1224,6 +1230,10 @@ export interface ISettingsState {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
ldap: {
|
||||||
|
loginLabel: string;
|
||||||
|
loginEnabled: boolean;
|
||||||
|
};
|
||||||
onboardingCallPromptEnabled: boolean;
|
onboardingCallPromptEnabled: boolean;
|
||||||
saveDataErrorExecution: string;
|
saveDataErrorExecution: string;
|
||||||
saveDataSuccessExecution: string;
|
saveDataSuccessExecution: string;
|
||||||
|
@ -1385,6 +1395,50 @@ export type SchemaType =
|
||||||
| 'function'
|
| 'function'
|
||||||
| 'null'
|
| 'null'
|
||||||
| 'undefined';
|
| 'undefined';
|
||||||
|
|
||||||
|
export interface ILdapSyncData {
|
||||||
|
id: number;
|
||||||
|
startedAt: string;
|
||||||
|
endedAt: string;
|
||||||
|
created: number;
|
||||||
|
updated: number;
|
||||||
|
disabled: number;
|
||||||
|
scanned: number;
|
||||||
|
status: string;
|
||||||
|
error: string;
|
||||||
|
runMode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILdapSyncTable {
|
||||||
|
status: string;
|
||||||
|
endedAt: string;
|
||||||
|
runTime: string;
|
||||||
|
runMode: string;
|
||||||
|
details: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILdapConfig {
|
||||||
|
loginEnabled: boolean;
|
||||||
|
loginLabel: string;
|
||||||
|
connectionUrl: string;
|
||||||
|
allowUnauthorizedCerts: boolean;
|
||||||
|
connectionSecurity: string;
|
||||||
|
connectionPort: number;
|
||||||
|
baseDn: string;
|
||||||
|
bindingAdminDn: string;
|
||||||
|
bindingAdminPassword: string;
|
||||||
|
firstNameAttribute: string;
|
||||||
|
lastNameAttribute: string;
|
||||||
|
emailAttribute: string;
|
||||||
|
loginIdAttribute: string;
|
||||||
|
ldapIdAttribute: string;
|
||||||
|
userFilter: string;
|
||||||
|
synchronizationEnabled: boolean;
|
||||||
|
synchronizationInterval: number; // minutes
|
||||||
|
searchPageSize: number;
|
||||||
|
searchTimeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type Schema = { type: SchemaType; key?: string; value: string | Schema[]; path: string };
|
export type Schema = { type: SchemaType; key?: string; value: string | Schema[]; path: string };
|
||||||
|
|
||||||
export type UsageState = {
|
export type UsageState = {
|
||||||
|
|
29
packages/editor-ui/src/api/ldap.ts
Normal file
29
packages/editor-ui/src/api/ldap.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { ILdapConfig, ILdapSyncData, IRestApiContext } from '@/Interface';
|
||||||
|
import { makeRestApiRequest } from '@/utils';
|
||||||
|
import { IDataObject } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export function getLdapConfig(context: IRestApiContext): Promise<ILdapConfig> {
|
||||||
|
return makeRestApiRequest(context, 'GET', '/ldap/config');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testLdapConnection(context: IRestApiContext): Promise<{}> {
|
||||||
|
return makeRestApiRequest(context, 'POST', '/ldap/test-connection');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateLdapConfig(
|
||||||
|
context: IRestApiContext,
|
||||||
|
adConfig: ILdapConfig,
|
||||||
|
): Promise<ILdapConfig> {
|
||||||
|
return makeRestApiRequest(context, 'PUT', '/ldap/config', adConfig as unknown as IDataObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runLdapSync(context: IRestApiContext, data: IDataObject): Promise<{}> {
|
||||||
|
return makeRestApiRequest(context, 'POST', '/ldap/sync', data as unknown as IDataObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLdapSynchronizations(
|
||||||
|
context: IRestApiContext,
|
||||||
|
pagination: { page: number },
|
||||||
|
): Promise<ILdapSyncData[]> {
|
||||||
|
return makeRestApiRequest(context, 'GET', '/ldap/sync', pagination);
|
||||||
|
}
|
|
@ -74,6 +74,14 @@ export default mixins(userHelpers, pushConnection).extend({
|
||||||
available: this.canAccessApiSettings(),
|
available: this.canAccessApiSettings(),
|
||||||
activateOnRouteNames: [VIEWS.API_SETTINGS],
|
activateOnRouteNames: [VIEWS.API_SETTINGS],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'settings-ldap',
|
||||||
|
icon: 'network-wired',
|
||||||
|
label: this.$locale.baseText('settings.ldap'),
|
||||||
|
position: 'top',
|
||||||
|
available: this.canAccessLdapSettings(),
|
||||||
|
activateOnRouteNames: [VIEWS.LDAP_SETTINGS],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const item of this.settingsFakeDoorFeatures) {
|
for (const item of this.settingsFakeDoorFeatures) {
|
||||||
|
@ -126,6 +134,9 @@ export default mixins(userHelpers, pushConnection).extend({
|
||||||
canAccessApiSettings(): boolean {
|
canAccessApiSettings(): boolean {
|
||||||
return this.canUserAccessRouteByName(VIEWS.API_SETTINGS);
|
return this.canUserAccessRouteByName(VIEWS.API_SETTINGS);
|
||||||
},
|
},
|
||||||
|
canAccessLdapSettings(): boolean {
|
||||||
|
return this.canUserAccessRouteByName(VIEWS.LDAP_SETTINGS);
|
||||||
|
},
|
||||||
canAccessLogStreamingSettings(): boolean {
|
canAccessLogStreamingSettings(): boolean {
|
||||||
return this.canUserAccessRouteByName(VIEWS.LOG_STREAMING_SETTINGS);
|
return this.canUserAccessRouteByName(VIEWS.LOG_STREAMING_SETTINGS);
|
||||||
},
|
},
|
||||||
|
@ -155,6 +166,11 @@ export default mixins(userHelpers, pushConnection).extend({
|
||||||
this.$router.push({ name: VIEWS.API_SETTINGS });
|
this.$router.push({ name: VIEWS.API_SETTINGS });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'settings-ldap':
|
||||||
|
if (this.$router.currentRoute.name !== VIEWS.LDAP_SETTINGS) {
|
||||||
|
this.$router.push({ name: VIEWS.LDAP_SETTINGS });
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'settings-log-streaming':
|
case 'settings-log-streaming':
|
||||||
if (this.$router.currentRoute.name !== VIEWS.LOG_STREAMING_SETTINGS) {
|
if (this.$router.currentRoute.name !== VIEWS.LOG_STREAMING_SETTINGS) {
|
||||||
this.$router.push({ name: VIEWS.LOG_STREAMING_SETTINGS });
|
this.$router.push({ name: VIEWS.LOG_STREAMING_SETTINGS });
|
||||||
|
|
|
@ -312,6 +312,7 @@ export enum VIEWS {
|
||||||
FORGOT_PASSWORD = 'ForgotMyPasswordView',
|
FORGOT_PASSWORD = 'ForgotMyPasswordView',
|
||||||
CHANGE_PASSWORD = 'ChangePasswordView',
|
CHANGE_PASSWORD = 'ChangePasswordView',
|
||||||
USERS_SETTINGS = 'UsersSettings',
|
USERS_SETTINGS = 'UsersSettings',
|
||||||
|
LDAP_SETTINGS = 'LdapSettings',
|
||||||
PERSONAL_SETTINGS = 'PersonalSettings',
|
PERSONAL_SETTINGS = 'PersonalSettings',
|
||||||
API_SETTINGS = 'APISettings',
|
API_SETTINGS = 'APISettings',
|
||||||
NOT_FOUND = 'NotFoundView',
|
NOT_FOUND = 'NotFoundView',
|
||||||
|
@ -381,6 +382,7 @@ export enum WORKFLOW_MENU_ACTIONS {
|
||||||
*/
|
*/
|
||||||
export enum EnterpriseEditionFeature {
|
export enum EnterpriseEditionFeature {
|
||||||
Sharing = 'sharing',
|
Sharing = 'sharing',
|
||||||
|
Ldap = 'ldap',
|
||||||
LogStreaming = 'logStreaming',
|
LogStreaming = 'logStreaming',
|
||||||
}
|
}
|
||||||
export const MAIN_NODE_PANEL_WIDTH = 360;
|
export const MAIN_NODE_PANEL_WIDTH = 360;
|
||||||
|
@ -442,6 +444,15 @@ export enum STORES {
|
||||||
HISTORY = 'history',
|
HISTORY = 'history',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SignInType {
|
||||||
|
LDAP = 'ldap',
|
||||||
|
EMAIL = 'email',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const N8N_SALES_EMAIL = 'sales@n8n.io';
|
||||||
|
|
||||||
|
export const N8N_CONTACT_EMAIL = 'contact@n8n.io';
|
||||||
|
|
||||||
export const EXPRESSION_EDITOR_PARSER_TIMEOUT = 15_000; // ms
|
export const EXPRESSION_EDITOR_PARSER_TIMEOUT = 15_000; // ms
|
||||||
|
|
||||||
export const POSTHOG_ASSUMPTION_TEST = 'adore-assumption-tests';
|
export const POSTHOG_ASSUMPTION_TEST = 'adore-assumption-tests';
|
||||||
|
|
|
@ -527,6 +527,7 @@
|
||||||
"forgotPassword.recoveryEmailSent": "Recovery email sent",
|
"forgotPassword.recoveryEmailSent": "Recovery email sent",
|
||||||
"forgotPassword.returnToSignIn": "Back to sign in",
|
"forgotPassword.returnToSignIn": "Back to sign in",
|
||||||
"forgotPassword.sendingEmailError": "Problem sending email",
|
"forgotPassword.sendingEmailError": "Problem sending email",
|
||||||
|
"forgotPassword.ldapUserPasswordResetUnavailable": "Please contact your LDAP administrator to reset your password",
|
||||||
"forgotPassword.smtpErrorContactAdministrator": "Please contact your administrator (problem with your SMTP setup)",
|
"forgotPassword.smtpErrorContactAdministrator": "Please contact your administrator (problem with your SMTP setup)",
|
||||||
"forms.resourceFiltersDropdown.filters": "Filters",
|
"forms.resourceFiltersDropdown.filters": "Filters",
|
||||||
"forms.resourceFiltersDropdown.ownedBy": "Owned by",
|
"forms.resourceFiltersDropdown.ownedBy": "Owned by",
|
||||||
|
@ -1521,7 +1522,6 @@
|
||||||
"importParameter.showError.invalidProtocol1.title": "Use the {node} node",
|
"importParameter.showError.invalidProtocol1.title": "Use the {node} node",
|
||||||
"importParameter.showError.invalidProtocol2.title": "Invalid Protocol",
|
"importParameter.showError.invalidProtocol2.title": "Invalid Protocol",
|
||||||
"importParameter.showError.invalidProtocol.message": "The HTTP node doesn’t support {protocol} requests",
|
"importParameter.showError.invalidProtocol.message": "The HTTP node doesn’t support {protocol} requests",
|
||||||
|
|
||||||
"contextual.credentials.sharing.unavailable.title": "Upgrade to collaborate",
|
"contextual.credentials.sharing.unavailable.title": "Upgrade to collaborate",
|
||||||
"contextual.credentials.sharing.unavailable.title.cloud": "Upgrade to collaborate",
|
"contextual.credentials.sharing.unavailable.title.cloud": "Upgrade to collaborate",
|
||||||
"contextual.credentials.sharing.unavailable.title.desktop": "Upgrade to n8n Cloud to collaborate",
|
"contextual.credentials.sharing.unavailable.title.desktop": "Upgrade to n8n Cloud to collaborate",
|
||||||
|
@ -1547,5 +1547,86 @@
|
||||||
"contextual.workflows.sharing.unavailable.button.desktop": "View plans",
|
"contextual.workflows.sharing.unavailable.button.desktop": "View plans",
|
||||||
"contextual.upgradeLinkUrl": "https://subscription.n8n.io/",
|
"contextual.upgradeLinkUrl": "https://subscription.n8n.io/",
|
||||||
"contextual.upgradeLinkUrl.cloud": "https://app.n8n.cloud/manage?edition=cloud",
|
"contextual.upgradeLinkUrl.cloud": "https://app.n8n.cloud/manage?edition=cloud",
|
||||||
"contextual.upgradeLinkUrl.desktop": "https://n8n.io/pricing"
|
"contextual.upgradeLinkUrl.desktop": "https://n8n.io/pricing",
|
||||||
|
"settings.ldap": "LDAP",
|
||||||
|
"settings.ldap.infoTip": "Learn more about <a href='https://docs.n8n.io/user-management/ldap/' target='_blank'>LDAP in the Docs</a>",
|
||||||
|
"settings.ldap.save": "Save connection",
|
||||||
|
"settings.ldap.connectionTestError": "Problem testing LDAP connection",
|
||||||
|
"settings.ldap.connectionTest": "LDAP connection tested",
|
||||||
|
"settings.ldap.runSync.title": "LDAP synchronization done",
|
||||||
|
"settings.ldap.runSync.showError.message": "Problem during synchronization. Check the logs",
|
||||||
|
"settings.ldap.updateConfiguration": "LDAP configuration updated",
|
||||||
|
"settings.ldap.testingConnection": "Testing connection",
|
||||||
|
"settings.ldap.testConnection": "Test connection",
|
||||||
|
"settings.ldap.synchronizationTable.column.status": "Status",
|
||||||
|
"settings.ldap.synchronizationTable.column.endedAt": "Ended At",
|
||||||
|
"settings.ldap.synchronizationTable.column.runMode": "Run Mode",
|
||||||
|
"settings.ldap.synchronizationTable.column.runTime": "Run Time",
|
||||||
|
"settings.ldap.synchronizationTable.column.details": "Details",
|
||||||
|
"settings.ldap.synchronizationTable.empty.message": "Test synchronization to preview updates",
|
||||||
|
"settings.ldap.dryRun": "Test synchronization",
|
||||||
|
"settings.ldap.synchronizeNow": "Run synchronization",
|
||||||
|
"settings.ldap.synchronizationError": "LDAP Synchronization Error",
|
||||||
|
"settings.ldap.configurationError": "LDAP Configuration Error",
|
||||||
|
"settings.ldap.usersScanned": "Users scanned {scanned}",
|
||||||
|
"settings.ldap.confirmMessage.beforeSaveForm.cancelButtonText": "Yes, disable it",
|
||||||
|
"settings.ldap.confirmMessage.beforeSaveForm.confirmButtonText": "No",
|
||||||
|
"settings.ldap.confirmMessage.beforeSaveForm.headline": "Are you sure you want to disable LDAP login?",
|
||||||
|
"settings.ldap.confirmMessage.beforeSaveForm.message": "If you do so, all LDAP users will be converted to email users.",
|
||||||
|
"settings.ldap.disabled.title": "Available in custom plans",
|
||||||
|
"settings.ldap.disabled.description": "LDAP is available as a paid feature. Get in touch to learn more about it.",
|
||||||
|
"settings.ldap.disabled.buttonText": "Contact us",
|
||||||
|
"settings.ldap.toast.sync.success": "Synchronization succeeded",
|
||||||
|
"settings.ldap.toast.connection.success": "Connection succeeded",
|
||||||
|
"settings.ldap.form.loginEnabled.label": "Enable LDAP Login",
|
||||||
|
"settings.ldap.form.loginEnabled.tooltip": "Connection settings and data will still be saved if you disable LDAP Login",
|
||||||
|
"settings.ldap.form.loginLabel.label": "LDAP Login",
|
||||||
|
"settings.ldap.form.loginLabel.placeholder": "e.g. LDAP Username or email address",
|
||||||
|
"settings.ldap.form.loginLabel.infoText": "The placeholder text that appears in the login field on the login page",
|
||||||
|
"settings.ldap.form.serverAddress.label": "LDAP Server Address",
|
||||||
|
"settings.ldap.form.serverAddress.placeholder": "123.123.123.123",
|
||||||
|
"settings.ldap.form.serverAddress.infoText": "IP or domain of the LDAP server",
|
||||||
|
"settings.ldap.form.port.label": "LDAP Server Port",
|
||||||
|
"settings.ldap.form.port.infoText": "Port used to connect to the LDAP server",
|
||||||
|
"settings.ldap.form.connectionSecurity.label": "Connection Security",
|
||||||
|
"settings.ldap.form.connectionSecurity.infoText": "Type of connection security",
|
||||||
|
"settings.ldap.form.allowUnauthorizedCerts.label": "Ignore SSL/TLS Issues",
|
||||||
|
"settings.ldap.form.baseDn.label": "Base DN",
|
||||||
|
"settings.ldap.form.baseDn.placeholder": "o=acme,dc=example,dc=com",
|
||||||
|
"settings.ldap.form.baseDn.infoText": "Distinguished Name of the location where n8n should start its search for user in the AD/LDAP tree",
|
||||||
|
"settings.ldap.form.bindingType.label": "Binding as",
|
||||||
|
"settings.ldap.form.bindingType.infoText": "Type of binding used to connection to the LDAP server",
|
||||||
|
"settings.ldap.form.adminDn.label": "Binding DN",
|
||||||
|
"settings.ldap.form.adminDn.placeholder": "uid=2da2de69435c,ou=Users,o=Acme,dc=com",
|
||||||
|
"settings.ldap.form.adminDn.infoText": "Distinguished Name of the user to perform the search",
|
||||||
|
"settings.ldap.form.adminPassword.label": "Binding Password",
|
||||||
|
"settings.ldap.form.adminPassword.infoText": "Password of the user provided in the Binding DN field above",
|
||||||
|
"settings.ldap.form.userFilter.label": "User Filter",
|
||||||
|
"settings.ldap.form.userFilter.placeholder": "(ObjectClass=user)",
|
||||||
|
"settings.ldap.form.userFilter.infoText": "LDAP query to use when searching for user. Only users returned by this filter will be allowed to sign-in in n8n",
|
||||||
|
"settings.ldap.form.attributeMappingInfo.label": "Attribute mapping",
|
||||||
|
"settings.ldap.form.ldapId.label": "ID",
|
||||||
|
"settings.ldap.form.ldapId.placeholder": "uid",
|
||||||
|
"settings.ldap.form.ldapId.infoText": "The attribute in the LDAP server used as a unique identifier in n8n. It should be an unique LDAP attribute like uid",
|
||||||
|
"settings.ldap.form.loginId.label": "Login ID",
|
||||||
|
"settings.ldap.form.loginId.placeholder": "mail",
|
||||||
|
"settings.ldap.form.loginId.infoText": "The attribute in the LDAP server used to log-in in n8n",
|
||||||
|
"settings.ldap.form.email.label": "Email",
|
||||||
|
"settings.ldap.form.email.placeholder": "mail",
|
||||||
|
"settings.ldap.form.email.infoText": "The attribute in the LDAP server used to populate the email in n8n",
|
||||||
|
"settings.ldap.form.firstName.label": "First Name",
|
||||||
|
"settings.ldap.form.firstName.placeholder": "givenName",
|
||||||
|
"settings.ldap.form.firstName.infoText": "The attribute in the LDAP server used to populate the first name in n8n",
|
||||||
|
"settings.ldap.form.lastName.label": "Last Name",
|
||||||
|
"settings.ldap.form.lastName.placeholder": "sn",
|
||||||
|
"settings.ldap.form.lastName.infoText": "The attribute in the LDAP server used to populate the last name in n8n",
|
||||||
|
"settings.ldap.form.synchronizationEnabled.label": "Enable periodic LDAP synchronization",
|
||||||
|
"settings.ldap.form.synchronizationEnabled.tooltip": "Enable users to be synchronized periodically",
|
||||||
|
"settings.ldap.form.synchronizationInterval.label": "Synchronization Interval (Minutes)",
|
||||||
|
"settings.ldap.form.synchronizationInterval.infoText": "How often the synchronization should run",
|
||||||
|
"settings.ldap.form.pageSize.label": "Page Size",
|
||||||
|
"settings.ldap.form.pageSize.infoText": "Max number of records to return per page during synchronization. 0 for unlimited",
|
||||||
|
"settings.ldap.form.searchTimeout.label": "Search Timeout (Seconds)",
|
||||||
|
"settings.ldap.form.searchTimeout.infoText": "The timeout value for queries to the AD/LDAP server. Increase if you are getting timeout errors caused by a slow AD/LDAP server",
|
||||||
|
"settings.ldap.section.synchronization.title": "Synchronization"
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,7 @@ import {
|
||||||
faUserFriends,
|
faUserFriends,
|
||||||
faUsers,
|
faUsers,
|
||||||
faVideo,
|
faVideo,
|
||||||
|
faTree,
|
||||||
faStickyNote as faSolidStickyNote,
|
faStickyNote as faSolidStickyNote,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
@ -250,5 +251,6 @@ addIcon(faUserCircle);
|
||||||
addIcon(faUserFriends);
|
addIcon(faUserFriends);
|
||||||
addIcon(faUsers);
|
addIcon(faUsers);
|
||||||
addIcon(faVideo);
|
addIcon(faVideo);
|
||||||
|
addIcon(faTree);
|
||||||
|
|
||||||
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import WorkflowExecutionsList from '@/components/ExecutionsView/ExecutionsList.v
|
||||||
import ExecutionsLandingPage from '@/components/ExecutionsView/ExecutionsLandingPage.vue';
|
import ExecutionsLandingPage from '@/components/ExecutionsView/ExecutionsLandingPage.vue';
|
||||||
import ExecutionPreview from '@/components/ExecutionsView/ExecutionPreview.vue';
|
import ExecutionPreview from '@/components/ExecutionsView/ExecutionPreview.vue';
|
||||||
import SettingsView from './views/SettingsView.vue';
|
import SettingsView from './views/SettingsView.vue';
|
||||||
|
import SettingsLdapView from './views/SettingsLdapView.vue';
|
||||||
import SettingsPersonalView from './views/SettingsPersonalView.vue';
|
import SettingsPersonalView from './views/SettingsPersonalView.vue';
|
||||||
import SettingsUsersView from './views/SettingsUsersView.vue';
|
import SettingsUsersView from './views/SettingsUsersView.vue';
|
||||||
import SettingsCommunityNodesView from './views/SettingsCommunityNodesView.vue';
|
import SettingsCommunityNodesView from './views/SettingsCommunityNodesView.vue';
|
||||||
|
@ -30,7 +31,7 @@ import WorkflowsView from '@/views/WorkflowsView.vue';
|
||||||
import { IPermissions } from './Interface';
|
import { IPermissions } from './Interface';
|
||||||
import { LOGIN_STATUS, ROLE } from '@/utils';
|
import { LOGIN_STATUS, ROLE } from '@/utils';
|
||||||
import { RouteConfigSingleView } from 'vue-router/types/router';
|
import { RouteConfigSingleView } from 'vue-router/types/router';
|
||||||
import { VIEWS } from './constants';
|
import { EnterpriseEditionFeature, VIEWS } from './constants';
|
||||||
import { useSettingsStore } from './stores/settings';
|
import { useSettingsStore } from './stores/settings';
|
||||||
import { useTemplatesStore } from './stores/templates';
|
import { useTemplatesStore } from './stores/templates';
|
||||||
import SettingsUsageAndPlanVue from './views/SettingsUsageAndPlan.vue';
|
import SettingsUsageAndPlanVue from './views/SettingsUsageAndPlan.vue';
|
||||||
|
@ -606,6 +607,20 @@ const router = new Router({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'ldap',
|
||||||
|
name: VIEWS.LDAP_SETTINGS,
|
||||||
|
components: {
|
||||||
|
settingsView: SettingsLdapView,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
permissions: {
|
||||||
|
allow: {
|
||||||
|
role: [ROLE.Owner],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { createApiKey, deleteApiKey, getApiKey } from '@/api/api-keys';
|
import { createApiKey, deleteApiKey, getApiKey } from '@/api/api-keys';
|
||||||
|
import {
|
||||||
|
getLdapConfig,
|
||||||
|
getLdapSynchronizations,
|
||||||
|
runLdapSync,
|
||||||
|
testLdapConnection,
|
||||||
|
updateLdapConfig,
|
||||||
|
} from '@/api/ldap';
|
||||||
import { getPromptsData, getSettings, submitContactInfo, submitValueSurvey } from '@/api/settings';
|
import { getPromptsData, getSettings, submitContactInfo, submitValueSurvey } from '@/api/settings';
|
||||||
import { testHealthEndpoint } from '@/api/templates';
|
import { testHealthEndpoint } from '@/api/templates';
|
||||||
import {
|
import {
|
||||||
|
@ -15,6 +22,7 @@ import {
|
||||||
IN8nValueSurveyData,
|
IN8nValueSurveyData,
|
||||||
ISettingsState,
|
ISettingsState,
|
||||||
WorkflowCallerPolicyDefaultOption,
|
WorkflowCallerPolicyDefaultOption,
|
||||||
|
ILdapConfig,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { ITelemetrySettings } from 'n8n-workflow';
|
import { ITelemetrySettings } from 'n8n-workflow';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
@ -42,6 +50,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ldap: {
|
||||||
|
loginLabel: '',
|
||||||
|
loginEnabled: false,
|
||||||
|
},
|
||||||
onboardingCallPromptEnabled: false,
|
onboardingCallPromptEnabled: false,
|
||||||
saveDataErrorExecution: 'all',
|
saveDataErrorExecution: 'all',
|
||||||
saveDataSuccessExecution: 'all',
|
saveDataSuccessExecution: 'all',
|
||||||
|
@ -69,6 +81,12 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||||
publicApiPath(): string {
|
publicApiPath(): string {
|
||||||
return this.api.path;
|
return this.api.path;
|
||||||
},
|
},
|
||||||
|
isLdapLoginEnabled(): boolean {
|
||||||
|
return this.ldap.loginEnabled;
|
||||||
|
},
|
||||||
|
ldapLoginLabel(): string {
|
||||||
|
return this.ldap.loginLabel;
|
||||||
|
},
|
||||||
showSetupPage(): boolean {
|
showSetupPage(): boolean {
|
||||||
return this.userManagement.showSetupOnFirstLoad === true;
|
return this.userManagement.showSetupOnFirstLoad === true;
|
||||||
},
|
},
|
||||||
|
@ -147,6 +165,8 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||||
this.userManagement.smtpSetup = settings.userManagement.smtpSetup;
|
this.userManagement.smtpSetup = settings.userManagement.smtpSetup;
|
||||||
this.api = settings.publicApi;
|
this.api = settings.publicApi;
|
||||||
this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled;
|
this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled;
|
||||||
|
this.ldap.loginEnabled = settings.ldap.loginEnabled;
|
||||||
|
this.ldap.loginLabel = settings.ldap.loginLabel;
|
||||||
},
|
},
|
||||||
async getSettings(): Promise<void> {
|
async getSettings(): Promise<void> {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
|
@ -253,6 +273,26 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
await deleteApiKey(rootStore.getRestApiContext);
|
await deleteApiKey(rootStore.getRestApiContext);
|
||||||
},
|
},
|
||||||
|
async getLdapConfig() {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
return await getLdapConfig(rootStore.getRestApiContext);
|
||||||
|
},
|
||||||
|
async getLdapSynchronizations(pagination: { page: number }) {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
return await getLdapSynchronizations(rootStore.getRestApiContext, pagination);
|
||||||
|
},
|
||||||
|
async testLdapConnection() {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
return await testLdapConnection(rootStore.getRestApiContext);
|
||||||
|
},
|
||||||
|
async updateLdapConfig(ldapConfig: ILdapConfig) {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
return await updateLdapConfig(rootStore.getRestApiContext, ldapConfig);
|
||||||
|
},
|
||||||
|
async runLdapSync(data: IDataObject) {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
return await runLdapSync(rootStore.getRestApiContext, data);
|
||||||
|
},
|
||||||
setSaveDataErrorExecution(newValue: string) {
|
setSaveDataErrorExecution(newValue: string) {
|
||||||
Vue.set(this, 'saveDataErrorExecution', newValue);
|
Vue.set(this, 'saveDataErrorExecution', newValue);
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { useUIStore } from './ui';
|
||||||
|
|
||||||
const isDefaultUser = (user: IUserResponse | null) =>
|
const isDefaultUser = (user: IUserResponse | null) =>
|
||||||
Boolean(user && user.isPending && user.globalRole && user.globalRole.name === ROLE.Owner);
|
Boolean(user && user.isPending && user.globalRole && user.globalRole.name === ROLE.Owner);
|
||||||
|
|
||||||
const isPendingUser = (user: IUserResponse | null) => Boolean(user && user.isPending);
|
const isPendingUser = (user: IUserResponse | null) => Boolean(user && user.isPending);
|
||||||
|
|
||||||
export const useUsersStore = defineStore(STORES.USERS, {
|
export const useUsersStore = defineStore(STORES.USERS, {
|
||||||
|
@ -58,8 +59,8 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
||||||
getUserById(state) {
|
getUserById(state) {
|
||||||
return (userId: string): IUser | null => state.users[userId];
|
return (userId: string): IUser | null => state.users[userId];
|
||||||
},
|
},
|
||||||
globalRoleName(): string {
|
globalRoleName(): IRole {
|
||||||
return this.currentUser?.globalRole?.name || '';
|
return this.currentUser?.globalRole?.name ?? 'default';
|
||||||
},
|
},
|
||||||
canUserDeleteTags(): boolean {
|
canUserDeleteTags(): boolean {
|
||||||
return isAuthorized(PERMISSIONS.TAGS.CAN_DELETE_TAGS, this.currentUser);
|
return isAuthorized(PERMISSIONS.TAGS.CAN_DELETE_TAGS, this.currentUser);
|
||||||
|
@ -116,7 +117,7 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
||||||
: undefined,
|
: undefined,
|
||||||
isDefaultUser: isDefaultUser(updatedUser),
|
isDefaultUser: isDefaultUser(updatedUser),
|
||||||
isPendingUser: isPendingUser(updatedUser),
|
isPendingUser: isPendingUser(updatedUser),
|
||||||
isOwner: Boolean(updatedUser.globalRole && updatedUser.globalRole.name === ROLE.Owner),
|
isOwner: updatedUser.globalRole?.name === ROLE.Owner,
|
||||||
};
|
};
|
||||||
Vue.set(this.users, user.id, user);
|
Vue.set(this.users, user.id, user);
|
||||||
});
|
});
|
||||||
|
|
|
@ -59,3 +59,7 @@ export function isChildOf(parent: Element, child: Element): boolean {
|
||||||
|
|
||||||
return isChildOf(parent, child.parentElement);
|
return isChildOf(parent, child.parentElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const capitalizeFirstLetter = (text: string): string => {
|
||||||
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
|
};
|
||||||
|
|
|
@ -142,7 +142,7 @@ export const isAuthorized = (permissions: IPermissions, currentUser: IUser | nul
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentUser && currentUser.globalRole) {
|
if (currentUser?.globalRole?.name) {
|
||||||
const role = currentUser.isDefaultUser ? ROLE.Default : currentUser.globalRole.name;
|
const role = currentUser.isDefaultUser ? ROLE.Default : currentUser.globalRole.name;
|
||||||
if (permissions.deny.role && permissions.deny.role.includes(role)) {
|
if (permissions.deny.role && permissions.deny.role.includes(role)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -163,7 +163,7 @@ export const isAuthorized = (permissions: IPermissions, currentUser: IUser | nul
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentUser && currentUser.globalRole) {
|
if (currentUser?.globalRole?.name) {
|
||||||
const role = currentUser.isDefaultUser ? ROLE.Default : currentUser.globalRole.name;
|
const role = currentUser.isDefaultUser ? ROLE.Default : currentUser.globalRole.name;
|
||||||
if (permissions.allow.role && permissions.allow.role.includes(role)) {
|
if (permissions.allow.role && permissions.allow.role.includes(role)) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -82,10 +82,14 @@ export default mixins(showMessage).extend({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
let message = this.$locale.baseText('forgotPassword.smtpErrorContactAdministrator');
|
||||||
|
if (error.httpStatusCode === 422) {
|
||||||
|
message = this.$locale.baseText(error.message);
|
||||||
|
}
|
||||||
this.$showMessage({
|
this.$showMessage({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: this.$locale.baseText('forgotPassword.sendingEmailError'),
|
title: this.$locale.baseText('forgotPassword.sendingEmailError'),
|
||||||
message: this.$locale.baseText('forgotPassword.smtpErrorContactAdministrator'),
|
message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
786
packages/editor-ui/src/views/SettingsLdapView.vue
Normal file
786
packages/editor-ui/src/views/SettingsLdapView.vue
Normal file
|
@ -0,0 +1,786 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="!isLDAPFeatureEnabled">
|
||||||
|
<div :class="[$style.header, 'mb-2xl']">
|
||||||
|
<n8n-heading size="2xlarge">
|
||||||
|
{{ $locale.baseText('settings.ldap') }}
|
||||||
|
</n8n-heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n8n-info-tip type="note" theme="info-light" tooltipPlacement="right">
|
||||||
|
<div>
|
||||||
|
LDAP allows users to authenticate with their centralized account. It's compatible with
|
||||||
|
services that provide an LDAP interface like Active Directory, Okta and Jumpcloud.
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
</n8n-info-tip>
|
||||||
|
<n8n-action-box
|
||||||
|
:description="$locale.baseText('settings.ldap.disabled.description')"
|
||||||
|
:buttonText="$locale.baseText('settings.ldap.disabled.buttonText')"
|
||||||
|
@click="onContactUsClick"
|
||||||
|
>
|
||||||
|
<template #heading>
|
||||||
|
<span>{{ $locale.baseText('settings.ldap.disabled.title') }}</span>
|
||||||
|
</template>
|
||||||
|
</n8n-action-box>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div :class="$style.container">
|
||||||
|
<div :class="$style.header">
|
||||||
|
<n8n-heading size="2xlarge">
|
||||||
|
{{ $locale.baseText('settings.ldap') }}
|
||||||
|
</n8n-heading>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.docsInfoTip">
|
||||||
|
<n8n-info-tip theme="info" type="note">
|
||||||
|
<template>
|
||||||
|
<span v-html="$locale.baseText('settings.ldap.infoTip')"></span>
|
||||||
|
</template>
|
||||||
|
</n8n-info-tip>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.settingsForm">
|
||||||
|
<n8n-form-inputs
|
||||||
|
v-if="formInputs"
|
||||||
|
ref="ldapConfigForm"
|
||||||
|
:inputs="formInputs"
|
||||||
|
:eventBus="formBus"
|
||||||
|
:columnView="true"
|
||||||
|
verticalSpacing="l"
|
||||||
|
@input="onInput"
|
||||||
|
@ready="onReadyToSubmit"
|
||||||
|
@submit="onSubmit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<n8n-button
|
||||||
|
v-if="loginEnabled"
|
||||||
|
:label="
|
||||||
|
loadingTestConnection
|
||||||
|
? $locale.baseText('settings.ldap.testingConnection')
|
||||||
|
: $locale.baseText('settings.ldap.testConnection')
|
||||||
|
"
|
||||||
|
size="large"
|
||||||
|
class="mr-s"
|
||||||
|
:disabled="hasAnyChanges || !readyToSubmit"
|
||||||
|
:loading="loadingTestConnection"
|
||||||
|
@click="onTestConnectionClick"
|
||||||
|
/>
|
||||||
|
<n8n-button
|
||||||
|
:label="$locale.baseText('settings.ldap.save')"
|
||||||
|
size="large"
|
||||||
|
:disabled="!hasAnyChanges || !readyToSubmit"
|
||||||
|
@click="onSaveClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="loginEnabled">
|
||||||
|
<n8n-heading tag="h1" class="mb-xl mt-3xl" size="medium">{{
|
||||||
|
$locale.baseText('settings.ldap.section.synchronization.title')
|
||||||
|
}}</n8n-heading>
|
||||||
|
<div :class="$style.syncTable">
|
||||||
|
<el-table
|
||||||
|
v-loading="loadingTable"
|
||||||
|
:border="true"
|
||||||
|
:stripe="true"
|
||||||
|
:data="dataTable"
|
||||||
|
:cell-style="cellClassStyle"
|
||||||
|
style="width: 100%"
|
||||||
|
height="250"
|
||||||
|
:key="tableKey"
|
||||||
|
>
|
||||||
|
<el-table-column
|
||||||
|
prop="status"
|
||||||
|
:label="$locale.baseText('settings.ldap.synchronizationTable.column.status')"
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="endedAt"
|
||||||
|
:label="$locale.baseText('settings.ldap.synchronizationTable.column.endedAt')"
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="runMode"
|
||||||
|
:label="$locale.baseText('settings.ldap.synchronizationTable.column.runMode')"
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="runTime"
|
||||||
|
:label="$locale.baseText('settings.ldap.synchronizationTable.column.runTime')"
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="details"
|
||||||
|
:label="$locale.baseText('settings.ldap.synchronizationTable.column.details')"
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty>{{
|
||||||
|
$locale.baseText('settings.ldap.synchronizationTable.empty.message')
|
||||||
|
}}</template>
|
||||||
|
<template #append>
|
||||||
|
<infinite-loading
|
||||||
|
@infinite="getLdapSynchronizations"
|
||||||
|
force-use-infinite-wrapper=".el-table__body-wrapper"
|
||||||
|
>
|
||||||
|
</infinite-loading>
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="pb-3xl">
|
||||||
|
<n8n-button
|
||||||
|
:label="$locale.baseText('settings.ldap.dryRun')"
|
||||||
|
type="secondary"
|
||||||
|
size="large"
|
||||||
|
class="mr-s"
|
||||||
|
:disabled="hasAnyChanges || !readyToSubmit"
|
||||||
|
:loading="loadingDryRun"
|
||||||
|
@click="onDryRunClick"
|
||||||
|
/>
|
||||||
|
<n8n-button
|
||||||
|
:label="$locale.baseText('settings.ldap.synchronizeNow')"
|
||||||
|
size="large"
|
||||||
|
:disabled="hasAnyChanges || !readyToSubmit"
|
||||||
|
:loading="loadingLiveRun"
|
||||||
|
@click="onLiveRunClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { convertToDisplayDate } from '@/utils';
|
||||||
|
import { showMessage } from '@/mixins/showMessage';
|
||||||
|
import {
|
||||||
|
ILdapConfig,
|
||||||
|
ILdapSyncData,
|
||||||
|
ILdapSyncTable,
|
||||||
|
IFormInput,
|
||||||
|
IFormInputs,
|
||||||
|
IUser,
|
||||||
|
} from '@/Interface';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
|
import humanizeDuration from 'humanize-duration';
|
||||||
|
import { rowCallbackParams, cellCallbackParams, ElTable } from 'element-ui/types/table';
|
||||||
|
import { capitalizeFirstLetter } from '@/utils';
|
||||||
|
import InfiniteLoading from 'vue-infinite-loading';
|
||||||
|
import { mapStores } from 'pinia';
|
||||||
|
import { useUsersStore } from '@/stores/users';
|
||||||
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
|
import { getLdapSynchronizations } from '@/api/ldap';
|
||||||
|
import { N8N_CONTACT_EMAIL, N8N_SALES_EMAIL } from '@/constants';
|
||||||
|
import { ElTableColumn } from 'element-ui/types/table-column';
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
loginEnabled: boolean;
|
||||||
|
loginLabel: string;
|
||||||
|
serverAddress: string;
|
||||||
|
baseDn: string;
|
||||||
|
bindingType: string;
|
||||||
|
adminDn: string;
|
||||||
|
adminPassword: string;
|
||||||
|
loginId: string;
|
||||||
|
email: string;
|
||||||
|
lastName: string;
|
||||||
|
firstName: string;
|
||||||
|
ldapId: string;
|
||||||
|
synchronizationEnabled: boolean;
|
||||||
|
allowUnauthorizedCerts: boolean;
|
||||||
|
synchronizationInterval: number;
|
||||||
|
userFilter: string;
|
||||||
|
pageSize: number;
|
||||||
|
searchTimeout: number;
|
||||||
|
port: number;
|
||||||
|
connectionSecurity: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type tableRow = {
|
||||||
|
status: string;
|
||||||
|
startAt: string;
|
||||||
|
endedAt: string;
|
||||||
|
error: string;
|
||||||
|
runMode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type rowType = rowCallbackParams & tableRow;
|
||||||
|
|
||||||
|
type cellType = cellCallbackParams & { property: keyof tableRow };
|
||||||
|
|
||||||
|
export default mixins(showMessage).extend({
|
||||||
|
name: 'SettingsLdapView',
|
||||||
|
components: {
|
||||||
|
InfiniteLoading,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dataTable: [] as ILdapSyncTable[],
|
||||||
|
tableKey: 0,
|
||||||
|
adConfig: {} as ILdapConfig,
|
||||||
|
loadingTestConnection: false,
|
||||||
|
loadingDryRun: false,
|
||||||
|
loadingLiveRun: false,
|
||||||
|
loadingTable: false,
|
||||||
|
hasAnyChanges: false,
|
||||||
|
formInputs: null as null | IFormInputs,
|
||||||
|
formBus: new Vue(),
|
||||||
|
readyToSubmit: false,
|
||||||
|
page: 0,
|
||||||
|
loginEnabled: false,
|
||||||
|
syncEnabled: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
if (!this.isLDAPFeatureEnabled) return;
|
||||||
|
await this.getLdapConfig();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useUsersStore, useSettingsStore),
|
||||||
|
currentUser(): null | IUser {
|
||||||
|
return this.usersStore.currentUser;
|
||||||
|
},
|
||||||
|
isLDAPFeatureEnabled(): boolean {
|
||||||
|
return this.settingsStore.settings.enterprise.ldap === true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onContactUsClick(event: MouseEvent): void {
|
||||||
|
const email = this.settingsStore.isCloudDeployment ? N8N_CONTACT_EMAIL : N8N_SALES_EMAIL;
|
||||||
|
location.href = `mailto:${email}`;
|
||||||
|
},
|
||||||
|
cellClassStyle({ row, column }: { row: rowType; column: cellType }) {
|
||||||
|
if (column.property === 'status') {
|
||||||
|
if (row.status === 'Success') {
|
||||||
|
return { color: 'green' };
|
||||||
|
} else if (row.status === 'Error') {
|
||||||
|
return { color: 'red' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (column.property === 'runMode') {
|
||||||
|
if (row.runMode === 'Dry') {
|
||||||
|
return { color: 'orange' };
|
||||||
|
} else if (row.runMode === 'Live') {
|
||||||
|
return { color: 'blue' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
onInput(input: { name: string; value: string | number | boolean }) {
|
||||||
|
if (input.name === 'loginEnabled' && typeof input.value === 'boolean') {
|
||||||
|
this.loginEnabled = input.value;
|
||||||
|
}
|
||||||
|
if (input.name === 'synchronizationEnabled' && typeof input.value === 'boolean') {
|
||||||
|
this.syncEnabled = input.value;
|
||||||
|
}
|
||||||
|
this.hasAnyChanges = true;
|
||||||
|
},
|
||||||
|
onReadyToSubmit(ready: boolean) {
|
||||||
|
this.readyToSubmit = ready;
|
||||||
|
},
|
||||||
|
syncDataMapper(sync: ILdapSyncData): ILdapSyncTable {
|
||||||
|
const startedAt = new Date(sync.startedAt);
|
||||||
|
const endedAt = new Date(sync.endedAt);
|
||||||
|
const runTimeInMinutes = endedAt.getTime() - startedAt.getTime();
|
||||||
|
return {
|
||||||
|
runTime: humanizeDuration(runTimeInMinutes),
|
||||||
|
runMode: capitalizeFirstLetter(sync.runMode),
|
||||||
|
status: capitalizeFirstLetter(sync.status),
|
||||||
|
endedAt: convertToDisplayDate(endedAt.getTime()),
|
||||||
|
details: this.$locale.baseText('settings.ldap.usersScanned', {
|
||||||
|
interpolate: {
|
||||||
|
scanned: sync.scanned.toString(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async onSubmit(): Promise<void> {
|
||||||
|
// We want to save all form values (incl. the hidden onces), so we are using
|
||||||
|
// `values` data prop of the `FormInputs` child component since they are all preserved there
|
||||||
|
const formInputs = this.$refs.ldapConfigForm as (Vue & { values: FormValues }) | undefined;
|
||||||
|
if (!this.hasAnyChanges || !formInputs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfiguration: ILdapConfig = {
|
||||||
|
loginEnabled: formInputs.values.loginEnabled,
|
||||||
|
loginLabel: formInputs.values.loginLabel,
|
||||||
|
connectionUrl: formInputs.values.serverAddress,
|
||||||
|
allowUnauthorizedCerts: formInputs.values.allowUnauthorizedCerts,
|
||||||
|
connectionPort: +formInputs.values.port,
|
||||||
|
connectionSecurity: formInputs.values.connectionSecurity,
|
||||||
|
baseDn: formInputs.values.baseDn,
|
||||||
|
bindingAdminDn: formInputs.values.bindingType === 'admin' ? formInputs.values.adminDn : '',
|
||||||
|
bindingAdminPassword:
|
||||||
|
formInputs.values.bindingType === 'admin' ? formInputs.values.adminPassword : '',
|
||||||
|
emailAttribute: formInputs.values.email,
|
||||||
|
firstNameAttribute: formInputs.values.firstName,
|
||||||
|
lastNameAttribute: formInputs.values.lastName,
|
||||||
|
loginIdAttribute: formInputs.values.loginId,
|
||||||
|
ldapIdAttribute: formInputs.values.ldapId,
|
||||||
|
userFilter: formInputs.values.userFilter,
|
||||||
|
synchronizationEnabled: formInputs.values.synchronizationEnabled,
|
||||||
|
synchronizationInterval: +formInputs.values.synchronizationInterval,
|
||||||
|
searchPageSize: +formInputs.values.pageSize,
|
||||||
|
searchTimeout: +formInputs.values.searchTimeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
let saveForm = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.adConfig.loginEnabled === true && newConfiguration.loginEnabled === false) {
|
||||||
|
saveForm = await this.confirmMessage(
|
||||||
|
this.$locale.baseText('settings.ldap.confirmMessage.beforeSaveForm.message'),
|
||||||
|
this.$locale.baseText('settings.ldap.confirmMessage.beforeSaveForm.headline'),
|
||||||
|
null,
|
||||||
|
this.$locale.baseText('settings.ldap.confirmMessage.beforeSaveForm.cancelButtonText'),
|
||||||
|
this.$locale.baseText('settings.ldap.confirmMessage.beforeSaveForm.confirmButtonText'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!saveForm) {
|
||||||
|
this.hasAnyChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.adConfig = await this.settingsStore.updateLdapConfig(newConfiguration);
|
||||||
|
this.$showToast({
|
||||||
|
title: this.$locale.baseText('settings.ldap.updateConfiguration'),
|
||||||
|
message: '',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.$showError(error, this.$locale.baseText('settings.ldap.configurationError'));
|
||||||
|
} finally {
|
||||||
|
if (saveForm) {
|
||||||
|
this.hasAnyChanges = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSaveClick() {
|
||||||
|
this.formBus.$emit('submit');
|
||||||
|
},
|
||||||
|
async onTestConnectionClick() {
|
||||||
|
this.loadingTestConnection = true;
|
||||||
|
try {
|
||||||
|
await this.settingsStore.testLdapConnection();
|
||||||
|
this.$showToast({
|
||||||
|
title: this.$locale.baseText('settings.ldap.connectionTest'),
|
||||||
|
message: this.$locale.baseText('settings.ldap.toast.connection.success'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.$showToast({
|
||||||
|
title: this.$locale.baseText('settings.ldap.connectionTestError'),
|
||||||
|
message: error.message,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.loadingTestConnection = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onDryRunClick() {
|
||||||
|
this.loadingDryRun = true;
|
||||||
|
try {
|
||||||
|
await this.settingsStore.runLdapSync({ type: 'dry' });
|
||||||
|
this.$showToast({
|
||||||
|
title: this.$locale.baseText('settings.ldap.runSync.title'),
|
||||||
|
message: this.$locale.baseText('settings.ldap.toast.sync.success'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.$showError(error, this.$locale.baseText('settings.ldap.synchronizationError'));
|
||||||
|
} finally {
|
||||||
|
this.loadingDryRun = false;
|
||||||
|
await this.reloadLdapSynchronizations();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onLiveRunClick() {
|
||||||
|
this.loadingLiveRun = true;
|
||||||
|
try {
|
||||||
|
await this.settingsStore.runLdapSync({ type: 'live' });
|
||||||
|
this.$showToast({
|
||||||
|
title: this.$locale.baseText('settings.ldap.runSync.title'),
|
||||||
|
message: this.$locale.baseText('settings.ldap.toast.sync.success'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.$showError(error, this.$locale.baseText('settings.ldap.synchronizationError'));
|
||||||
|
} finally {
|
||||||
|
this.loadingLiveRun = false;
|
||||||
|
await this.reloadLdapSynchronizations();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getLdapConfig() {
|
||||||
|
try {
|
||||||
|
this.adConfig = await this.settingsStore.getLdapConfig();
|
||||||
|
this.loginEnabled = this.adConfig.loginEnabled;
|
||||||
|
this.syncEnabled = this.adConfig.synchronizationEnabled;
|
||||||
|
const whenLoginEnabled: IFormInput['shouldDisplay'] = (values) =>
|
||||||
|
values['loginEnabled'] === true;
|
||||||
|
const whenSyncAndLoginEnabled: IFormInput['shouldDisplay'] = (values) =>
|
||||||
|
values['synchronizationEnabled'] === true && values['loginEnabled'] === true;
|
||||||
|
const whenAdminBindingAndLoginEnabled: IFormInput['shouldDisplay'] = (values) =>
|
||||||
|
values['bindingType'] === 'admin' && values['loginEnabled'] === true;
|
||||||
|
this.formInputs = [
|
||||||
|
{
|
||||||
|
name: 'loginEnabled',
|
||||||
|
initialValue: this.adConfig.loginEnabled,
|
||||||
|
properties: {
|
||||||
|
type: 'toggle',
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.loginEnabled.label'),
|
||||||
|
tooltipText: this.$locale.baseText('settings.ldap.form.loginEnabled.tooltip'),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'loginLabel',
|
||||||
|
initialValue: this.adConfig.loginLabel,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.loginLabel.label'),
|
||||||
|
required: true,
|
||||||
|
placeholder: this.$locale.baseText('settings.ldap.form.loginLabel.placeholder'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.loginLabel.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'serverAddress',
|
||||||
|
initialValue: this.adConfig.connectionUrl,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.serverAddress.label'),
|
||||||
|
required: true,
|
||||||
|
capitalize: true,
|
||||||
|
placeholder: this.$locale.baseText('settings.ldap.form.serverAddress.placeholder'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.serverAddress.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'port',
|
||||||
|
initialValue: this.adConfig.connectionPort,
|
||||||
|
properties: {
|
||||||
|
type: 'number',
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.port.label'),
|
||||||
|
capitalize: true,
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.port.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'connectionSecurity',
|
||||||
|
initialValue: this.adConfig.connectionSecurity,
|
||||||
|
properties: {
|
||||||
|
type: 'select',
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.connectionSecurity.label'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.connectionSecurity.infoText'),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'None',
|
||||||
|
value: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'TLS',
|
||||||
|
value: 'tls',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'STARTTLS',
|
||||||
|
value: 'startTls',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
capitalize: true,
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'allowUnauthorizedCerts',
|
||||||
|
initialValue: this.adConfig.allowUnauthorizedCerts,
|
||||||
|
properties: {
|
||||||
|
type: 'toggle',
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.allowUnauthorizedCerts.label'),
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
shouldDisplay(values): boolean {
|
||||||
|
return values['connectionSecurity'] !== 'none' && values['loginEnabled'] === true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'baseDn',
|
||||||
|
initialValue: this.adConfig.baseDn,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.baseDn.label'),
|
||||||
|
required: true,
|
||||||
|
capitalize: true,
|
||||||
|
placeholder: this.$locale.baseText('settings.ldap.form.baseDn.placeholder'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.baseDn.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bindingType',
|
||||||
|
initialValue: 'admin',
|
||||||
|
properties: {
|
||||||
|
type: 'select',
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.bindingType.label'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.bindingType.infoText'),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'admin',
|
||||||
|
label: 'Admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'anonymous',
|
||||||
|
label: 'Anonymous',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'adminDn',
|
||||||
|
initialValue: this.adConfig.bindingAdminDn,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.adminDn.label'),
|
||||||
|
placeholder: this.$locale.baseText('settings.ldap.form.adminDn.placeholder'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.adminDn.infoText'),
|
||||||
|
capitalize: true,
|
||||||
|
},
|
||||||
|
shouldDisplay: whenAdminBindingAndLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'adminPassword',
|
||||||
|
initialValue: this.adConfig.bindingAdminPassword,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.adminPassword.label'),
|
||||||
|
type: 'password',
|
||||||
|
capitalize: true,
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.adminPassword.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenAdminBindingAndLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'userFilter',
|
||||||
|
initialValue: this.adConfig.userFilter,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.userFilter.label'),
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
capitalize: true,
|
||||||
|
placeholder: this.$locale.baseText('settings.ldap.form.userFilter.placeholder'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.userFilter.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'attributeMappingInfo',
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.attributeMappingInfo.label'),
|
||||||
|
type: 'info',
|
||||||
|
labelSize: 'large',
|
||||||
|
labelAlignment: 'left',
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ldapId',
|
||||||
|
initialValue: this.adConfig.ldapIdAttribute,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.ldapId.label'),
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
capitalize: true,
|
||||||
|
placeholder: this.$locale.baseText('settings.ldap.form.ldapId.placeholder'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.ldapId.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'loginId',
|
||||||
|
initialValue: this.adConfig.loginIdAttribute,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.loginId.label'),
|
||||||
|
type: 'text',
|
||||||
|
autocomplete: 'email',
|
||||||
|
required: true,
|
||||||
|
capitalize: true,
|
||||||
|
placeholder: this.$locale.baseText('settings.ldap.form.loginId.placeholder'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.loginId.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
initialValue: this.adConfig.emailAttribute,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.email.label'),
|
||||||
|
type: 'text',
|
||||||
|
autocomplete: 'email',
|
||||||
|
required: true,
|
||||||
|
capitalize: true,
|
||||||
|
placeholder: this.$locale.baseText('settings.ldap.form.email.placeholder'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.email.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'firstName',
|
||||||
|
initialValue: this.adConfig.firstNameAttribute,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.firstName.label'),
|
||||||
|
type: 'text',
|
||||||
|
autocomplete: 'email',
|
||||||
|
required: true,
|
||||||
|
capitalize: true,
|
||||||
|
placeholder: this.$locale.baseText('settings.ldap.form.firstName.placeholder'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.firstName.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lastName',
|
||||||
|
initialValue: this.adConfig.lastNameAttribute,
|
||||||
|
properties: {
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.lastName.label'),
|
||||||
|
type: 'text',
|
||||||
|
autocomplete: 'email',
|
||||||
|
required: true,
|
||||||
|
capitalize: true,
|
||||||
|
placeholder: this.$locale.baseText('settings.ldap.form.lastName.placeholder'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.lastName.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'synchronizationEnabled',
|
||||||
|
initialValue: this.adConfig.synchronizationEnabled,
|
||||||
|
properties: {
|
||||||
|
type: 'toggle',
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.synchronizationEnabled.label'),
|
||||||
|
tooltipText: this.$locale.baseText(
|
||||||
|
'settings.ldap.form.synchronizationEnabled.tooltip',
|
||||||
|
),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
shouldDisplay: whenLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'synchronizationInterval',
|
||||||
|
initialValue: this.adConfig.synchronizationInterval,
|
||||||
|
properties: {
|
||||||
|
type: 'number',
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.synchronizationInterval.label'),
|
||||||
|
infoText: this.$locale.baseText(
|
||||||
|
'settings.ldap.form.synchronizationInterval.infoText',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenSyncAndLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pageSize',
|
||||||
|
initialValue: this.adConfig.searchPageSize,
|
||||||
|
properties: {
|
||||||
|
type: 'number',
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.pageSize.label'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.pageSize.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenSyncAndLoginEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'searchTimeout',
|
||||||
|
initialValue: this.adConfig.searchTimeout,
|
||||||
|
properties: {
|
||||||
|
type: 'number',
|
||||||
|
label: this.$locale.baseText('settings.ldap.form.searchTimeout.label'),
|
||||||
|
infoText: this.$locale.baseText('settings.ldap.form.searchTimeout.infoText'),
|
||||||
|
},
|
||||||
|
shouldDisplay: whenSyncAndLoginEnabled,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
this.$showError(error, this.$locale.baseText('settings.ldap.configurationError'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getLdapSynchronizations(state: any) {
|
||||||
|
try {
|
||||||
|
this.loadingTable = true;
|
||||||
|
const data = await this.settingsStore.getLdapSynchronizations({
|
||||||
|
page: this.page,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.length !== 0) {
|
||||||
|
this.dataTable.push(...data.map(this.syncDataMapper));
|
||||||
|
this.page += 1;
|
||||||
|
state.loaded();
|
||||||
|
} else {
|
||||||
|
state.complete();
|
||||||
|
}
|
||||||
|
this.loadingTable = false;
|
||||||
|
} catch (error) {
|
||||||
|
this.$showError(error, this.$locale.baseText('settings.ldap.synchronizationError'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async reloadLdapSynchronizations() {
|
||||||
|
try {
|
||||||
|
this.page = 0;
|
||||||
|
this.tableKey += 1;
|
||||||
|
this.dataTable = [];
|
||||||
|
} catch (error) {
|
||||||
|
this.$showError(error, this.$locale.baseText('settings.ldap.synchronizationError'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.container {
|
||||||
|
> * {
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.syncTable {
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
*:first-child {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.enableFeatureContainer {
|
||||||
|
margin-bottom: var(--spacing-1xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enableFeatureContainer > span {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enableFeatureContainer {
|
||||||
|
> * {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settingsForm {
|
||||||
|
:global(.form-text) {
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.docsInfoTip {
|
||||||
|
&,
|
||||||
|
& > div {
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -32,7 +32,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="!signInWithLdap">
|
||||||
<div :class="$style.sectionHeader">
|
<div :class="$style.sectionHeader">
|
||||||
<n8n-heading size="large">{{ $locale.baseText('settings.personal.security') }}</n8n-heading>
|
<n8n-heading size="large">{{ $locale.baseText('settings.personal.security') }}</n8n-heading>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,10 +58,11 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { showMessage } from '@/mixins/showMessage';
|
import { showMessage } from '@/mixins/showMessage';
|
||||||
import { CHANGE_PASSWORD_MODAL_KEY } from '@/constants';
|
import { CHANGE_PASSWORD_MODAL_KEY, SignInType } from '@/constants';
|
||||||
import { IFormInputs, IUser } from '@/Interface';
|
import { IFormInputs, IUser } from '@/Interface';
|
||||||
import { useUIStore } from '@/stores/ui';
|
import { useUIStore } from '@/stores/ui';
|
||||||
import { useUsersStore } from '@/stores/users';
|
import { useUsersStore } from '@/stores/users';
|
||||||
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
@ -87,6 +88,7 @@ export default mixins(showMessage).extend({
|
||||||
required: true,
|
required: true,
|
||||||
autocomplete: 'given-name',
|
autocomplete: 'given-name',
|
||||||
capitalize: true,
|
capitalize: true,
|
||||||
|
disabled: this.isLDAPFeatureEnabled && this.signInWithLdap,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -98,6 +100,7 @@ export default mixins(showMessage).extend({
|
||||||
required: true,
|
required: true,
|
||||||
autocomplete: 'family-name',
|
autocomplete: 'family-name',
|
||||||
capitalize: true,
|
capitalize: true,
|
||||||
|
disabled: this.isLDAPFeatureEnabled && this.signInWithLdap,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -110,15 +113,22 @@ export default mixins(showMessage).extend({
|
||||||
validationRules: [{ name: 'VALID_EMAIL' }],
|
validationRules: [{ name: 'VALID_EMAIL' }],
|
||||||
autocomplete: 'email',
|
autocomplete: 'email',
|
||||||
capitalize: true,
|
capitalize: true,
|
||||||
|
disabled: this.isLDAPFeatureEnabled && this.signInWithLdap,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useUIStore, useUsersStore),
|
...mapStores(useUIStore, useUsersStore, useSettingsStore),
|
||||||
currentUser(): IUser | null {
|
currentUser(): IUser | null {
|
||||||
return this.usersStore.currentUser;
|
return this.usersStore.currentUser;
|
||||||
},
|
},
|
||||||
|
signInWithLdap(): boolean {
|
||||||
|
return this.currentUser?.signInType === 'ldap';
|
||||||
|
},
|
||||||
|
isLDAPFeatureEnabled(): boolean {
|
||||||
|
return this.settingsStore.settings.enterprise.ldap === true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onInput() {
|
onInput() {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { IFormBoxConfig } from '@/Interface';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useUsersStore } from '@/stores/users';
|
import { useUsersStore } from '@/stores/users';
|
||||||
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
|
|
||||||
export default mixins(showMessage).extend({
|
export default mixins(showMessage).extend({
|
||||||
name: 'SigninView',
|
name: 'SigninView',
|
||||||
|
@ -23,7 +24,22 @@ export default mixins(showMessage).extend({
|
||||||
AuthView,
|
AuthView,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const FORM_CONFIG: IFormBoxConfig = {
|
return {
|
||||||
|
FORM_CONFIG: {} as IFormBoxConfig,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useUsersStore, useSettingsStore),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
let emailLabel = this.$locale.baseText('auth.email');
|
||||||
|
const ldapLoginLabel = this.settingsStore.ldapLoginLabel;
|
||||||
|
const isLdapLoginEnabled = this.settingsStore.isLdapLoginEnabled;
|
||||||
|
if (isLdapLoginEnabled && ldapLoginLabel) {
|
||||||
|
emailLabel = ldapLoginLabel;
|
||||||
|
}
|
||||||
|
this.FORM_CONFIG = {
|
||||||
title: this.$locale.baseText('auth.signin'),
|
title: this.$locale.baseText('auth.signin'),
|
||||||
buttonText: this.$locale.baseText('auth.signin'),
|
buttonText: this.$locale.baseText('auth.signin'),
|
||||||
redirectText: this.$locale.baseText('forgotPassword'),
|
redirectText: this.$locale.baseText('forgotPassword'),
|
||||||
|
@ -32,10 +48,10 @@ export default mixins(showMessage).extend({
|
||||||
{
|
{
|
||||||
name: 'email',
|
name: 'email',
|
||||||
properties: {
|
properties: {
|
||||||
label: this.$locale.baseText('auth.email'),
|
label: emailLabel,
|
||||||
type: 'email',
|
type: 'email',
|
||||||
required: true,
|
required: true,
|
||||||
validationRules: [{ name: 'VALID_EMAIL' }],
|
...(!isLdapLoginEnabled && { validationRules: [{ name: 'VALID_EMAIL' }] }),
|
||||||
showRequiredAsterisk: false,
|
showRequiredAsterisk: false,
|
||||||
validateOnBlur: false,
|
validateOnBlur: false,
|
||||||
autocomplete: 'email',
|
autocomplete: 'email',
|
||||||
|
@ -56,14 +72,6 @@ export default mixins(showMessage).extend({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
|
||||||
FORM_CONFIG,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useUsersStore),
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async onSubmit(values: { [key: string]: string }) {
|
async onSubmit(values: { [key: string]: string }) {
|
||||||
|
|
|
@ -171,6 +171,7 @@ importers:
|
||||||
jsonschema: ^1.4.1
|
jsonschema: ^1.4.1
|
||||||
jsonwebtoken: 9.0.0
|
jsonwebtoken: 9.0.0
|
||||||
jwks-rsa: ~1.12.1
|
jwks-rsa: ~1.12.1
|
||||||
|
ldapts: ^4.2.2
|
||||||
localtunnel: ^2.0.0
|
localtunnel: ^2.0.0
|
||||||
lodash.get: ^4.4.2
|
lodash.get: ^4.4.2
|
||||||
lodash.intersection: ^4.4.0
|
lodash.intersection: ^4.4.0
|
||||||
|
@ -264,6 +265,7 @@ importers:
|
||||||
jsonschema: 1.4.1
|
jsonschema: 1.4.1
|
||||||
jsonwebtoken: 9.0.0
|
jsonwebtoken: 9.0.0
|
||||||
jwks-rsa: 1.12.3
|
jwks-rsa: 1.12.3
|
||||||
|
ldapts: 4.2.2
|
||||||
localtunnel: 2.0.2
|
localtunnel: 2.0.2
|
||||||
lodash.get: 4.4.2
|
lodash.get: 4.4.2
|
||||||
lodash.intersection: 4.4.0
|
lodash.intersection: 4.4.0
|
||||||
|
@ -528,6 +530,7 @@ importers:
|
||||||
'@types/dateformat': ^3.0.0
|
'@types/dateformat': ^3.0.0
|
||||||
'@types/express': ^4.17.6
|
'@types/express': ^4.17.6
|
||||||
'@types/file-saver': ^2.0.1
|
'@types/file-saver': ^2.0.1
|
||||||
|
'@types/humanize-duration': ^3.27.1
|
||||||
'@types/jsonpath': ^0.2.0
|
'@types/jsonpath': ^0.2.0
|
||||||
'@types/lodash-es': ^4.17.6
|
'@types/lodash-es': ^4.17.6
|
||||||
'@types/lodash.camelcase': ^4.3.6
|
'@types/lodash.camelcase': ^4.3.6
|
||||||
|
@ -545,6 +548,7 @@ importers:
|
||||||
fast-json-stable-stringify: ^2.1.0
|
fast-json-stable-stringify: ^2.1.0
|
||||||
file-saver: ^2.0.2
|
file-saver: ^2.0.2
|
||||||
flatted: ^3.2.4
|
flatted: ^3.2.4
|
||||||
|
humanize-duration: ^3.27.2
|
||||||
jquery: ^3.4.1
|
jquery: ^3.4.1
|
||||||
jshint: ^2.9.7
|
jshint: ^2.9.7
|
||||||
jsonpath: ^1.1.1
|
jsonpath: ^1.1.1
|
||||||
|
@ -576,6 +580,7 @@ importers:
|
||||||
vue-agile: ^2.0.0
|
vue-agile: ^2.0.0
|
||||||
vue-fragment: 1.5.1
|
vue-fragment: 1.5.1
|
||||||
vue-i18n: ^8.26.7
|
vue-i18n: ^8.26.7
|
||||||
|
vue-infinite-loading: ^2.4.5
|
||||||
vue-json-pretty: 1.9.3
|
vue-json-pretty: 1.9.3
|
||||||
vue-prism-editor: ^0.3.0
|
vue-prism-editor: ^0.3.0
|
||||||
vue-router: ^3.6.5
|
vue-router: ^3.6.5
|
||||||
|
@ -606,6 +611,7 @@ importers:
|
||||||
fast-json-stable-stringify: 2.1.0
|
fast-json-stable-stringify: 2.1.0
|
||||||
file-saver: 2.0.5
|
file-saver: 2.0.5
|
||||||
flatted: 3.2.7
|
flatted: 3.2.7
|
||||||
|
humanize-duration: 3.27.3
|
||||||
jquery: 3.6.1
|
jquery: 3.6.1
|
||||||
jsonpath: 1.1.1
|
jsonpath: 1.1.1
|
||||||
jsplumb: 2.15.4
|
jsplumb: 2.15.4
|
||||||
|
@ -629,6 +635,7 @@ importers:
|
||||||
vue-agile: 2.0.0
|
vue-agile: 2.0.0
|
||||||
vue-fragment: 1.5.1_vue@2.7.13
|
vue-fragment: 1.5.1_vue@2.7.13
|
||||||
vue-i18n: 8.27.2_vue@2.7.13
|
vue-i18n: 8.27.2_vue@2.7.13
|
||||||
|
vue-infinite-loading: 2.4.5_vue@2.7.13
|
||||||
vue-json-pretty: 1.9.3
|
vue-json-pretty: 1.9.3
|
||||||
vue-prism-editor: 0.3.0
|
vue-prism-editor: 0.3.0
|
||||||
vue-router: 3.6.5_vue@2.7.13
|
vue-router: 3.6.5_vue@2.7.13
|
||||||
|
@ -645,6 +652,7 @@ importers:
|
||||||
'@types/dateformat': 3.0.1
|
'@types/dateformat': 3.0.1
|
||||||
'@types/express': 4.17.14
|
'@types/express': 4.17.14
|
||||||
'@types/file-saver': 2.0.5
|
'@types/file-saver': 2.0.5
|
||||||
|
'@types/humanize-duration': 3.27.1
|
||||||
'@types/jsonpath': 0.2.0
|
'@types/jsonpath': 0.2.0
|
||||||
'@types/lodash-es': 4.17.6
|
'@types/lodash-es': 4.17.6
|
||||||
'@types/lodash.camelcase': 4.3.7
|
'@types/lodash.camelcase': 4.3.7
|
||||||
|
@ -5525,6 +5533,12 @@ packages:
|
||||||
resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==}
|
resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/asn1/0.2.0:
|
||||||
|
resolution: {integrity: sha512-5TMxIpYbIA9c1J0hYQjQDX3wr+rTgQEAXaW2BI8ECM8FO53wSW4HFZplTalrKSHuZUc76NtXcePRhwuOHqGD5g==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 16.11.65
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/aws4/1.11.2:
|
/@types/aws4/1.11.2:
|
||||||
resolution: {integrity: sha512-x0f96eBPrCCJzJxdPbUvDFRva4yPpINJzTuXXpmS2j9qLUpF2nyGzvXPlRziuGbCsPukwY4JfuO+8xwsoZLzGw==}
|
resolution: {integrity: sha512-x0f96eBPrCCJzJxdPbUvDFRva4yPpINJzTuXXpmS2j9qLUpF2nyGzvXPlRziuGbCsPukwY4JfuO+8xwsoZLzGw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5774,6 +5788,10 @@ packages:
|
||||||
resolution: {integrity: sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==}
|
resolution: {integrity: sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/humanize-duration/3.27.1:
|
||||||
|
resolution: {integrity: sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/imap-simple/4.2.5:
|
/@types/imap-simple/4.2.5:
|
||||||
resolution: {integrity: sha512-Sfu70sdFXzVIhivsflpanlED8gZr4VRzz2AVU9i1ARU8gskr9nDd4tVGkqYtxfwajQfZDklkXbeHSOZYEeJmTQ==}
|
resolution: {integrity: sha512-Sfu70sdFXzVIhivsflpanlED8gZr4VRzz2AVU9i1ARU8gskr9nDd4tVGkqYtxfwajQfZDklkXbeHSOZYEeJmTQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6332,6 +6350,10 @@ packages:
|
||||||
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/uuid/9.0.0:
|
||||||
|
resolution: {integrity: sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/validator/13.7.10:
|
/@types/validator/13.7.10:
|
||||||
resolution: {integrity: sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ==}
|
resolution: {integrity: sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -13103,6 +13125,10 @@ packages:
|
||||||
engines: {node: '>=10.17.0'}
|
engines: {node: '>=10.17.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/humanize-duration/3.27.3:
|
||||||
|
resolution: {integrity: sha512-iimHkHPfIAQ8zCDQLgn08pRqSVioyWvnGfaQ8gond2wf7Jq2jJ+24ykmnRyiz3fIldcn4oUuQXpjqKLhSVR7lw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/humanize-ms/1.2.1:
|
/humanize-ms/1.2.1:
|
||||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -14962,6 +14988,21 @@ packages:
|
||||||
invert-kv: 1.0.0
|
invert-kv: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ldapts/4.2.2:
|
||||||
|
resolution: {integrity: sha512-UHe7BtEhPUFHZZ6XHnRvLHWQrftTap3PgGU0nOLtrFeigZvfpXSsqJ8C9uXNouDV+iDHqoWwplS0eHoDu/GIEQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dependencies:
|
||||||
|
'@types/asn1': 0.2.0
|
||||||
|
'@types/node': 16.11.65
|
||||||
|
'@types/uuid': 9.0.0
|
||||||
|
asn1: 0.2.6
|
||||||
|
debug: 4.3.4
|
||||||
|
strict-event-emitter-types: 2.0.0
|
||||||
|
uuid: 9.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lead/1.0.0:
|
/lead/1.0.0:
|
||||||
resolution: {integrity: sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==}
|
resolution: {integrity: sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
@ -19825,6 +19866,10 @@ packages:
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/strict-event-emitter-types/2.0.0:
|
||||||
|
resolution: {integrity: sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/strict-uri-encode/2.0.0:
|
/strict-uri-encode/2.0.0:
|
||||||
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
|
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -21435,6 +21480,11 @@ packages:
|
||||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/uuid/9.0.0:
|
||||||
|
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/v-click-outside/3.2.0:
|
/v-click-outside/3.2.0:
|
||||||
resolution: {integrity: sha512-QD0bDy38SHJXQBjgnllmkI/rbdiwmq9RC+/+pvrFjYJKTn8dtp7Penf9q1lLBta280fYG2q53mgLhQ+3l3z74w==}
|
resolution: {integrity: sha512-QD0bDy38SHJXQBjgnllmkI/rbdiwmq9RC+/+pvrFjYJKTn8dtp7Penf9q1lLBta280fYG2q53mgLhQ+3l3z74w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -21844,6 +21894,14 @@ packages:
|
||||||
vue: 2.7.13
|
vue: 2.7.13
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/vue-infinite-loading/2.4.5_vue@2.7.13:
|
||||||
|
resolution: {integrity: sha512-xhq95Mxun060bRnsOoLE2Be6BR7jYwuC89kDe18+GmCLVrRA/dU0jrGb12Xu6NjmKs+iTW0AA6saSEmEW4cR7g==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^2.6.10
|
||||||
|
dependencies:
|
||||||
|
vue: 2.7.13
|
||||||
|
dev: false
|
||||||
|
|
||||||
/vue-json-pretty/1.9.3:
|
/vue-json-pretty/1.9.3:
|
||||||
resolution: {integrity: sha512-b13DP1WGQ+ACUU2K5hmwFfHrHnydCFSTerE7fppeYMojSWN/5EOPODQECfIIRaJ7zzHtPW9OifkThFGPyY0xRg==}
|
resolution: {integrity: sha512-b13DP1WGQ+ACUU2K5hmwFfHrHnydCFSTerE7fppeYMojSWN/5EOPODQECfIIRaJ7zzHtPW9OifkThFGPyY0xRg==}
|
||||||
engines: {node: '>= 10.0.0', npm: '>= 5.0.0'}
|
engines: {node: '>= 10.0.0', npm: '>= 5.0.0'}
|
||||||
|
|
Loading…
Reference in a new issue