n8n
Version:
n8n Workflow Automation Tool
431 lines • 19.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LdapService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const config_1 = require("@n8n/config");
const constants_1 = require("@n8n/constants");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const decorators_1 = require("@n8n/decorators");
const n8n_core_1 = require("n8n-core");
const n8n_workflow_1 = require("n8n-workflow");
const bad_request_error_1 = require("../../errors/response-errors/bad-request.error");
const internal_server_error_1 = require("../../errors/response-errors/internal-server.error");
const event_service_1 = require("../../events/event.service");
const sso_helpers_1 = require("../../sso.ee/sso-helpers");
const constants_2 = require("./constants");
const helpers_ee_1 = require("./helpers.ee");
let LdapService = class LdapService {
constructor(logger, settingsRepository, cipher, eventService, licenseState) {
this.logger = logger;
this.settingsRepository = settingsRepository;
this.cipher = cipher;
this.eventService = eventService;
this.licenseState = licenseState;
this.metadata = { name: 'ldap', type: 'password' };
this.syncTimer = undefined;
this.userClass = db_1.User;
}
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: ${(0, sso_helpers_1.getCurrentAuthenticationMethod)()})`, error);
}
this.setConfig(ldapConfig);
}
async loadConfig() {
const { value } = await this.settingsRepository.findOneByOrFail({
key: constants_1.LDAP_FEATURE_NAME,
});
const ldapConfig = (0, n8n_workflow_1.jsonParse)(value);
if (ldapConfig.enforceEmailUniqueness === undefined) {
ldapConfig.enforceEmailUniqueness = true;
}
ldapConfig.bindingAdminPassword = await this.cipher.decryptV2(ldapConfig.bindingAdminPassword);
return ldapConfig;
}
async updateConfig(ldapConfig) {
const { valid, message } = (0, helpers_ee_1.validateLdapConfigurationSchema)(ldapConfig);
if (!valid) {
throw new n8n_workflow_1.UnexpectedError(message);
}
if (ldapConfig.loginEnabled && ['saml', 'oidc'].includes((0, sso_helpers_1.getCurrentAuthenticationMethod)())) {
throw new bad_request_error_1.BadRequestError('LDAP cannot be enabled if SSO in enabled');
}
this.setConfig({ ...ldapConfig });
ldapConfig.bindingAdminPassword = await this.cipher.encryptV2(ldapConfig.bindingAdminPassword);
if (!ldapConfig.loginEnabled) {
ldapConfig.synchronizationEnabled = false;
const ldapUsers = await (0, helpers_ee_1.getLdapUsers)();
if (ldapUsers.length) {
await (0, helpers_ee_1.deleteAllLdapIdentities)();
}
}
await this.settingsRepository.update({ key: constants_1.LDAP_FEATURE_NAME }, { value: JSON.stringify(ldapConfig), loadOnStartup: true });
await this.setGlobalLdapConfigVariables(ldapConfig);
}
setConfig(ldapConfig) {
this.config = ldapConfig;
this.client = undefined;
if (this.syncTimer && !this.config.synchronizationEnabled) {
this.stopSync();
}
else if (!this.syncTimer && this.config.synchronizationEnabled) {
this.scheduleSync();
}
else if (this.syncTimer && this.config.synchronizationEnabled) {
this.stopSync();
this.scheduleSync();
}
}
async setGlobalLdapConfigVariables(ldapConfig) {
await this.setLdapLoginEnabled(ldapConfig.loginEnabled);
di_1.Container.get(config_1.GlobalConfig).sso.ldap.loginLabel = ldapConfig.loginLabel;
}
async setLdapLoginEnabled(enabled) {
const currentAuthenticationMethod = (0, sso_helpers_1.getCurrentAuthenticationMethod)();
if (enabled && !(0, sso_helpers_1.isEmailCurrentAuthenticationMethod)() && !(0, sso_helpers_1.isLdapCurrentAuthenticationMethod)()) {
throw new internal_server_error_1.InternalServerError(`Cannot switch LDAP login enabled state when an authentication method other than email or ldap is active (current: ${currentAuthenticationMethod})`);
}
di_1.Container.get(config_1.GlobalConfig).sso.ldap.loginEnabled = enabled;
const targetAuthenticationMethod = !enabled && currentAuthenticationMethod === 'ldap' ? 'email' : currentAuthenticationMethod;
await (0, sso_helpers_1.setCurrentAuthenticationMethod)(enabled ? 'ldap' : targetAuthenticationMethod);
}
async getClient() {
if (this.config === undefined) {
throw new n8n_workflow_1.UnexpectedError('Service cannot be used without setting the property config');
}
if (this.client === undefined) {
if (!this.ldapts) {
this.ldapts = await Promise.resolve().then(() => __importStar(require('ldapts')));
}
const url = (0, helpers_ee_1.formatUrl)(this.config.connectionUrl, this.config.connectionPort, this.config.connectionSecurity);
const ldapOptions = { url };
const tlsOptions = {};
if (this.config.connectionSecurity !== 'none') {
Object.assign(tlsOptions, {
rejectUnauthorized: !this.config.allowUnauthorizedCerts,
});
if (this.config.connectionSecurity === 'tls') {
ldapOptions.tlsOptions = tlsOptions;
}
}
this.client = new this.ldapts.Client(ldapOptions);
if (this.config.connectionSecurity === 'startTls') {
await this.client.startTLS(tlsOptions);
}
}
}
async bindAdmin() {
await this.getClient();
if (this.client) {
await this.client.bind(this.config.bindingAdminDn, this.config.bindingAdminPassword);
}
}
async searchWithAdminBinding(filter) {
await this.bindAdmin();
if (this.client) {
const { searchEntries } = await this.client.search(this.config.baseDn, {
attributes: (0, helpers_ee_1.getMappingAttributes)(this.config),
explicitBufferAttributes: constants_2.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 [];
}
async hasEmailDuplicatesInLdap(email) {
try {
const searchResults = await this.searchWithAdminBinding((0, helpers_ee_1.createFilter)(`(${this.config.emailAttribute}=${(0, helpers_ee_1.escapeFilter)(email)})`, this.config.userFilter));
return searchResults.length > 1;
}
catch (error) {
this.logger.error('LDAP - Error checking for duplicate emails', {
email,
error: error instanceof Error ? error.message : 'Unknown error',
});
return true;
}
}
async validUser(dn, password) {
await this.getClient();
if (this.client) {
await this.client.bind(dn, password);
await this.client.unbind();
}
}
async findAndAuthenticateLdapUser(loginId, password, loginIdAttribute, userFilter) {
let searchResult = [];
try {
searchResult = await this.searchWithAdminBinding((0, helpers_ee_1.createFilter)(`(${loginIdAttribute}=${(0, helpers_ee_1.escapeFilter)(loginId)})`, userFilter));
}
catch (e) {
if (e instanceof Error) {
this.eventService.emit('ldap-login-sync-failed', { error: e.message });
this.logger.error('LDAP - Error during search', { message: e.message });
}
return undefined;
}
if (!searchResult.length) {
return undefined;
}
let user = searchResult.pop();
if (user === undefined) {
user = { dn: '' };
}
try {
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;
}
(0, helpers_ee_1.resolveEntryBinaryAttributes)(user);
return user;
}
async testConnection() {
await this.bindAdmin();
}
scheduleSync() {
if (!this.config.synchronizationInterval) {
throw new n8n_workflow_1.UnexpectedError('Interval variable has to be defined');
}
this.syncTimer = setInterval(async () => {
await this.runSync('live');
}, this.config.synchronizationInterval * 60000);
}
async runSync(mode) {
this.logger.debug(`LDAP - Starting a synchronization run in ${mode} mode`);
let adUsers = [];
try {
adUsers = await this.searchWithAdminBinding((0, helpers_ee_1.createFilter)(`(${this.config.loginIdAttribute}=*)`, this.config.userFilter));
this.logger.debug('LDAP - Users return by the query', {
users: adUsers,
});
(0, helpers_ee_1.resolveBinaryAttributes)(adUsers);
}
catch (e) {
if (e instanceof Error) {
this.logger.error(`LDAP - ${e.message}`);
throw e;
}
}
const startedAt = new Date();
const localAdUsers = await (0, helpers_ee_1.getLdapIds)();
const processableAdUsers = this.config.enforceEmailUniqueness
? this.filterEmailDuplicates(adUsers)
: adUsers;
const { usersToCreate, usersToUpdate, usersToDisable } = this.getUsersToProcess(processableAdUsers, localAdUsers);
const filteredUsersToCreate = usersToCreate.filter(([id, user]) => {
if (!(0, db_1.isValidEmail)(user.email)) {
this.logger.warn(`LDAP - Invalid email format for user ${id}`);
return false;
}
return true;
});
const filteredUsersToUpdate = usersToUpdate.filter(([id, user]) => {
if (!(0, db_1.isValidEmail)(user.email)) {
this.logger.warn(`LDAP - Invalid email format for user ${id}`);
return false;
}
return true;
});
this.logger.debug('LDAP - Users to process', {
created: filteredUsersToCreate.length,
updated: filteredUsersToUpdate.length,
disabled: usersToDisable.length,
});
const endedAt = new Date();
let status = 'success';
let errorMessage = '';
try {
if (mode === 'live') {
await (0, helpers_ee_1.processUsers)(filteredUsersToCreate, filteredUsersToUpdate, usersToDisable);
}
}
catch (error) {
status = 'error';
errorMessage = error instanceof Error ? error.message : String(error);
}
await (0, helpers_ee_1.saveLdapSynchronization)({
startedAt,
endedAt,
created: filteredUsersToCreate.length,
updated: filteredUsersToUpdate.length,
disabled: usersToDisable.length,
scanned: adUsers.length,
runMode: mode,
status,
error: errorMessage,
});
this.eventService.emit('ldap-general-sync-finished', {
type: !this.syncTimer ? 'scheduled' : `manual_${mode}`,
succeeded: status === 'success',
usersSynced: filteredUsersToCreate.length + filteredUsersToUpdate.length + usersToDisable.length,
error: errorMessage,
});
if (status === 'success') {
this.logger.debug('LDAP - Synchronization finished successfully');
}
else {
this.logger.error('LDAP - Synchronization finished with errors', { error: errorMessage });
}
}
stopSync() {
clearInterval(this.syncTimer);
this.syncTimer = undefined;
}
getUsersToProcess(adUsers, localAdUsers) {
return {
usersToCreate: this.getUsersToCreate(adUsers, localAdUsers),
usersToUpdate: this.getUsersToUpdate(adUsers, localAdUsers),
usersToDisable: this.getUsersToDisable(adUsers, localAdUsers),
};
}
filterEmailDuplicates(adUsers) {
const emailCounts = new Map();
for (const adUser of adUsers) {
const email = adUser[this.config.emailAttribute];
if (!email)
continue;
emailCounts.set(email, (emailCounts.get(email) ?? 0) + 1);
}
return adUsers.filter((adUser) => {
const email = adUser[this.config.emailAttribute];
if (email && (emailCounts.get(email) ?? 0) > 1) {
this.logger.warn('LDAP sync skipped entry: multiple LDAP accounts share the same email', {
email,
ldapId: adUser[this.config.ldapIdAttribute],
});
return false;
}
return true;
});
}
getUsersToCreate(remoteAdUsers, localLdapIds) {
return remoteAdUsers
.filter((adUser) => !localLdapIds.includes(adUser[this.config.ldapIdAttribute]))
.map((adUser) => (0, helpers_ee_1.mapLdapUserToDbUser)(adUser, this.config, true));
}
getUsersToUpdate(remoteAdUsers, localLdapIds) {
return remoteAdUsers
.filter((adUser) => localLdapIds.includes(adUser[this.config.ldapIdAttribute]))
.map((adUser) => (0, helpers_ee_1.mapLdapUserToDbUser)(adUser, this.config));
}
getUsersToDisable(remoteAdUsers, localLdapIds) {
const remoteAdUserIds = remoteAdUsers.map((adUser) => adUser[this.config.ldapIdAttribute]);
return localLdapIds.filter((user) => !remoteAdUserIds.includes(user));
}
async handleLogin(loginId, password) {
if (!this.licenseState.isLdapLicensed())
return undefined;
if (!this.config.loginEnabled)
return undefined;
const { loginIdAttribute, userFilter } = this.config;
const ldapUser = await this.findAndAuthenticateLdapUser(loginId, password, loginIdAttribute, userFilter);
if (!ldapUser)
return undefined;
const [ldapId, ldapAttributesValues] = (0, helpers_ee_1.mapLdapAttributesToUser)(ldapUser, this.config);
const { email: emailAttributeValue } = ldapAttributesValues;
if (!ldapId || !emailAttributeValue)
return undefined;
const ldapAuthIdentity = await (0, helpers_ee_1.getAuthIdentityByLdapId)(ldapId);
if (!ldapAuthIdentity) {
if (this.config.enforceEmailUniqueness) {
const hasDuplicates = await this.hasEmailDuplicatesInLdap(emailAttributeValue);
if (hasDuplicates) {
this.logger.warn('LDAP login blocked: Multiple LDAP accounts share the same email', {
email: emailAttributeValue,
ldapId,
});
return undefined;
}
}
const emailUser = await (0, helpers_ee_1.getUserByEmail)(emailAttributeValue);
if (emailUser && emailUser.email === emailAttributeValue) {
const identity = await (0, helpers_ee_1.createLdapAuthIdentity)(emailUser, ldapId);
await (0, helpers_ee_1.updateLdapUserOnLocalDb)(identity, ldapAttributesValues);
}
else {
const user = await (0, helpers_ee_1.createLdapUserOnLocalDb)(ldapAttributesValues, ldapId);
di_1.Container.get(event_service_1.EventService).emit('user-signed-up', {
user,
userType: 'ldap',
wasDisabledLdapUser: false,
});
return user;
}
}
else {
if (ldapAuthIdentity.user) {
if (ldapAuthIdentity.user.disabled)
return undefined;
await (0, helpers_ee_1.updateLdapUserOnLocalDb)(ldapAuthIdentity, ldapAttributesValues);
}
}
return (await (0, helpers_ee_1.getUserByLdapId)(ldapId)) ?? undefined;
}
};
exports.LdapService = LdapService;
exports.LdapService = LdapService = __decorate([
(0, decorators_1.AuthHandler)(),
__metadata("design:paramtypes", [backend_common_1.Logger,
db_1.SettingsRepository,
n8n_core_1.Cipher,
event_service_1.EventService,
backend_common_1.LicenseState])
], LdapService);
//# sourceMappingURL=ldap.service.ee.js.map