perf(core): Lazyload LDAP during bootup (#15907)

This commit is contained in:
Iván Ovejero
2025-06-02 15:04:54 +02:00
committed by GitHub
parent 62d70f5225
commit 31b6f32a36
5 changed files with 76 additions and 84 deletions

View File

@@ -1,2 +1 @@
export * from './methods/email';
export * from './methods/ldap';

View File

@@ -1,69 +0,0 @@
import type { User } from '@n8n/db';
import { Container } from '@n8n/di';
import { EventService } from '@/events/event.service';
import {
createLdapUserOnLocalDb,
getUserByEmail,
getAuthIdentityByLdapId,
isLdapEnabled,
mapLdapAttributesToUser,
createLdapAuthIdentity,
updateLdapUserOnLocalDb,
} from '@/ldap.ee/helpers.ee';
import { LdapService } from '@/ldap.ee/ldap.service.ee';
export const handleLdapLogin = async (
loginId: string,
password: string,
): Promise<User | undefined> => {
if (!isLdapEnabled()) return undefined;
const ldapService = Container.get(LdapService);
if (!ldapService.config.loginEnabled) return undefined;
const { loginIdAttribute, userFilter } = ldapService.config;
const ldapUser = await ldapService.findAndAuthenticateLdapUser(
loginId,
password,
loginIdAttribute,
userFilter,
);
if (!ldapUser) return undefined;
const [ldapId, ldapAttributesValues] = mapLdapAttributesToUser(ldapUser, ldapService.config);
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 user = await createLdapUserOnLocalDb(ldapAttributesValues, ldapId);
Container.get(EventService).emit('user-signed-up', {
user,
userType: 'ldap',
wasDisabledLdapUser: 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;
};

View File

@@ -10,6 +10,7 @@ import * as auth from '@/auth';
import { AuthService } from '@/auth/auth.service';
import config from '@/config';
import { EventService } from '@/events/event.service';
import { LdapService } from '@/ldap.ee/ldap.service.ee';
import { License } from '@/license';
import { MfaService } from '@/mfa/mfa.service';
import { PostHogClient } from '@/posthog';
@@ -32,6 +33,7 @@ describe('AuthController', () => {
mockInstance(UserRepository);
mockInstance(PostHogClient);
mockInstance(License);
const ldapService = mockInstance(LdapService);
const controller = Container.get(AuthController);
const userService = Container.get(UserService);
const authService = Container.get(AuthService);
@@ -65,7 +67,7 @@ describe('AuthController', () => {
mockedAuth.handleEmailLogin.mockResolvedValue(member);
mockedAuth.handleLdapLogin.mockResolvedValue(member);
ldapService.handleLdapLogin.mockResolvedValue(member);
config.set('userManagement.authenticationMethod', 'ldap');
@@ -79,7 +81,8 @@ describe('AuthController', () => {
body.emailOrLdapLoginId,
body.password,
);
expect(mockedAuth.handleLdapLogin).toHaveBeenCalledWith(
expect(ldapService.handleLdapLogin).toHaveBeenCalledWith(
body.emailOrLdapLoginId,
body.password,
);

View File

@@ -3,10 +3,11 @@ import { Logger } from '@n8n/backend-common';
import type { User, PublicUser } from '@n8n/db';
import { UserRepository } from '@n8n/db';
import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
import { Container } from '@n8n/di';
import { isEmail } from 'class-validator';
import { Response } from 'express';
import { handleEmailLogin, handleLdapLogin } from '@/auth';
import { handleEmailLogin } from '@/auth';
import { AuthService } from '@/auth/auth.service';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { AuthError } from '@/errors/response-errors/auth.error';
@@ -73,7 +74,8 @@ export class AuthController {
user = preliminaryUser;
usedAuthenticationMethod = 'email';
} else {
user = await handleLdapLogin(emailOrLdapLoginId, password);
const { LdapService } = await import('@/ldap.ee/ldap.service.ee');
user = await Container.get(LdapService).handleLdapLogin(emailOrLdapLoginId, password);
}
} else {
user = await handleEmailLogin(emailOrLdapLoginId, password);

View File

@@ -3,7 +3,7 @@ import type { LdapConfig } from '@n8n/constants';
import { LDAP_FEATURE_NAME } from '@n8n/constants';
import { SettingsRepository } from '@n8n/db';
import type { User, RunningMode, SyncStatus } from '@n8n/db';
import { Service } from '@n8n/di';
import { Service, Container } from '@n8n/di';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { QueryFailedError } from '@n8n/typeorm';
import type { Entry as LdapUser, ClientOptions } from 'ldapts';
@@ -17,14 +17,13 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { EventService } from '@/events/event.service';
import {
getCurrentAuthenticationMethod,
isEmailCurrentAuthenticationMethod,
isLdapCurrentAuthenticationMethod,
setCurrentAuthenticationMethod,
} from '@/sso.ee/sso-helpers';
import { BINARY_AD_ATTRIBUTES, LDAP_LOGIN_ENABLED, LDAP_LOGIN_LABEL } from './constants';
import {
createLdapUserOnLocalDb,
getUserByEmail,
getAuthIdentityByLdapId,
isLdapEnabled,
mapLdapAttributesToUser,
createLdapAuthIdentity,
updateLdapUserOnLocalDb,
createFilter,
deleteAllLdapIdentities,
escapeFilter,
@@ -38,7 +37,15 @@ import {
resolveEntryBinaryAttributes,
saveLdapSynchronization,
validateLdapConfigurationSchema,
} from './helpers.ee';
} from '@/ldap.ee/helpers.ee';
import {
getCurrentAuthenticationMethod,
isEmailCurrentAuthenticationMethod,
isLdapCurrentAuthenticationMethod,
setCurrentAuthenticationMethod,
} from '@/sso.ee/sso-helpers';
import { BINARY_AD_ATTRIBUTES, LDAP_LOGIN_ENABLED, LDAP_LOGIN_LABEL } from './constants';
@Service()
export class LdapService {
@@ -432,4 +439,54 @@ export class LdapService {
const remoteAdUserIds = remoteAdUsers.map((adUser) => adUser[this.config.ldapIdAttribute]);
return localLdapIds.filter((user) => !remoteAdUserIds.includes(user));
}
async handleLdapLogin(loginId: string, password: string): Promise<User | undefined> {
if (!isLdapEnabled()) 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] = mapLdapAttributesToUser(ldapUser, this.config);
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 user = await createLdapUserOnLocalDb(ldapAttributesValues, ldapId);
Container.get(EventService).emit('user-signed-up', {
user,
userType: 'ldap',
wasDisabledLdapUser: 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;
}
}