mirror of
https://github.com/n8n-io/n8n.git
synced 2024-09-20 06:47:32 -07:00
refactor(core): Use DI for LDAP code (no-changelog) (#8248)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
parent
a6a5372b5f
commit
3c2a4000ae
|
@ -1,39 +0,0 @@
|
|||
import { ApplicationError } from 'n8n-workflow';
|
||||
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 ApplicationError('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;
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
import type { Entry as LdapUser, ClientOptions } from 'ldapts';
|
||||
import { Client } from 'ldapts';
|
||||
import type { LdapConfig } from './types';
|
||||
import { formatUrl, getMappingAttributes } from './helpers';
|
||||
import { BINARY_AD_ATTRIBUTES } from './constants';
|
||||
import type { ConnectionOptions } from 'tls';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
|
||||
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 ApplicationError('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 [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
|
@ -1,216 +0,0 @@
|
|||
import type { Entry as LdapUser } from 'ldapts';
|
||||
import { QueryFailedError } from 'typeorm/error/QueryFailedError';
|
||||
import type { 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 { Container } from 'typedi';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { Logger } from '@/Logger';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
|
||||
export class LdapSync {
|
||||
private intervalId: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
private _config: LdapConfig;
|
||||
|
||||
private _ldapService: LdapService;
|
||||
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor() {
|
||||
this.logger = Container.get(Logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ApplicationError('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> {
|
||||
this.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),
|
||||
);
|
||||
|
||||
this.logger.debug('LDAP - Users return by the query', {
|
||||
users: adUsers,
|
||||
});
|
||||
|
||||
resolveBinaryAttributes(adUsers);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
this.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,
|
||||
);
|
||||
|
||||
this.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 Container.get(InternalHooks).onLdapSyncFinished({
|
||||
type: !this.intervalId ? 'scheduled' : `manual_${mode}`,
|
||||
succeeded: true,
|
||||
users_synced: usersToCreate.length + usersToUpdate.length + usersToDisable.length,
|
||||
error: errorMessage,
|
||||
});
|
||||
|
||||
this.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));
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
import type { Entry as LdapUser } from 'ldapts';
|
||||
import { Filter } from 'ldapts/filters/Filter';
|
||||
import { Container } from 'typedi';
|
||||
import { Cipher } from 'n8n-core';
|
||||
import { validate } from 'jsonschema';
|
||||
import * as Db from '@/Db';
|
||||
import config from '@/config';
|
||||
|
@ -10,33 +9,19 @@ import type { Role } from '@db/entities/Role';
|
|||
import { User } from '@db/entities/User';
|
||||
import { AuthIdentity } from '@db/entities/AuthIdentity';
|
||||
import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory';
|
||||
import { LdapManager } from './LdapManager.ee';
|
||||
|
||||
import {
|
||||
BINARY_AD_ATTRIBUTES,
|
||||
LDAP_CONFIG_SCHEMA,
|
||||
LDAP_FEATURE_NAME,
|
||||
LDAP_LOGIN_ENABLED,
|
||||
LDAP_LOGIN_LABEL,
|
||||
} from './constants';
|
||||
import type { ConnectionSecurity, LdapConfig } from './types';
|
||||
import { ApplicationError, jsonParse } from 'n8n-workflow';
|
||||
import { License } from '@/License';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import {
|
||||
getCurrentAuthenticationMethod,
|
||||
isEmailCurrentAuthenticationMethod,
|
||||
isLdapCurrentAuthenticationMethod,
|
||||
setCurrentAuthenticationMethod,
|
||||
} from '@/sso/ssoHelpers';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
import { Logger } from '@/Logger';
|
||||
import { UserRepository } from '@db/repositories/user.repository';
|
||||
import { SettingsRepository } from '@db/repositories/settings.repository';
|
||||
import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProviderSyncHistory.repository';
|
||||
import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
|
||||
/**
|
||||
* Check whether the LDAP feature is disabled in the instance
|
||||
|
@ -45,37 +30,6 @@ export const isLdapEnabled = () => {
|
|||
return Container.get(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 async function setLdapLoginEnabled(enabled: boolean): Promise<void> {
|
||||
if (isEmailCurrentAuthenticationMethod() || isLdapCurrentAuthenticationMethod()) {
|
||||
if (enabled) {
|
||||
config.set(LDAP_LOGIN_ENABLED, true);
|
||||
await setCurrentAuthenticationMethod('ldap');
|
||||
} else if (!enabled) {
|
||||
config.set(LDAP_LOGIN_ENABLED, false);
|
||||
await setCurrentAuthenticationMethod('email');
|
||||
}
|
||||
} else {
|
||||
throw new InternalServerError(
|
||||
`Cannot switch LDAP login enabled state when an authentication method other than email or ldap is active (current: ${getCurrentAuthenticationMethod()})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the LDAP login label from the configuration object
|
||||
*/
|
||||
|
@ -115,29 +69,7 @@ export const validateLdapConfigurationSchema = (
|
|||
return { valid, message };
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the LDAP configuration (decrypted) form the database
|
||||
*/
|
||||
export const getLdapConfig = async (): Promise<LdapConfig> => {
|
||||
const configuration = await Container.get(SettingsRepository).findOneByOrFail({
|
||||
key: LDAP_FEATURE_NAME,
|
||||
});
|
||||
const configurationData = jsonParse<LdapConfig>(configuration.value);
|
||||
configurationData.bindingAdminPassword = Container.get(Cipher).decrypt(
|
||||
configurationData.bindingAdminPassword,
|
||||
);
|
||||
return configurationData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Take the LDAP configuration and set login enabled and login label to the config object
|
||||
*/
|
||||
export const setGlobalLdapConfigVariables = async (ldapConfig: LdapConfig): Promise<void> => {
|
||||
await setLdapLoginEnabled(ldapConfig.loginEnabled);
|
||||
setLdapLoginLabel(ldapConfig.loginLabel);
|
||||
};
|
||||
|
||||
const resolveEntryBinaryAttributes = (entry: LdapUser): LdapUser => {
|
||||
export const resolveEntryBinaryAttributes = (entry: LdapUser): LdapUser => {
|
||||
Object.entries(entry)
|
||||
.filter(([k]) => BINARY_AD_ATTRIBUTES.includes(k))
|
||||
.forEach(([k]) => {
|
||||
|
@ -150,63 +82,6 @@ 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 ApplicationError(message);
|
||||
}
|
||||
|
||||
if (ldapConfig.loginEnabled && getCurrentAuthenticationMethod() === 'saml') {
|
||||
throw new BadRequestError('LDAP cannot be enabled if SSO in enabled');
|
||||
}
|
||||
|
||||
LdapManager.updateConfig({ ...ldapConfig });
|
||||
|
||||
ldapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(ldapConfig.bindingAdminPassword);
|
||||
|
||||
if (!ldapConfig.loginEnabled) {
|
||||
ldapConfig.synchronizationEnabled = false;
|
||||
const ldapUsers = await getLdapUsers();
|
||||
if (ldapUsers.length) {
|
||||
await deleteAllLdapIdentities();
|
||||
}
|
||||
}
|
||||
|
||||
await Container.get(SettingsRepository).update(
|
||||
{ key: LDAP_FEATURE_NAME },
|
||||
{ value: JSON.stringify(ldapConfig), loadOnStartup: true },
|
||||
);
|
||||
await 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()) return;
|
||||
|
||||
const ldapConfig = await getLdapConfig();
|
||||
|
||||
try {
|
||||
await setGlobalLdapConfigVariables(ldapConfig);
|
||||
} catch (error) {
|
||||
Container.get(Logger).warn(
|
||||
`Cannot set LDAP login enabled state when an authentication method other than email or ldap is active (current: ${getCurrentAuthenticationMethod()})`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
@ -220,69 +95,6 @@ export const escapeFilter = (filter: string): string => {
|
|||
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 Container.get(InternalHooks).onLdapLoginSyncFailed({
|
||||
error: e.message,
|
||||
});
|
||||
Container.get(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) {
|
||||
Container.get(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
|
||||
*/
|
||||
|
@ -475,6 +287,6 @@ export const updateLdapUserOnLocalDb = async (identity: AuthIdentity, data: Part
|
|||
}
|
||||
};
|
||||
|
||||
const deleteAllLdapIdentities = async () => {
|
||||
export const deleteAllLdapIdentities = async () => {
|
||||
return Container.get(AuthIdentityRepository).delete({ providerType: 'ldap' });
|
||||
};
|
||||
|
|
|
@ -1,31 +1,25 @@
|
|||
import pick from 'lodash/pick';
|
||||
import { Authorized, Get, Post, Put, RestController, RequireGlobalScope } from '@/decorators';
|
||||
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers';
|
||||
import { LdapManager } from '@/Ldap/LdapManager.ee';
|
||||
import type { LdapService } from '@/Ldap/LdapService.ee';
|
||||
import type { LdapSync } from '@/Ldap/LdapSync.ee';
|
||||
import { LdapConfiguration } from '@/Ldap/types';
|
||||
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
|
||||
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from './constants';
|
||||
import { getLdapSynchronizations } from './helpers';
|
||||
import { LdapConfiguration } from './types';
|
||||
import { LdapService } from './ldap.service';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/ldap')
|
||||
export class LdapController {
|
||||
private ldapService: LdapService;
|
||||
|
||||
private ldapSync: LdapSync;
|
||||
|
||||
constructor(private readonly internalHooks: InternalHooks) {
|
||||
const { service, sync } = LdapManager.getInstance();
|
||||
this.ldapService = service;
|
||||
this.ldapSync = sync;
|
||||
}
|
||||
constructor(
|
||||
private readonly internalHooks: InternalHooks,
|
||||
private readonly ldapService: LdapService,
|
||||
) {}
|
||||
|
||||
@Get('/config')
|
||||
@RequireGlobalScope('ldap:manage')
|
||||
async getConfig() {
|
||||
return getLdapConfig();
|
||||
return this.ldapService.loadConfig();
|
||||
}
|
||||
|
||||
@Post('/test-connection')
|
||||
|
@ -42,12 +36,12 @@ export class LdapController {
|
|||
@RequireGlobalScope('ldap:manage')
|
||||
async updateConfig(req: LdapConfiguration.Update) {
|
||||
try {
|
||||
await updateLdapConfig(req.body);
|
||||
await this.ldapService.updateConfig(req.body);
|
||||
} catch (error) {
|
||||
throw new BadRequestError((error as { message: string }).message);
|
||||
}
|
||||
|
||||
const data = await getLdapConfig();
|
||||
const data = await this.ldapService.loadConfig();
|
||||
|
||||
void this.internalHooks.onUserUpdatedLdapSettings({
|
||||
user_id: req.user.id,
|
||||
|
@ -68,7 +62,7 @@ export class LdapController {
|
|||
@RequireGlobalScope('ldap:sync')
|
||||
async syncLdap(req: LdapConfiguration.Sync) {
|
||||
try {
|
||||
await this.ldapSync.run(req.body.type);
|
||||
await this.ldapService.runSync(req.body.type);
|
||||
} catch (error) {
|
||||
throw new BadRequestError((error as { message: string }).message);
|
||||
}
|
449
packages/cli/src/Ldap/ldap.service.ts
Normal file
449
packages/cli/src/Ldap/ldap.service.ts
Normal file
|
@ -0,0 +1,449 @@
|
|||
import { Service } from 'typedi';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
import type { Entry as LdapUser, ClientOptions } from 'ldapts';
|
||||
import { Client } from 'ldapts';
|
||||
import type { ConnectionOptions } from 'tls';
|
||||
import { ApplicationError, jsonParse } from 'n8n-workflow';
|
||||
import { Cipher } from 'n8n-core';
|
||||
|
||||
import config from '@/config';
|
||||
import type { Role } from '@db/entities/Role';
|
||||
import type { User } from '@db/entities/User';
|
||||
import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHistory';
|
||||
import { SettingsRepository } from '@db/repositories/settings.repository';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { Logger } from '@/Logger';
|
||||
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import {
|
||||
getCurrentAuthenticationMethod,
|
||||
isEmailCurrentAuthenticationMethod,
|
||||
isLdapCurrentAuthenticationMethod,
|
||||
setCurrentAuthenticationMethod,
|
||||
} from '@/sso/ssoHelpers';
|
||||
|
||||
import type { LdapConfig } from './types';
|
||||
import {
|
||||
createFilter,
|
||||
deleteAllLdapIdentities,
|
||||
escapeFilter,
|
||||
formatUrl,
|
||||
getLdapIds,
|
||||
getLdapUserRole,
|
||||
getLdapUsers,
|
||||
getMappingAttributes,
|
||||
mapLdapUserToDbUser,
|
||||
processUsers,
|
||||
resolveBinaryAttributes,
|
||||
resolveEntryBinaryAttributes,
|
||||
saveLdapSynchronization,
|
||||
validateLdapConfigurationSchema,
|
||||
} from './helpers';
|
||||
import {
|
||||
BINARY_AD_ATTRIBUTES,
|
||||
LDAP_FEATURE_NAME,
|
||||
LDAP_LOGIN_ENABLED,
|
||||
LDAP_LOGIN_LABEL,
|
||||
} from './constants';
|
||||
|
||||
@Service()
|
||||
export class LdapService {
|
||||
private client: Client | undefined;
|
||||
|
||||
private syncTimer: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
config: LdapConfig;
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly internalHooks: InternalHooks,
|
||||
private readonly settingsRepository: SettingsRepository,
|
||||
private readonly cipher: Cipher,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
const ldapConfig = await this.loadConfig();
|
||||
|
||||
try {
|
||||
await this.setGlobalLdapConfigVariables(ldapConfig);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Cannot set LDAP login enabled state when an authentication method other than email or ldap is active (current: ${getCurrentAuthenticationMethod()})`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
this.setConfig(ldapConfig);
|
||||
}
|
||||
|
||||
/** Retrieve the LDAP configuration (decrypted) from the database */
|
||||
async loadConfig() {
|
||||
const { value } = await this.settingsRepository.findOneByOrFail({
|
||||
key: LDAP_FEATURE_NAME,
|
||||
});
|
||||
const ldapConfig = jsonParse<LdapConfig>(value);
|
||||
ldapConfig.bindingAdminPassword = this.cipher.decrypt(ldapConfig.bindingAdminPassword);
|
||||
return ldapConfig;
|
||||
}
|
||||
|
||||
async updateConfig(ldapConfig: LdapConfig): Promise<void> {
|
||||
const { valid, message } = validateLdapConfigurationSchema(ldapConfig);
|
||||
|
||||
if (!valid) {
|
||||
throw new ApplicationError(message);
|
||||
}
|
||||
|
||||
if (ldapConfig.loginEnabled && getCurrentAuthenticationMethod() === 'saml') {
|
||||
throw new BadRequestError('LDAP cannot be enabled if SSO in enabled');
|
||||
}
|
||||
|
||||
this.setConfig({ ...ldapConfig });
|
||||
|
||||
ldapConfig.bindingAdminPassword = this.cipher.encrypt(ldapConfig.bindingAdminPassword);
|
||||
|
||||
if (!ldapConfig.loginEnabled) {
|
||||
ldapConfig.synchronizationEnabled = false;
|
||||
const ldapUsers = await getLdapUsers();
|
||||
if (ldapUsers.length) {
|
||||
await deleteAllLdapIdentities();
|
||||
}
|
||||
}
|
||||
|
||||
await this.settingsRepository.update(
|
||||
{ key: LDAP_FEATURE_NAME },
|
||||
{ value: JSON.stringify(ldapConfig), loadOnStartup: true },
|
||||
);
|
||||
await this.setGlobalLdapConfigVariables(ldapConfig);
|
||||
}
|
||||
|
||||
/** Set the LDAP configuration and expire the current client */
|
||||
setConfig(ldapConfig: LdapConfig) {
|
||||
this.config = ldapConfig;
|
||||
this.client = undefined;
|
||||
// If user disabled synchronization in the UI and there a job schedule,
|
||||
// stop it
|
||||
if (this.syncTimer && !this.config.synchronizationEnabled) {
|
||||
this.stopSync();
|
||||
// If instance crashed with a job scheduled, once the server starts
|
||||
// again, reschedule it.
|
||||
} else if (!this.syncTimer && this.config.synchronizationEnabled) {
|
||||
this.scheduleSync();
|
||||
// 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.syncTimer && this.config.synchronizationEnabled) {
|
||||
this.stopSync();
|
||||
this.scheduleSync();
|
||||
}
|
||||
}
|
||||
|
||||
/** Take the LDAP configuration and set login enabled and login label to the config object */
|
||||
private async setGlobalLdapConfigVariables(ldapConfig: LdapConfig): Promise<void> {
|
||||
await this.setLdapLoginEnabled(ldapConfig.loginEnabled);
|
||||
config.set(LDAP_LOGIN_LABEL, ldapConfig.loginLabel);
|
||||
}
|
||||
|
||||
/** Set the LDAP login enabled to the configuration object */
|
||||
private async setLdapLoginEnabled(enabled: boolean): Promise<void> {
|
||||
if (isEmailCurrentAuthenticationMethod() || isLdapCurrentAuthenticationMethod()) {
|
||||
if (enabled) {
|
||||
config.set(LDAP_LOGIN_ENABLED, true);
|
||||
await setCurrentAuthenticationMethod('ldap');
|
||||
} else if (!enabled) {
|
||||
config.set(LDAP_LOGIN_ENABLED, false);
|
||||
await setCurrentAuthenticationMethod('email');
|
||||
}
|
||||
} else {
|
||||
throw new InternalServerError(
|
||||
`Cannot switch LDAP login enabled state when an authentication method other than email or ldap is active (current: ${getCurrentAuthenticationMethod()})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get new/existing LDAP client,
|
||||
* depending on whether the credentials
|
||||
* were updated or not
|
||||
*/
|
||||
private async getClient() {
|
||||
if (this.config === undefined) {
|
||||
throw new ApplicationError('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 [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and authenticate user in the LDAP server.
|
||||
*/
|
||||
async findAndAuthenticateLdapUser(
|
||||
loginId: string,
|
||||
password: string,
|
||||
loginIdAttribute: string,
|
||||
userFilter: string,
|
||||
): Promise<LdapUser | undefined> {
|
||||
// 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 this.searchWithAdminBinding(
|
||||
createFilter(`(${loginIdAttribute}=${escapeFilter(loginId)})`, userFilter),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
void this.internalHooks.onLdapLoginSyncFailed({
|
||||
error: e.message,
|
||||
});
|
||||
this.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 this.validUser(user.dn, password);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
this.logger.error('LDAP - Error validating user against LDAP server', {
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
resolveEntryBinaryAttributes(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt binding with the administrator credentials, to test the connection
|
||||
*/
|
||||
async testConnection(): Promise<void> {
|
||||
await this.bindAdmin();
|
||||
}
|
||||
|
||||
/** Schedule a synchronization job based on the interval set in the LDAP config */
|
||||
private scheduleSync(): void {
|
||||
if (!this.config.synchronizationInterval) {
|
||||
throw new ApplicationError('Interval variable has to be defined');
|
||||
}
|
||||
this.syncTimer = setInterval(async () => {
|
||||
await this.runSync('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 runSync(mode: RunningMode): Promise<void> {
|
||||
this.logger.debug(`LDAP - Starting a synchronization run in ${mode} mode`);
|
||||
|
||||
let adUsers: LdapUser[] = [];
|
||||
|
||||
try {
|
||||
adUsers = await this.searchWithAdminBinding(
|
||||
createFilter(`(${this.config.loginIdAttribute}=*)`, this.config.userFilter),
|
||||
);
|
||||
|
||||
this.logger.debug('LDAP - Users return by the query', {
|
||||
users: adUsers,
|
||||
});
|
||||
|
||||
resolveBinaryAttributes(adUsers);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
this.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,
|
||||
);
|
||||
|
||||
this.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 this.internalHooks.onLdapSyncFinished({
|
||||
type: !this.syncTimer ? 'scheduled' : `manual_${mode}`,
|
||||
succeeded: true,
|
||||
users_synced: usersToCreate.length + usersToUpdate.length + usersToDisable.length,
|
||||
error: errorMessage,
|
||||
});
|
||||
|
||||
this.logger.debug('LDAP - Synchronization finished successfully');
|
||||
}
|
||||
|
||||
/** Stop the current job scheduled, if any */
|
||||
stopSync(): void {
|
||||
clearInterval(this.syncTimer);
|
||||
this.syncTimer = 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));
|
||||
}
|
||||
}
|
|
@ -70,7 +70,7 @@ import { EventBusControllerEE } from '@/eventbus/eventBus.controller.ee';
|
|||
import { LicenseController } from '@/license/license.controller';
|
||||
import { setupPushServer, setupPushHandler } from '@/push';
|
||||
import { setupAuthMiddlewares } from './middlewares';
|
||||
import { handleLdapInit, isLdapEnabled } from './Ldap/helpers';
|
||||
import { isLdapEnabled } from './Ldap/helpers';
|
||||
import { AbstractServer } from './AbstractServer';
|
||||
import { PostHogClient } from './posthog';
|
||||
import { eventBus } from './eventbus';
|
||||
|
@ -255,7 +255,9 @@ export class Server extends AbstractServer {
|
|||
}
|
||||
|
||||
if (isLdapEnabled()) {
|
||||
const { LdapController } = await require('@/controllers/ldap.controller');
|
||||
const { LdapService } = await import('@/Ldap/ldap.service');
|
||||
const { LdapController } = await require('@/Ldap/ldap.controller');
|
||||
await Container.get(LdapService).init();
|
||||
controllers.push(LdapController);
|
||||
}
|
||||
|
||||
|
@ -351,8 +353,6 @@ export class Server extends AbstractServer {
|
|||
await Container.get(Queue).init();
|
||||
}
|
||||
|
||||
await handleLdapInit();
|
||||
|
||||
await handleMfaDisable();
|
||||
|
||||
await this.registerControllers(ignoredEndpoints);
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
import { Container } from 'typedi';
|
||||
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { LdapService } from '@/Ldap/ldap.service';
|
||||
import {
|
||||
createLdapUserOnLocalDb,
|
||||
findAndAuthenticateLdapUser,
|
||||
getLdapConfig,
|
||||
getLdapUserRole,
|
||||
getUserByEmail,
|
||||
getAuthIdentityByLdapId,
|
||||
isLdapDisabled,
|
||||
isLdapEnabled,
|
||||
mapLdapAttributesToUser,
|
||||
createLdapAuthIdentity,
|
||||
updateLdapUserOnLocalDb,
|
||||
} from '@/Ldap/helpers';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
export const handleLdapLogin = async (
|
||||
loginId: string,
|
||||
password: string,
|
||||
): Promise<User | undefined> => {
|
||||
if (isLdapDisabled()) return undefined;
|
||||
if (!isLdapEnabled()) return undefined;
|
||||
|
||||
const ldapConfig = await getLdapConfig();
|
||||
const ldapService = Container.get(LdapService);
|
||||
|
||||
if (!ldapConfig.loginEnabled) return undefined;
|
||||
if (!ldapService.config.loginEnabled) return undefined;
|
||||
|
||||
const { loginIdAttribute, userFilter } = ldapConfig;
|
||||
const { loginIdAttribute, userFilter } = ldapService.config;
|
||||
|
||||
const ldapUser = await findAndAuthenticateLdapUser(
|
||||
const ldapUser = await ldapService.findAndAuthenticateLdapUser(
|
||||
loginId,
|
||||
password,
|
||||
loginIdAttribute,
|
||||
|
@ -35,7 +35,7 @@ export const handleLdapLogin = async (
|
|||
|
||||
if (!ldapUser) return undefined;
|
||||
|
||||
const [ldapId, ldapAttributesValues] = mapLdapAttributesToUser(ldapUser, ldapConfig);
|
||||
const [ldapId, ldapAttributesValues] = mapLdapAttributesToUser(ldapUser, ldapService.config);
|
||||
|
||||
const { email: emailAttributeValue } = ldapAttributesValues;
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import { PasswordUtility } from '@/services/password.utility';
|
|||
import { UserManagementMailer } from '@/UserManagement/email';
|
||||
import { PasswordResetRequest } from '@/requests';
|
||||
import { issueCookie } from '@/auth/jwt';
|
||||
import { isLdapEnabled } from '@/Ldap/helpers';
|
||||
import { isSamlCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
|
||||
import { UserService } from '@/services/user.service';
|
||||
import { License } from '@/License';
|
||||
|
@ -111,7 +110,7 @@ export class PasswordResetController {
|
|||
return;
|
||||
}
|
||||
|
||||
if (isLdapEnabled() && ldapIdentity) {
|
||||
if (this.license.isLdapEnabled() && ldapIdentity) {
|
||||
throw new UnprocessableRequestError('forgotPassword.ldapUserPasswordResetUnavailable');
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import Container from 'typedi';
|
||||
import type { SuperAgentTest } from 'supertest';
|
||||
import type { Entry as LdapUser } from 'ldapts';
|
||||
import { Not } from 'typeorm';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
import { Cipher } from 'n8n-core';
|
||||
|
||||
import config from '@/config';
|
||||
import type { Role } from '@db/entities/Role';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
||||
import { LdapManager } from '@/Ldap/LdapManager.ee';
|
||||
import { LdapService } from '@/Ldap/LdapService.ee';
|
||||
import { LdapService } from '@/Ldap/ldap.service';
|
||||
import { saveLdapSynchronization } from '@/Ldap/helpers';
|
||||
import type { LdapConfig } from '@/Ldap/types';
|
||||
import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
|
||||
|
@ -16,8 +17,7 @@ import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '
|
|||
import { randomEmail, randomName, uniqueId } from './../shared/random';
|
||||
import * as testDb from './../shared/testDb';
|
||||
import * as utils from '../shared/utils/';
|
||||
import Container from 'typedi';
|
||||
import { Cipher } from 'n8n-core';
|
||||
|
||||
import { getGlobalMemberRole, getGlobalOwnerRole } from '../shared/db/roles';
|
||||
import { createLdapUser, createUser, getAllUsers, getLdapIdentities } from '../shared/db/users';
|
||||
import { UserRepository } from '@db/repositories/user.repository';
|
||||
|
@ -167,7 +167,7 @@ describe('PUT /ldap/config', () => {
|
|||
|
||||
test('should apply "Convert all LDAP users to email users" strategy when LDAP login disabled', async () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
LdapManager.updateConfig(ldapConfig);
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const member = await createLdapUser({ globalRole: globalMemberRole }, uniqueId());
|
||||
|
||||
|
@ -230,7 +230,7 @@ describe('POST /ldap/sync', () => {
|
|||
lastNameAttribute: 'sn',
|
||||
emailAttribute: 'mail',
|
||||
});
|
||||
LdapManager.updateConfig(ldapConfig);
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
});
|
||||
|
||||
describe('dry mode', () => {
|
||||
|
@ -493,7 +493,7 @@ test('GET /ldap/sync should return paginated synchronizations', async () => {
|
|||
describe('POST /login', () => {
|
||||
const runTest = async (ldapUser: LdapUser) => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
LdapManager.updateConfig(ldapConfig);
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
await setCurrentAuthenticationMethod('ldap');
|
||||
|
||||
|
@ -556,7 +556,7 @@ describe('POST /login', () => {
|
|||
|
||||
test('should allow instance owner to sign in with email/password when LDAP is enabled', async () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
LdapManager.updateConfig(ldapConfig);
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const response = await testServer.authlessAgent
|
||||
.post('/login')
|
||||
|
@ -590,7 +590,7 @@ describe('POST /login', () => {
|
|||
describe('Instance owner should able to delete LDAP users', () => {
|
||||
test("don't transfer workflows", async () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
LdapManager.updateConfig(ldapConfig);
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const member = await createLdapUser({ globalRole: globalMemberRole }, uniqueId());
|
||||
|
||||
|
@ -599,7 +599,7 @@ describe('Instance owner should able to delete LDAP users', () => {
|
|||
|
||||
test('transfer workflows and credentials', async () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
LdapManager.updateConfig(ldapConfig);
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const member = await createLdapUser({ globalRole: globalMemberRole }, uniqueId());
|
||||
|
||||
|
|
|
@ -170,10 +170,10 @@ export const setupTestServer = ({
|
|||
break;
|
||||
|
||||
case 'ldap':
|
||||
const { handleLdapInit } = await import('@/Ldap/helpers');
|
||||
const { LdapController } = await import('@/controllers/ldap.controller');
|
||||
const { LdapService } = await import('@/Ldap/ldap.service');
|
||||
const { LdapController } = await import('@/Ldap/ldap.controller');
|
||||
testServer.license.enable('feat:ldap');
|
||||
await handleLdapInit();
|
||||
await Container.get(LdapService).init();
|
||||
registerController(app, LdapController);
|
||||
break;
|
||||
|
||||
|
|
Loading…
Reference in a new issue