mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-11 13:53:22 +00:00
Initial implementation of ldapts
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import * as fs from "fs";
|
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 { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
@@ -19,7 +19,7 @@ import { IDirectoryService } from "./directory.service";
|
|||||||
const UserControlAccountDisabled = 2;
|
const UserControlAccountDisabled = 2;
|
||||||
|
|
||||||
export class LdapDirectoryService implements IDirectoryService {
|
export class LdapDirectoryService implements IDirectoryService {
|
||||||
private client: ldap.Client;
|
private client: ldapts.Client;
|
||||||
private dirConfig: LdapConfiguration;
|
private dirConfig: LdapConfiguration;
|
||||||
private syncConfig: SyncConfiguration;
|
private syncConfig: SyncConfiguration;
|
||||||
|
|
||||||
@@ -48,21 +48,25 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
await this.bind();
|
await this.bind();
|
||||||
|
|
||||||
let users: UserEntry[];
|
let users: UserEntry[];
|
||||||
if (this.syncConfig.users) {
|
|
||||||
users = await this.getUsers(force, test);
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups: GroupEntry[];
|
let groups: GroupEntry[];
|
||||||
if (this.syncConfig.groups) {
|
|
||||||
let groupForce = force;
|
try {
|
||||||
if (!groupForce && users != null) {
|
if (this.syncConfig.users) {
|
||||||
const activeUsers = users.filter((u) => !u.deleted && !u.disabled);
|
users = await this.getUsers(force, test);
|
||||||
groupForce = activeUsers.length > 0;
|
|
||||||
}
|
}
|
||||||
groups = await this.getGroups(groupForce);
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await this.client.unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.unbind();
|
|
||||||
return [groups, users];
|
return [groups, users];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +105,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
const deletedPath = this.makeSearchPath("CN=Deleted Objects");
|
const deletedPath = this.makeSearchPath("CN=Deleted Objects");
|
||||||
this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter);
|
this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter);
|
||||||
|
|
||||||
const delControl = new (ldap as any).Control({
|
const delControl = new ldapts.Control("1.2.840.113556.1.4.417", { critical: true });
|
||||||
type: "1.2.840.113556.1.4.417",
|
|
||||||
criticality: true,
|
|
||||||
});
|
|
||||||
const deletedUsers = await this.search<UserEntry>(
|
const deletedUsers = await this.search<UserEntry>(
|
||||||
deletedPath,
|
deletedPath,
|
||||||
deletedFilter,
|
deletedFilter,
|
||||||
@@ -334,144 +335,93 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
path: string,
|
path: string,
|
||||||
filter: string,
|
filter: string,
|
||||||
processEntry: (searchEntry: any) => T,
|
processEntry: (searchEntry: any) => T,
|
||||||
controls: ldap.Control[] = [],
|
controls: ldapts.Control[] = [],
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const options: ldap.SearchOptions = {
|
const options: ldapts.SearchOptions = {
|
||||||
filter: filter,
|
filter: filter,
|
||||||
scope: "sub",
|
scope: "sub",
|
||||||
paged: this.dirConfig.pagedSearch,
|
paged: this.dirConfig.pagedSearch,
|
||||||
};
|
};
|
||||||
const entries: T[] = [];
|
const { searchEntries } = await this.client.search(path, options, controls);
|
||||||
return new Promise<T[]>((resolve, reject) => {
|
return searchEntries.map((e) => processEntry(e)).filter((e) => e != null);
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async bind(): Promise<any> {
|
private async bind(): Promise<any> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
|
||||||
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
|
throw new Error(this.i18nService.t("dirConfigIncomplete"));
|
||||||
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(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const tlsOptions: any = {};
|
const protocol = "ldap" + (this.dirConfig.ssl && !this.dirConfig.startTls ? "s" : "");
|
||||||
if (this.dirConfig.ssl) {
|
const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port;
|
||||||
if (this.dirConfig.sslAllowUnauthorized) {
|
const options: ldapts.ClientOptions = {
|
||||||
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
|
url: url.trim().toLowerCase(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const tlsOptions: tls.ConnectionOptions = {};
|
||||||
|
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.startTls) {
|
if (
|
||||||
if (
|
this.dirConfig.sslCertPath != null &&
|
||||||
this.dirConfig.sslCaPath != null &&
|
this.dirConfig.sslCertPath !== "" &&
|
||||||
this.dirConfig.sslCaPath !== "" &&
|
fs.existsSync(this.dirConfig.sslCertPath)
|
||||||
fs.existsSync(this.dirConfig.sslCaPath)
|
) {
|
||||||
) {
|
tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
|
||||||
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
|
}
|
||||||
}
|
if (
|
||||||
if (
|
this.dirConfig.sslKeyPath != null &&
|
||||||
this.dirConfig.sslCertPath != null &&
|
this.dirConfig.sslKeyPath !== "" &&
|
||||||
this.dirConfig.sslCertPath !== "" &&
|
fs.existsSync(this.dirConfig.sslKeyPath)
|
||||||
fs.existsSync(this.dirConfig.sslCertPath)
|
) {
|
||||||
) {
|
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
|
||||||
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;
|
|
||||||
options.tlsOptions = tlsOptions;
|
|
||||||
|
|
||||||
this.client = ldap.createClient(options);
|
|
||||||
|
|
||||||
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 (user == null || pass == null) {
|
|
||||||
reject(this.i18nService.t("usernamePasswordNotConfigured"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
this.client.bind(user, pass, (err) => {
|
if (
|
||||||
if (err != null) {
|
this.dirConfig.tlsCaPath != null &&
|
||||||
reject(err.message);
|
this.dirConfig.tlsCaPath !== "" &&
|
||||||
} else {
|
fs.existsSync(this.dirConfig.tlsCaPath)
|
||||||
resolve();
|
) {
|
||||||
}
|
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async unbind(): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.client.unbind((err) => {
|
|
||||||
if (err != null) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
|
||||||
|
options.tlsOptions = tlsOptions;
|
||||||
|
|
||||||
|
this.client = new ldapts.Client(options);
|
||||||
|
|
||||||
|
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 (user == null || pass == null) {
|
||||||
|
throw new Error(this.i18nService.t("usernamePasswordNotConfigured"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dirConfig.startTls && this.dirConfig.ssl) {
|
||||||
|
await this.client.startTLS(options.tlsOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.client.bind(user, pass);
|
||||||
|
} finally {
|
||||||
|
await this.client.unbind();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bufToGuid(buf: Buffer) {
|
private bufToGuid(buf: Buffer) {
|
||||||
@@ -494,7 +444,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
return guid.toLowerCase();
|
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
|
// 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)
|
// 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
|
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
|
||||||
@@ -510,6 +460,6 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return checkServerIdentity(host, cert);
|
return tls.checkServerIdentity(host, cert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user