From 31b6f32a363da2b199eff135f4f1fbe227ef47c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 2 Jun 2025 15:04:54 +0200 Subject: [PATCH] perf(core): Lazyload LDAP during bootup (#15907) --- packages/cli/src/auth/index.ts | 1 - packages/cli/src/auth/methods/ldap.ts | 69 ----------------- .../__tests__/auth.controller.test.ts | 7 +- .../cli/src/controllers/auth.controller.ts | 6 +- packages/cli/src/ldap.ee/ldap.service.ee.ts | 77 ++++++++++++++++--- 5 files changed, 76 insertions(+), 84 deletions(-) delete mode 100644 packages/cli/src/auth/methods/ldap.ts diff --git a/packages/cli/src/auth/index.ts b/packages/cli/src/auth/index.ts index 29d853c30e0..7b28bab755e 100644 --- a/packages/cli/src/auth/index.ts +++ b/packages/cli/src/auth/index.ts @@ -1,2 +1 @@ export * from './methods/email'; -export * from './methods/ldap'; diff --git a/packages/cli/src/auth/methods/ldap.ts b/packages/cli/src/auth/methods/ldap.ts deleted file mode 100644 index ecb2b7a0d78..00000000000 --- a/packages/cli/src/auth/methods/ldap.ts +++ /dev/null @@ -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 => { - 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; -}; diff --git a/packages/cli/src/controllers/__tests__/auth.controller.test.ts b/packages/cli/src/controllers/__tests__/auth.controller.test.ts index e672b4a1adb..7393a10f452 100644 --- a/packages/cli/src/controllers/__tests__/auth.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/auth.controller.test.ts @@ -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, ); diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index a313b36a7a3..3c44fe296a6 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -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); diff --git a/packages/cli/src/ldap.ee/ldap.service.ee.ts b/packages/cli/src/ldap.ee/ldap.service.ee.ts index 0f94b6b5e6b..c32ef8a44dc 100644 --- a/packages/cli/src/ldap.ee/ldap.service.ee.ts +++ b/packages/cli/src/ldap.ee/ldap.service.ee.ts @@ -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 { + 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; + } }