From 91dfd7e0b7e3486e52cdc04039ad96deddcc0de9 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:58:52 +1000 Subject: [PATCH] [PM-13007] Replace ldapjs with ldapts (#641) --- package-lock.json | 164 ++++++------- package.json | 3 +- src/services/ldap-directory.service.ts | 326 ++++++++++++------------- 3 files changed, 228 insertions(+), 265 deletions(-) diff --git a/package-lock.json b/package-lock.json index 35e42e338..c4c879d26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "keytar": "7.9.0", - "ldapjs": "2.3.3", + "ldapts": "7.2.1", "lowdb": "1.0.0", "ngx-toastr": "17.0.2", "node-fetch": "2.7.0", @@ -53,7 +53,6 @@ "@ngtools/webpack": "17.3.10", "@types/inquirer": "8.2.10", "@types/jest": "29.5.13", - "@types/ldapjs": "2.2.5", "@types/lowdb": "1.0.15", "@types/node": "20.16.10", "@types/node-fetch": "2.6.11", @@ -5124,6 +5123,15 @@ "node": ">= 10" } }, + "node_modules/@types/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5427,16 +5435,6 @@ "@types/node": "*" } }, - "node_modules/@types/ldapjs": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.5.tgz", - "integrity": "sha512-Lv/nD6QDCmcT+V1vaTRnEKE8UgOilVv5pHcQuzkU1LcRe4mbHHuUo/KHi0LKrpdHhQY8FJzryF38fcVdeUIrzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/lodash": { "version": "4.17.10", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz", @@ -5479,7 +5477,6 @@ "version": "20.16.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -6486,12 +6483,6 @@ "node": ">=6.5" } }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "license": "MIT" - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -7163,7 +7154,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.8" } @@ -7490,18 +7483,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/backoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", - "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", - "license": "MIT", - "dependencies": { - "precond": "0.2" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -8999,6 +8980,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { @@ -11553,10 +11535,12 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, "engines": [ "node >=0.6.0" ], - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -15166,36 +15150,68 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/ldap-filter": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", - "integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==", + "node_modules/ldapts": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-7.2.1.tgz", + "integrity": "sha512-2NSA9drjHdRiApF+TO18c+Hy/uyBLs96OS6Gia4+dPQWPxvqDbu3Ji2beCbNCXTvvgxDj4cLZ0WoOZLt5ojfAg==", "license": "MIT", "dependencies": { - "assert-plus": "^1.0.0" + "@types/asn1": ">=0.2.4", + "asn1": "~0.2.6", + "debug": "~4.3.7", + "strict-event-emitter-types": "~2.0.0", + "uuid": "~10.0.0", + "whatwg-url": "~14.0.0" }, "engines": { - "node": ">=0.8" + "node": ">=18" } }, - "node_modules/ldapjs": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz", - "integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "node_modules/ldapts/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ldapts/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "license": "MIT", "dependencies": { - "abstract-logging": "^2.0.0", - "asn1": "^0.2.4", - "assert-plus": "^1.0.0", - "backoff": "^2.5.0", - "ldap-filter": "^0.3.3", - "once": "^1.4.0", - "vasync": "^2.2.0", - "verror": "^1.8.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=10.13.0" + "node": ">=18" + } + }, + "node_modules/ldapts/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/ldapts/node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/less": { @@ -18275,14 +18291,6 @@ "node": ">=10" } }, - "node_modules/precond": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", - "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -20327,6 +20335,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==", + "license": "ISC" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -21480,7 +21494,6 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -21725,37 +21738,13 @@ "node": ">= 0.8" } }, - "node_modules/vasync": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", - "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "verror": "1.10.0" - } - }, - "node_modules/vasync/node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -22311,7 +22300,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/package.json b/package.json index ddd602506..248c66706 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "@ngtools/webpack": "17.3.10", "@types/inquirer": "8.2.10", "@types/jest": "29.5.13", - "@types/ldapjs": "2.2.5", "@types/lowdb": "1.0.15", "@types/node": "20.16.10", "@types/node-fetch": "2.6.11", @@ -164,7 +163,7 @@ "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "keytar": "7.9.0", - "ldapjs": "2.3.3", + "ldapts": "7.2.1", "lowdb": "1.0.0", "ngx-toastr": "17.0.2", "node-fetch": "2.7.0", diff --git a/src/services/ldap-directory.service.ts b/src/services/ldap-directory.service.ts index 1e6f31f73..de558bfd7 100644 --- a/src/services/ldap-directory.service.ts +++ b/src/services/ldap-directory.service.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; -import { checkServerIdentity, PeerCertificate } from "tls"; +import * as tls from "tls"; -import * as ldap from "ldapjs"; +import * as ldapts from "ldapts"; import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; @@ -19,7 +19,7 @@ import { IDirectoryService } from "./directory.service"; const UserControlAccountDisabled = 2; export class LdapDirectoryService implements IDirectoryService { - private client: ldap.Client; + private client: ldapts.Client; private dirConfig: LdapConfiguration; private syncConfig: SyncConfiguration; @@ -48,21 +48,25 @@ export class LdapDirectoryService implements IDirectoryService { await this.bind(); let users: UserEntry[]; - if (this.syncConfig.users) { - users = await this.getUsers(force, test); - } - let groups: GroupEntry[]; - if (this.syncConfig.groups) { - let groupForce = force; - if (!groupForce && users != null) { - const activeUsers = users.filter((u) => !u.deleted && !u.disabled); - groupForce = activeUsers.length > 0; + + try { + if (this.syncConfig.users) { + users = await this.getUsers(force, test); + } + + if (this.syncConfig.groups) { + let groupForce = force; + if (!groupForce && users != null) { + const activeUsers = users.filter((u) => !u.deleted && !u.disabled); + groupForce = activeUsers.length > 0; + } + groups = await this.getGroups(groupForce); } - groups = await this.getGroups(groupForce); + } finally { + await this.client.unbind(); } - await this.unbind(); return [groups, users]; } @@ -101,10 +105,7 @@ export class LdapDirectoryService implements IDirectoryService { const deletedPath = this.makeSearchPath("CN=Deleted Objects"); this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter); - const delControl = new (ldap as any).Control({ - type: "1.2.840.113556.1.4.417", - criticality: true, - }); + const delControl = new ldapts.Control("1.2.840.113556.1.4.417", { critical: true }); const deletedUsers = await this.search( deletedPath, deletedFilter, @@ -120,7 +121,7 @@ export class LdapDirectoryService implements IDirectoryService { private buildUser(searchEntry: any, deleted: boolean): UserEntry { const user = new UserEntry(); - user.referenceId = searchEntry.objectName; + user.referenceId = this.getReferenceId(searchEntry); user.deleted = deleted; if (user.referenceId == null) { @@ -172,7 +173,7 @@ export class LdapDirectoryService implements IDirectoryService { let groupSearchEntries: any[] = []; const initialSearchGroupIds = await this.search(path, filter, (se: any) => { groupSearchEntries.push(se); - return se.objectName; + return this.getReferenceId(se); }); if (searchSinceRevision && initialSearchGroupIds.length === 0) { @@ -188,7 +189,7 @@ export class LdapDirectoryService implements IDirectoryService { const userPath = this.makeSearchPath(this.syncConfig.userPath); const userIdMap = new Map(); await this.search(userPath, userFilter, (se: any) => { - userIdMap.set(se.objectName, this.getExternalId(se, se.objectName)); + userIdMap.set(this.getReferenceId(se), this.getExternalId(se, this.getReferenceId(se))); return se; }); @@ -204,7 +205,7 @@ export class LdapDirectoryService implements IDirectoryService { private buildGroup(searchEntry: any, userMap: Map) { const group = new GroupEntry(); - group.referenceId = searchEntry.objectName; + group.referenceId = this.getReferenceId(searchEntry); if (group.referenceId == null) { return null; } @@ -220,7 +221,7 @@ export class LdapDirectoryService implements IDirectoryService { return null; } - const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute); + const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute); if (members != null) { for (const memDn of members) { if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) { @@ -234,15 +235,26 @@ export class LdapDirectoryService implements IDirectoryService { return group; } - private getExternalId(searchEntry: any, referenceId: string) { - const attrObj = this.getAttrObj(searchEntry, "objectGUID"); - if (attrObj != null && attrObj._vals != null && attrObj._vals.length > 0) { - return this.bufToGuid(attrObj._vals[0]); + /** + * The externalId is the "objectGUID" property if present (a unique identifier used by Active Directory), + * otherwise it falls back to the provided referenceId. + */ + private getExternalId(searchEntry: ldapts.Entry, referenceId: string) { + const attr = this.getAttr(searchEntry, "objectGUID"); + if (attr != null) { + return this.bufToGuid(attr); } else { return referenceId; } } + /** + * Gets the object's reference id (dn) + */ + private getReferenceId(entry: ldapts.Entry): string { + return entry.dn; + } + private buildBaseFilter(objectClass: string, subFilter: string): string { let filter = this.buildObjectClassFilter(objectClass); if (subFilter != null && subFilter.trim() !== "") { @@ -281,42 +293,48 @@ export class LdapDirectoryService implements IDirectoryService { return null; } - private getAttrObj(searchEntry: any, attr: string): any { - if (searchEntry == null || searchEntry.attributes == null) { + /** + */ + + /** + * Get all values for an ldap attribute + * @param searchEntry The ldap entry + * @param attr An attribute name on the ldap entry + * @returns An array containing all values of the attribute, or null if there are no values + */ + private getAttrVals( + searchEntry: ldapts.Entry, + attr: string, + ): T[] | null { + if (searchEntry == null || searchEntry[attr] == null) { return null; } - const attrs = searchEntry.attributes.filter((a: any) => a.type === attr); - if ( - attrs == null || - attrs.length === 0 || - attrs[0].vals == null || - attrs[0].vals.length === 0 - ) { - return null; + const vals = searchEntry[attr]; + if (!Array.isArray(vals)) { + return [vals] as T[]; } - return attrs[0]; + return vals as T[]; } - private getAttrVals(searchEntry: any, attr: string): string[] { - const obj = this.getAttrObj(searchEntry, attr); - if (obj == null) { - return null; - } - return obj.vals; - } - - private getAttr(searchEntry: any, attr: string): string { + /** + * Get the first value for an ldap attribute + * @param searchEntry The ldap entry + * @param attr An attribute name on the ldap entry + * @returns The first value of the attribute, or null if there is not at least 1 value + */ + private getAttr(searchEntry: ldapts.Entry, attr: string): T { const vals = this.getAttrVals(searchEntry, attr); - if (vals == null) { + if (vals == null || vals.length < 1) { return null; } - return vals[0]; + + return vals[0] as T; } private entryDisabled(searchEntry: any): boolean { - const c = this.getAttr(searchEntry, "userAccountControl"); + const c = this.getAttr(searchEntry, "userAccountControl"); if (c != null) { try { const control = parseInt(c, null); @@ -333,145 +351,103 @@ export class LdapDirectoryService implements IDirectoryService { private async search( path: string, filter: string, - processEntry: (searchEntry: any) => T, - controls: ldap.Control[] = [], + processEntry: (searchEntry: ldapts.Entry) => T, + controls: ldapts.Control[] = [], ): Promise { - const options: ldap.SearchOptions = { + const options: ldapts.SearchOptions = { filter: filter, scope: "sub", paged: this.dirConfig.pagedSearch, }; - const entries: T[] = []; - return new Promise((resolve, reject) => { - this.client.search(path, options, controls, (err, res) => { - if (err != null) { - reject(err); - return; - } - - res.on("error", (resErr) => { - reject(resErr); - }); - - res.on("searchEntry", (entry) => { - const e = processEntry(entry); - if (e != null) { - entries.push(e); - } - }); - - res.on("end", (result) => { - resolve(entries); - }); - }); - }); + const { searchEntries } = await this.client.search(path, options, controls); + return searchEntries.map((e) => processEntry(e)).filter((e) => e != null); } private async bind(): Promise { - return new Promise((resolve, reject) => { - if (this.dirConfig.hostname == null || this.dirConfig.port == null) { - reject(this.i18nService.t("dirConfigIncomplete")); - return; - } - const protocol = "ldap" + (this.dirConfig.ssl && !this.dirConfig.startTls ? "s" : ""); - const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port; - const options: ldap.ClientOptions = { - url: url.trim().toLowerCase(), - }; + if (this.dirConfig.hostname == null || this.dirConfig.port == null) { + throw new Error(this.i18nService.t("dirConfigIncomplete")); + } - const tlsOptions: any = {}; - if (this.dirConfig.ssl) { - if (this.dirConfig.sslAllowUnauthorized) { - tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized; - } - if (!this.dirConfig.startTls) { - if ( - this.dirConfig.sslCaPath != null && - this.dirConfig.sslCaPath !== "" && - fs.existsSync(this.dirConfig.sslCaPath) - ) { - tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)]; - } - if ( - this.dirConfig.sslCertPath != null && - this.dirConfig.sslCertPath !== "" && - fs.existsSync(this.dirConfig.sslCertPath) - ) { - tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath); - } - if ( - this.dirConfig.sslKeyPath != null && - this.dirConfig.sslKeyPath !== "" && - fs.existsSync(this.dirConfig.sslKeyPath) - ) { - tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath); - } - } else { - if ( - this.dirConfig.tlsCaPath != null && - this.dirConfig.tlsCaPath !== "" && - fs.existsSync(this.dirConfig.tlsCaPath) - ) { - tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)]; - } - } - } + const protocol = this.dirConfig.ssl && !this.dirConfig.startTls ? "ldaps" : "ldap"; - tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames; - options.tlsOptions = tlsOptions; + const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port; + const options: ldapts.ClientOptions = { + url: url.trim().toLowerCase(), + }; - this.client = ldap.createClient(options); + // If using ldaps, TLS options are given to the client constructor + if (protocol === "ldaps") { + options.tlsOptions = this.buildTlsOptions(); + } - const user = - this.dirConfig.username == null || this.dirConfig.username.trim() === "" - ? null - : this.dirConfig.username; - const pass = - this.dirConfig.password == null || this.dirConfig.password.trim() === "" - ? null - : this.dirConfig.password; + this.client = new ldapts.Client(options); - if (user == null || pass == null) { - reject(this.i18nService.t("usernamePasswordNotConfigured")); - return; - } + const user = + this.dirConfig.username == null || this.dirConfig.username.trim() === "" + ? null + : this.dirConfig.username; + const pass = + this.dirConfig.password == null || this.dirConfig.password.trim() === "" + ? null + : this.dirConfig.password; - if (this.dirConfig.startTls && this.dirConfig.ssl) { - this.client.starttls(options.tlsOptions, undefined, (err, res) => { - if (err != null) { - reject(err.message); - } else { - this.client.bind(user, pass, (err2) => { - if (err2 != null) { - reject(err2.message); - } else { - resolve(); - } - }); - } - }); - } else { - this.client.bind(user, pass, (err) => { - if (err != null) { - reject(err.message); - } else { - resolve(); - } - }); - } - }); + if (user == null || pass == null) { + throw new Error(this.i18nService.t("usernamePasswordNotConfigured")); + } + + // If using StartTLS, TLS options are given to the StartTLS call + if (this.dirConfig.startTls && this.dirConfig.ssl) { + await this.client.startTLS(this.buildTlsOptions()); + } + + try { + await this.client.bind(user, pass); + } catch { + await this.client.unbind(); + } } - private async unbind(): Promise { - return new Promise((resolve, reject) => { - this.client.unbind((err) => { - if (err != null) { - reject(err); - } else { - resolve(); - } - }); - }); + private buildTlsOptions(): tls.ConnectionOptions { + const tlsOptions: tls.ConnectionOptions = {}; + + if (this.dirConfig.sslAllowUnauthorized) { + tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized; + } + if (!this.dirConfig.startTls) { + if ( + this.dirConfig.sslCaPath != null && + this.dirConfig.sslCaPath !== "" && + fs.existsSync(this.dirConfig.sslCaPath) + ) { + tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)]; + } + if ( + this.dirConfig.sslCertPath != null && + this.dirConfig.sslCertPath !== "" && + fs.existsSync(this.dirConfig.sslCertPath) + ) { + tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath); + } + if ( + this.dirConfig.sslKeyPath != null && + this.dirConfig.sslKeyPath !== "" && + fs.existsSync(this.dirConfig.sslKeyPath) + ) { + tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath); + } + } else { + if ( + this.dirConfig.tlsCaPath != null && + this.dirConfig.tlsCaPath !== "" && + fs.existsSync(this.dirConfig.tlsCaPath) + ) { + tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)]; + } + } + + tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames; + + return tlsOptions; } private bufToGuid(buf: Buffer) { @@ -494,7 +470,7 @@ export class LdapDirectoryService implements IDirectoryService { return guid.toLowerCase(); } - private checkServerIdentityAltNames(host: string, cert: PeerCertificate) { + private checkServerIdentityAltNames(host: string, cert: tls.PeerCertificate) { // Fixes the cert representation when subject is empty and altNames are present // Required for node versions < 12.14.1 (which could be used for bwdc cli) // Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a @@ -510,6 +486,6 @@ export class LdapDirectoryService implements IDirectoryService { }; } - return checkServerIdentity(host, cert); + return tls.checkServerIdentity(host, cert); } }