-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.ts
418 lines (393 loc) · 10.7 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
import ldap, { SearchOptions, Control, SearchEntryObject } from "ldapjs";
import type { Logger } from "fast-node-logger";
import { search } from "./services/search";
export type { SearchEntryObject } from "ldapjs";
export interface IClientConfig
extends Omit<ldap.ClientOptions, "url" | "bindDN"> {
/** Domain name with format: ldap://{domain.com} */
ldapServerUrl: string;
/** Password to connect to AD */
pass?: string;
/** User to connect to AD */
user?: string;
/** instance of pino logger */
logger?: Logger;
}
/** A Change object maps to the LDAP protocol of a modify change, and requires you to set the operation and modification. The operation is a string, and must be one of:
* - replace: Replaces the attribute referenced in modification. If the modification has no values, it is equivalent to a delete.
* - add: Adds the attribute value(s) referenced in modification. The attribute may or may not already exist.
* - delete: Deletes the attribute (and all values) referenced in modification.
modification is just a plain old JS object with the values you want. */
export type ModifyChange<T = any> = {
operation: "add" | "delete" | "replace";
modification: {
[key in keyof Partial<T>]: any;
};
};
type ModifyAttributeFnInput<T> = {
dn: string;
changes: ModifyChange<T>[];
controls?: any;
};
type QueryFnInput<T> = {
options?: Omit<SearchOptions, "attributes">;
/** select return attributes
* - ["*"] for all available fields
*/
attributes?: Array<keyof Partial<T> | "*">;
controls?: Control | Control[];
/** base dn to search */
base: string;
};
type AddFnInput<T> = {
entry: {
[key in keyof Partial<T>]: string | string[];
};
dn: string;
controls?: any;
};
type CompareFnInput<T = any> = {
dn: string;
controls?: any;
/** attribute to compare
* - Note: it just use first property, no matter how many property gets
*/
field: {
[key in keyof Partial<T>]: string;
};
};
type DelFnInput = {
dn: string;
controls?: any;
};
type ExtendedOpFnInput = {
oid: string;
value: string;
controls?: any;
};
type ModifyDnFnInput = {
dn: string;
newDn: string;
controls?: any;
};
type BindFnInput = {
user?: string;
pass?: string;
};
/** @description this is a class to provide low level promise base interaction with ldap server */
export class Client {
private config: IClientConfig;
private client!: ldap.Client;
private logger?: Logger;
constructor(config: IClientConfig) {
this.config = config;
let reconnect: any = true;
if (typeof config.reconnect !== "undefined") {
reconnect = config.reconnect;
}
this.client = ldap.createClient({
...this.config,
reconnect,
url: this.config.ldapServerUrl,
log: this.config.logger,
});
}
/** connection status */
public getConnectionStatus = (): boolean => {
return this.client.connected;
};
/** @return a connected ldap client that is useful for use flexibility of [ldap.js](http://ldapjs.org/) directly. */
public async bind(input?: BindFnInput): Promise<ldap.Client> {
this.logger?.trace("bind()");
return new Promise((resolve, reject) => {
if (this.client) {
this.client.destroy((err: any) => {
reject(err);
});
}
this.client = ldap.createClient({
...this.config,
url: this.config.ldapServerUrl,
log: this.config.logger,
});
this.client.on("connectError", (err) => {
reject(err);
});
const user = input?.user ?? this.config.user;
const pass = input?.pass ?? this.config.pass;
if (user && pass) {
this.client.bind(user, pass, (err, result) => {
if (err) {
reject(err);
}
resolve(this.client);
});
} else {
reject(
new Error(
`user or pass not provided! you can provide in either bind function or when create new instance of client.`,
),
);
}
});
}
/** unbind connection */
public async unbind(): Promise<void> {
this.logger?.trace("unbind()");
return new Promise((resolve, reject) => {
this.client.unbind((err) => {
if (err) {
reject(err);
}
resolve();
});
});
}
/** unbind the connection and don't allow it to connect again. */
public async destroy(): Promise<void> {
return new Promise((resolve, reject) => {
this.client.destroy((err: any) => {
reject(err);
});
resolve();
});
}
/** bind to server if client is not already bound */
private async connect() {
this.logger?.trace("connect()");
if (this.client?.connected) {
return this.client;
}
const client = await this.bind();
return client;
}
/** @description raw search to provided full flexibility */
public async query<T = any>({
options,
controls,
base,
attributes,
}: QueryFnInput<T>) {
this.logger?.trace("query()");
await this.connect();
const data = await search({
client: this.client,
base,
options: {
...options,
attributes: attributes as string[],
},
controls,
});
return data;
}
/** @description raw search returns just attributes
*
* // TODO: add Generic type for return data
*/
public async queryAttributes<T = any>({
options,
attributes,
controls,
base,
}: QueryFnInput<T>): Promise<SearchEntryObject[]> {
this.logger?.trace("queryAttributes()");
await this.connect();
const data = await search({
client: this.client,
base,
options: {
...options,
attributes: attributes as string[],
},
controls,
});
return data.map((entry) => entry.object);
}
/** Performs an add operation against the LDAP server.
* @description Allows you to add an entry (which is just a plain JS object)
*/
public async add<T = any>({
entry,
dn,
controls,
}: AddFnInput<T>): Promise<boolean> {
this.logger?.trace("add()");
await this.connect();
return new Promise((resolve, reject) => {
if (controls) {
this.client.add(dn, entry, controls, function addCallback(err) {
if (err) {
reject(err);
}
resolve(true);
});
} else {
this.client.add(dn, entry, function addCallback(err) {
if (err) {
reject(err);
}
resolve(true);
});
}
});
}
/** Performs a LDAP compare operation with the given attribute and value against the entry referenced by dn. */
public async compare<T = any>({
dn,
controls,
field,
}: CompareFnInput<T>): Promise<boolean | undefined> {
this.logger?.trace("compare()");
await this.connect();
return new Promise((resolve, reject) => {
const [attribute, value] = Object.entries<string>(field)[0];
if (controls) {
this.client.compare(
dn,
attribute,
value,
controls,
function compareCallback(err, matched) {
if (err) {
reject(err);
}
resolve(matched);
},
);
} else {
this.client.compare(dn, attribute, value, function compareCallback(
err,
matched,
) {
if (err) {
reject(err);
}
resolve(matched);
});
}
});
}
/** Deletes an entry from the LDAP server. */
public async del({ dn, controls }: DelFnInput): Promise<boolean> {
this.logger?.trace("del()");
await this.connect();
return new Promise((resolve, reject) => {
if (controls) {
this.client.del(dn, controls, function delCallback(err) {
if (err) {
reject(err);
}
resolve(true);
});
} else {
this.client.del(dn, function delCallback(err) {
if (err) {
reject(err);
}
resolve(true);
});
}
});
}
/**
* @description Performs an extended operation against LDAP server.
* @example
* const {value} = await client.extendedOp('1.3.6.1.4.1.4203.1.11.3');
* console.log('whois: ' + value);
*/
public async extendedOp({
oid,
value,
controls,
}: ExtendedOpFnInput): Promise<{ value: string; res: any }> {
this.logger?.trace("extendedOp()");
await this.connect();
return new Promise((resolve, reject) => {
if (controls) {
this.client.exop(oid, value, controls, function extendedOpCallback(
err,
value,
res,
) {
if (err) {
reject(err);
}
resolve({ value, res });
});
} else {
this.client.exop(oid, value, function extendedOpCallback(
err,
value,
res,
) {
if (err) {
reject(err);
}
resolve({ value, res });
});
}
});
}
/**
* @description Performs a LDAP modifyDN (rename) operation against an entry in the LDAP server. A couple points with this client API:
* - There is no ability to set "keep old dn." It's always going to flag the old dn to be purged.
* - The client code will automatically figure out if the request is a "new superior" request ("new superior" means move to a different part of the tree, as opposed to just renaming the leaf).
*/
public async modifyDn({
dn,
newDn,
controls,
}: ModifyDnFnInput): Promise<boolean> {
this.logger?.trace("modifyDn()");
await this.connect();
return new Promise((resolve, reject) => {
if (controls) {
this.client.modifyDN(dn, newDn, controls, function modifyDnCallback(
err,
) {
if (err) {
reject(err);
}
resolve(true);
});
} else {
this.client.modifyDN(dn, newDn, function modifyDNCallback(err) {
if (err) {
reject(err);
}
resolve(true);
});
}
});
}
/** Performs a LDAP modify operation against attributes of the existing LDAP entity. This API requires you to pass in a Change object.
*/
public async modifyAttribute<T = any>({
dn,
changes,
controls,
}: ModifyAttributeFnInput<T>): Promise<boolean> {
this.logger?.trace("modifyAttribute()");
await this.connect();
return new Promise((resolve, reject) => {
if (controls) {
this.client.modify(dn, changes, controls, function modifyCallBack(
error,
) {
if (error) {
reject(error);
}
resolve(true);
});
} else {
this.client.modify(dn, changes, function modifyCallBack(error) {
if (error) {
reject(error);
}
resolve(true);
});
}
});
}
}