/* eslint-disable @typescript-eslint/no-use-before-define */ 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'; 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 { 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 */ 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 { 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 */ 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 => { return Container.get(RoleService).findGlobalMemberRole(); }; /** * 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 }; }; /** * Retrieve the LDAP configuration (decrypted) form the database */ export const getLdapConfig = async (): Promise => { const configuration = await Container.get(SettingsRepository).findOneByOrFail({ key: LDAP_FEATURE_NAME, }); const configurationData = jsonParse(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 => { await 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 => { const { valid, message } = validateLdapConfigurationSchema(ldapConfig); if (!valid) { throw new Error(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 => { 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) { _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 => { 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 */ export const getAuthIdentityByLdapId = async ( idAttributeValue: string, ): Promise => { return Container.get(AuthIdentityRepository).findOne({ relations: ['user', 'user.globalRole'], where: { providerId: idAttributeValue, providerType: 'ldap', }, }); }; export const getUserByEmail = async (email: string): Promise => { return Container.get(UserRepository).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] => { 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 => { const identities = await Container.get(AuthIdentityRepository).find({ select: ['providerId'], where: { providerType: 'ldap', }, }); return identities.map((i) => i.providerId); }; export const getLdapUsers = async (): Promise => { const identities = await Container.get(AuthIdentityRepository).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 => { 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, ): Promise => { await Container.get(AuthProviderSyncHistoryRepository).save({ ...data, providerType: 'ldap', }); }; /** * Retrieve all LDAP synchronizations in the database */ export const getLdapSynchronizations = async ( page: number, perPage: number, ): Promise => { const _page = Math.abs(page); return Container.get(AuthProviderSyncHistoryRepository).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 Container.get(AuthIdentityRepository).save(AuthIdentity.create(user, ldapId)); }; export const createLdapUserOnLocalDb = async (role: Role, data: Partial, ldapId: string) => { const user = await Container.get(UserRepository).save({ password: randomPassword(), globalRole: role, ...data, }); await createLdapAuthIdentity(user, ldapId); return user; }; export const updateLdapUserOnLocalDb = async (identity: AuthIdentity, data: Partial) => { const userId = identity?.user?.id; if (userId) { await Container.get(UserRepository).update({ id: userId }, data); } }; const deleteAllLdapIdentities = async () => { return Container.get(AuthIdentityRepository).delete({ providerType: 'ldap' }); };