UNPKG

@sync-in/server

Version:

The secure, open-source platform for file storage, sharing, collaboration, and sync

326 lines (325 loc) 15.2 kB
/* * Copyright (C) 2012-2025 Johan Legrand <johan.legrand@sync-in.com> * This file is part of Sync-in | The open source file sync and share solution * See the LICENSE file for licensing details */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "AuthMethodLdapService", { enumerable: true, get: function() { return AuthMethodLdapService; } }); const _common = require("@nestjs/common"); const _ldapts = require("ldapts"); const _appconstants = require("../../../app.constants"); const _user = require("../../../applications/users/constants/user"); const _adminusersmanagerservice = require("../../../applications/users/services/admin-users-manager.service"); const _usersmanagerservice = require("../../../applications/users/services/users-manager.service"); const _functions = require("../../../common/functions"); const _configenvironment = require("../../../configuration/config.environment"); const _authldap = require("../../constants/auth-ldap"); function _ts_decorate(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; } function _ts_metadata(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } let AuthMethodLdapService = class AuthMethodLdapService { async validateUser(login, password, ip, scope) { // Find user from his login or email let user = await this.usersManager.findUser(this.dbLogin(login), false); if (user) { if (user.isGuest) { // Allow guests to be authenticated from db and check if the current user is defined as active return this.usersManager.logUser(user, password, ip); } if (!user.isActive) { this.logger.error(`${this.validateUser.name} - user *${user.login}* is locked`); throw new _common.HttpException('Account locked', _common.HttpStatus.FORBIDDEN); } } // If a user was found, use the stored login. This allows logging in with an email. const entry = await this.checkAuth(user?.login || login, password); if (entry === false) { // LDAP auth failed if (user) { let authSuccess = false; if (scope) { // Try user app password authSuccess = await this.usersManager.validateAppPassword(user, password, ip, scope); } this.usersManager.updateAccesses(user, ip, authSuccess).catch((e)=>this.logger.error(`${this.validateUser.name} : ${e}`)); if (authSuccess) { // Logged with app password return user; } } return null; } else if (!entry[this.ldapConfig.attributes.login] || !entry[this.ldapConfig.attributes.email]) { this.logger.error(`${this.validateUser.name} - required ldap fields are missing : [${this.ldapConfig.attributes.login}, ${this.ldapConfig.attributes.email}] => (${JSON.stringify(entry)})`); return null; } const identity = this.createIdentity(entry, password); user = await this.updateOrCreateUser(identity, user); this.usersManager.updateAccesses(user, ip, true).catch((e)=>this.logger.error(`${this.validateUser.name} : ${e}`)); return user; } async checkAuth(login, password) { const ldapLogin = this.buildLdapLogin(login); // AD: bind directly with the user input (UPN or DOMAIN\user) // Generic LDAP: build DN from login attribute + baseDN const bindUserDN = this.isAD ? ldapLogin : `${this.ldapConfig.attributes.login}=${ldapLogin},${this.ldapConfig.baseDN}`; let client; let error; for (const s of this.ldapConfig.servers){ client = new _ldapts.Client({ ...this.clientOptions, url: s }); try { await client.bind(bindUserDN, password); return await this.checkAccess(ldapLogin, client); } catch (e) { if (e.errors?.length) { for (const err of e.errors){ this.logger.warn(`${this.checkAuth.name} - ${ldapLogin} : ${err}`); error = err; } } else { error = e; this.logger.warn(`${this.checkAuth.name} - ${ldapLogin} : ${e}`); } if (error instanceof _ldapts.InvalidCredentialsError) { return false; } } finally{ await client.unbind(); } } if (error && _appconstants.CONNECT_ERROR_CODE.has(error.code)) { throw new _common.HttpException('Authentication service error', _common.HttpStatus.INTERNAL_SERVER_ERROR); } return false; } async checkAccess(login, client) { const searchFilter = this.buildUserFilter(login, this.ldapConfig.filter); try { const { searchEntries } = await client.search(this.ldapConfig.baseDN, { scope: 'sub', filter: searchFilter, attributes: _authldap.ALL_LDAP_ATTRIBUTES }); if (searchEntries.length === 0) { this.logger.debug(`${this.checkAccess.name} - search filter : ${searchFilter}`); this.logger.warn(`${this.checkAccess.name} - no LDAP entry found for : ${login}`); return false; } if (searchEntries.length > 1) { this.logger.warn(`${this.checkAccess.name} - multiple LDAP entries found for : ${login}, using first one`); } // Always return the first valid entry return this.convertToLdapUserEntry(searchEntries[0]); } catch (e) { this.logger.debug(`${this.checkAccess.name} - search filter : ${searchFilter}`); this.logger.error(`${this.checkAccess.name} - ${login} : ${e}`); return false; } } async updateOrCreateUser(identity, user) { if (user === null) { // Create const createdUser = await this.adminUsersManager.createUserOrGuest(identity, identity.role); const freshUser = await this.usersManager.fromUserId(createdUser.id); if (!freshUser) { this.logger.error(`${this.updateOrCreateUser.name} - user was not found : ${createdUser.login} (${createdUser.id})`); throw new _common.HttpException('User not found', _common.HttpStatus.NOT_FOUND); } return freshUser; } if (identity.login !== user.login) { this.logger.error(`${this.updateOrCreateUser.name} - user login mismatch : ${identity.login} !== ${user.login}`); throw new _common.HttpException('Account matching error', _common.HttpStatus.FORBIDDEN); } // Update: check if user information has changed const identityHasChanged = Object.fromEntries((await Promise.all(Object.keys(identity).map(async (key)=>{ if (key === 'password') { const isSame = await (0, _functions.comparePassword)(identity[key], user.password); return isSame ? null : [ key, identity[key] ]; } return identity[key] !== user[key] ? [ key, identity[key] ] : null; }))).filter(Boolean)); if (Object.keys(identityHasChanged).length > 0) { try { if (identityHasChanged?.role != null) { if (user.role === _user.USER_ROLE.ADMINISTRATOR && !this.ldapConfig.adminGroup) { // Prevent removing the admin role when adminGroup was removed or not defined delete identityHasChanged.role; } } // Update user properties await this.adminUsersManager.updateUserOrGuest(user.id, identityHasChanged); // Extra stuff if (identityHasChanged?.password) { delete identityHasChanged.password; } Object.assign(user, identityHasChanged); if ('lastName' in identityHasChanged || 'firstName' in identityHasChanged) { // Force fullName update in the current user model user.setFullName(true); } } catch (e) { this.logger.warn(`${this.updateOrCreateUser.name} - unable to update user *${user.login}* : ${e}`); } } return user; } convertToLdapUserEntry(entry) { for (const attr of _authldap.ALL_LDAP_ATTRIBUTES){ if (attr === _authldap.LDAP_COMMON_ATTR.MEMBER_OF && entry[attr]) { entry[attr] = (Array.isArray(entry[attr]) ? entry[attr] : entry[attr] ? [ entry[attr] ] : []).filter((v)=>typeof v === 'string').map((v)=>v.match(/cn\s*=\s*([^,]+)/i)?.[1]?.trim()).filter(Boolean); continue; } if (Array.isArray(entry[attr])) { // Keep only the first value for all other attributes (e.g., email) entry[attr] = entry[attr].length > 0 ? entry[attr][0] : null; } } return entry; } createIdentity(entry, password) { const isAdmin = typeof this.ldapConfig.adminGroup === 'string' && this.ldapConfig.adminGroup && entry[_authldap.LDAP_COMMON_ATTR.MEMBER_OF]?.includes(this.ldapConfig.adminGroup); return { login: this.dbLogin(entry[this.ldapConfig.attributes.login]), email: entry[this.ldapConfig.attributes.email], password: password, role: isAdmin ? _user.USER_ROLE.ADMINISTRATOR : _user.USER_ROLE.USER, ...this.getFirstNameAndLastName(entry) }; } getFirstNameAndLastName(entry) { // 1) Prefer structured attributes if (entry.sn && entry.givenName) { return { firstName: entry.givenName, lastName: entry.sn }; } // 2) Fallback to displayName if available if (entry.displayName && entry.displayName.trim()) { return (0, _functions.splitFullName)(entry.displayName); } // 3) Fallback to cn if (entry.cn && entry.cn.trim()) { return (0, _functions.splitFullName)(entry.cn); } // 4) Nothing usable return { firstName: '', lastName: '' }; } dbLogin(login) { if (login.includes('\\')) { return login.split('\\').slice(-1)[0]; } return login; } buildLdapLogin(login) { if (this.ldapConfig.attributes.login === _authldap.LDAP_LOGIN_ATTR.UPN) { if (this.ldapConfig.upnSuffix && !login.includes('@')) { return `${login}@${this.ldapConfig.upnSuffix}`; } } else if (this.ldapConfig.attributes.login === _authldap.LDAP_LOGIN_ATTR.SAM) { if (this.ldapConfig.netbiosName && !login.includes('\\')) { return `${this.ldapConfig.netbiosName}\\${login}`; } } return login; } buildUserFilter(login, extraFilter) { // Build a safe LDAP filter to search for a user. // Important: - Values passed to EqualityFilter are auto-escaped by ldapts // - extraFilter is appended as-is (assumed trusted configuration) // Output: (&(|(userPrincipalName=john.doe@sync-in.com)(sAMAccountName=john.doe)(cn=john.doe)(uid=john.doe)(mail=john.doe@sync-in.com))(*extraFilter*)) // Handle the case where the sAMAccountName is provided in domain-qualified format (e.g., SYNC_IN\\user) // Note: sAMAccountName is always stored without the domain in Active Directory. const dbLogin = this.dbLogin(login); const or = new _ldapts.OrFilter({ filters: this.isAD ? [ new _ldapts.EqualityFilter({ attribute: _authldap.LDAP_LOGIN_ATTR.SAM, value: dbLogin }), new _ldapts.EqualityFilter({ attribute: _authldap.LDAP_LOGIN_ATTR.UPN, value: dbLogin }), new _ldapts.EqualityFilter({ attribute: _authldap.LDAP_LOGIN_ATTR.MAIL, value: dbLogin }) ] : [ new _ldapts.EqualityFilter({ attribute: _authldap.LDAP_LOGIN_ATTR.UID, value: dbLogin }), new _ldapts.EqualityFilter({ attribute: _authldap.LDAP_LOGIN_ATTR.CN, value: dbLogin }), new _ldapts.EqualityFilter({ attribute: _authldap.LDAP_LOGIN_ATTR.MAIL, value: dbLogin }) ] }); // Convert to LDAP filter string let filterString = new _ldapts.AndFilter({ filters: [ or ] }).toString(); // Optionally append an extra filter from config (trusted source) if (extraFilter && extraFilter.trim()) { filterString = `(&${filterString}${extraFilter})`; } return filterString; } constructor(usersManager, adminUsersManager){ this.usersManager = usersManager; this.adminUsersManager = adminUsersManager; this.logger = new _common.Logger(AuthMethodLdapService.name); this.ldapConfig = _configenvironment.configuration.auth.ldap; this.isAD = this.ldapConfig.attributes.login === _authldap.LDAP_LOGIN_ATTR.SAM || this.ldapConfig.attributes.login === _authldap.LDAP_LOGIN_ATTR.UPN; this.clientOptions = { timeout: 6000, connectTimeout: 6000, url: '' }; } }; AuthMethodLdapService = _ts_decorate([ (0, _common.Injectable)(), _ts_metadata("design:type", Function), _ts_metadata("design:paramtypes", [ typeof _usersmanagerservice.UsersManager === "undefined" ? Object : _usersmanagerservice.UsersManager, typeof _adminusersmanagerservice.AdminUsersManager === "undefined" ? Object : _adminusersmanagerservice.AdminUsersManager ]) ], AuthMethodLdapService); //# sourceMappingURL=auth-method-ldap.service.js.map