This repository has been archived by the owner on May 14, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
guide.html
691 lines (648 loc) · 47.1 KB
/
guide.html
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
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
<!DOCTYPE html>
<html lang="en">
<head>
<title>LDAP Guide | ldapjs</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="media/css/style.css">
<link rel="stylesheet" type="text/css" href="media/css/highlight.css">
</head>
<body>
<div id="header">
<h1>LDAP Guide | ldapjs Documentation</h1>
</div>
<div id="sidebar">
<div>Sections</div>
<span>
<ul>
<li><div><a href="index.html">Home</a></div></li>
<li><div><a href="guide.html">Guide</a></div></li>
<li><div><a href="examples.html">Examples</a></div></li>
<li><div><a href="client.html">Client API</a></div></li>
<li><div><a href="server.html">Server API</a></div></li>
<li><div><a href="dn.html">DN API</a></div></li>
<li><div><a href="filters.html">Filters API</a></div></li>
<li><div><a href="errors.html">Error API</a></div></li>
</ul>
</span>
<div>Contents</div>
</span>
<ul>
<li>
<div>
<a href="#what-exactly-is-ldap">What exactly is LDAP?</a>
</div>
<ul>
<li>
<div>
<a href="#how-is-ldapjs-any-different">How is ldapjs any different?</a>
</div>
</li>
</ul>
</li>
<li>
<div>
<a href="#ok-cool-learn-me-some-ldap">Ok, cool. Learn me some LDAP!</a>
</div>
<ul>
<li>
<div>
<a href="#install">Install</a>
</div>
</li>
<li>
<div>
<a href="#bind">Bind</a>
</div>
</li>
<li>
<div>
<a href="#search">Search</a>
</div>
</li>
<li>
<div>
<a href="#add">Add</a>
</div>
</li>
<li>
<div>
<a href="#modify">Modify</a>
</div>
</li>
<li>
<div>
<a href="#delete">Delete</a>
</div>
</li>
</ul>
</li>
<li>
<div>
<a href="#where-to-go-from-here">Where to go from here</a>
</div>
</li>
</ul>
</div>
<div id="content">
<h1 id="ldap-guide">LDAP Guide</h1>
<div class="intro">
<p>This guide was written assuming that you (1) don't know anything about ldapjs,
and perhaps more importantly (2) know little, if anything about LDAP. If you're
already an LDAP whiz, please don't read this and feel it's condescending. Most
people don't know how LDAP works, other than that "it's that thing that has my
password."</p>
<p>By the end of this guide, we'll have a simple LDAP server that accomplishes a
"real" task.</p>
</div>
<h1 id="what-exactly-is-ldap">What exactly is LDAP?</h1>
<p>If you haven't already read the
<a href="http://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol">wikipedia</a>
entry (which you should go do right now), LDAP is the "Lightweight Directory
Access Protocol". A directory service basically breaks down as follows:</p>
<ul>
<li>A directory is a tree of entries (similar to but different than an FS).</li>
<li>Every entry has a unique name in the tree.</li>
<li>An entry is a set of attributes.</li>
<li>An attribute is a key/value(s) pairing (multivalue is natural).</li>
</ul>
<p>It might be helpful to visualize:</p>
<pre><code> o=example
/ \
ou=users ou=groups
/ | | \
cn=john cn=jane cn=dudes cn=dudettes
/
keyid=foo
</code></pre>
<p>Let's say we wanted to look at the record cn=john:</p>
<pre><code class="language-shell">dn: cn=john, ou=users, o=example
cn: john
sn: smith
email: [email protected]
email: [email protected]
objectClass: person
</code></pre>
<p>A few things to note:</p>
<ul>
<li>All names in a directory tree are actually referred to as a <em>distinguished
name</em>, or <em>dn</em> for short. A dn is comprised of attributes that lead to that
node in the tree, as shown above (the syntax is foo=bar, ...).</li>
<li>The root of the tree is at the right of the <em>dn</em>, which is inverted from a
filesystem hierarchy.</li>
<li>Every entry in the tree is an <em>instance of</em> an <em>objectclass</em>.</li>
<li>An <em>objectclass</em> is a schema concept; think of it like a table in a
traditional ORM.</li>
<li>An <em>objectclass</em> defines what <em>attributes</em> an entry can have (on the ORM
analogy, an <em>attribute</em> would be like a column).</li>
</ul>
<p>That's it. LDAP, then, is the protocol for interacting with the directory tree,
and it's comprehensively specified for common operations, like
add/update/delete and importantly, search. Really, the power of LDAP comes
through the search operations defined in the protocol, which are richer
than HTTP query string filtering, but less powerful than full SQL. You can
think of LDAP as a NoSQL/document store with a well-defined query syntax.</p>
<p>So, why isn't LDAP more popular for a lot of applications? Like anything else
that has "simple" or "lightweight" in the name, it's not really that
lightweight. In particular, almost all of the implementations of LDAP stem
from the original University of Michigan codebase written in 1996. At that
time, the original intention of LDAP was to be an IP-accessible gateway to the
much more complex X.500 directories, which means that a lot of that
baggage has carried through to today. That makes for a high barrier to entry,
when most applications just don't need most of those features.</p>
<h2 id="how-is-ldapjs-any-different">How is ldapjs any different?</h2>
<p>Well, on the one hand, since ldapjs has to be 100% wire compatible with LDAP to
be useful, it's not. On the other hand, there are no forced assumptions about
what you need and don't need for your use of a directory system. For example,
want to run with no-schema in OpenLDAP/389DS/et al? Good luck. Most of the
server implementations support arbitrary "backends" for persistence, but really
you'll be using <a href="http://www.oracle.com/technetwork/database/berkeleydb/overview/index.html">BDB</a>.</p>
<p>Want to run schema-less in ldapjs, or wire it up with some mongoose models? No
problem. Want to back it to redis? Should be able to get some basics up in a
day or two.</p>
<p>Basically, the ldapjs philosophy is to deal with the "muck" of LDAP, and then
get out of the way so you can just use the "good parts."</p>
<h1 id="ok-cool-learn-me-some-ldap">Ok, cool. Learn me some LDAP!</h1>
<p>With the initial fluff out of the way, let's do something crazy to teach
you some LDAP. Let's put an LDAP server up over the top of your (Linux) host's
/etc/passwd and /etc/group files. Usually sysadmins "go the other way," and
replace /etc/passwd with a
<a href="http://en.wikipedia.org/wiki/Pluggable_authentication_module" title="Pluggable
authentication module">PAM</a> module to LDAP. While this is probably not a super
useful real-world use case, it will teach you some of the basics. If it is
useful to you, then that's gravy.</p>
<h2 id="install">Install</h2>
<p>If you don't already have node.js and npm, clearly you need those, so follow
the steps at <a href="http://nodejs.org">nodejs.org</a> and <a href="http://npmjs.org">npmjs.org</a>,
respectively. After that, run:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">npm install ldapjs</span>
</code></pre>
<p>Rather than overload you with client-side programming for now, we'll use
the OpenLDAP CLI to interact with our server. It's almost certainly already
installed on your system, but if not, you can get it from brew/apt/yum/your
package manager here.</p>
<p>To get started, open some file, and let's get the library loaded and a server
created:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> ldap = <span class="hljs-built_in">require</span>(<span class="hljs-string">'ldapjs'</span>);
<span class="hljs-keyword">const</span> server = ldap.<span class="hljs-title function_">createServer</span>();
server.<span class="hljs-title function_">listen</span>(<span class="hljs-number">1389</span>, <span class="hljs-function">() =></span> {
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">'/etc/passwd LDAP server up at: %s'</span>, server.<span class="hljs-property">url</span>);
});
</code></pre>
<p>And run that. Doing anything will give you errors (LDAP "No Such Object")
since we haven't added any support in yet, but go ahead and try it anyway:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapsearch -H ldap://localhost:1389 -x -b <span class="hljs-string">"o=myhost"</span> objectclass=*</span>
</code></pre>
<p>Before we go any further, note that the complete code for the server we are
about to build up is on the <a href="examples.html">examples</a> page.</p>
<h2 id="bind">Bind</h2>
<p>So, lesson #1 about LDAP: unlike HTTP, it's connection-oriented; that means that
you authenticate (in LDAP nomenclature this is called a <em>bind</em>), and all
subsequent operations operate at the level of priviledge you established during
a bind. You can bind any number of times on a single connection and change that
identity. Technically, it's optional, and you can support <em>anonymous</em>
operations from clients, but (1) you probably don't want that, and (2) most
LDAP clients will initiate a bind anyway (OpenLDAP will), so let's add it in
and get it out of our way.</p>
<p>What we're going to do is add a "root" user to our LDAP server. This root user
has no correspondence to our Unix root user, it's just something we're making up
and going to use for allowing an (LDAP) admin to do anything. To do so, add
this code into your file:</p>
<pre><code class="language-js">server.<span class="hljs-title function_">bind</span>(<span class="hljs-string">'cn=root'</span>, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =></span> {
<span class="hljs-keyword">if</span> (req.<span class="hljs-property">dn</span>.<span class="hljs-title function_">toString</span>() !== <span class="hljs-string">'cn=root'</span> || req.<span class="hljs-property">credentials</span> !== <span class="hljs-string">'secret'</span>)
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">InvalidCredentialsError</span>());
res.<span class="hljs-title function_">end</span>();
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>();
});
</code></pre>
<p>Not very secure, but this is a demo. What we did there was "mount" a tree in
the ldapjs server, and add a handler for the <em>bind</em> method. If you've ever used
express, this pattern should be really familiar; you can add any number of
handlers in, as we'll see later.</p>
<p>On to the meat of the method. What's up with this?</p>
<pre><code class="language-js"><span class="hljs-keyword">if</span> (req.<span class="hljs-property">dn</span>.<span class="hljs-title function_">toString</span>() !== <span class="hljs-string">'cn=root'</span> || req.<span class="hljs-property">credentials</span> !== <span class="hljs-string">'secret'</span>)
</code></pre>
<p>The first part <code>req.dn.toString() !== 'cn=root'</code>: you're probably thinking
"WTF?!? Does ldapjs allow something other than cn=root into this handler?" Sort
of. It allows cn=root <em>and any children</em> into that handler. So the entries
<code>cn=root</code> and <code>cn=evil, cn=root</code> would both match and flow into this handler.
Hence that check. The second check <code>req.credentials</code> is probably obvious, but
it brings up an important point, and that is the <code>req</code>, <code>res</code> objects in ldapjs
are not homogenous across server operation types. Unlike HTTP, there's not a
single message format, so each of the operations has fields and functions
appropriate to that type. The LDAP bind operation has <code>credentials</code>, which are
a string representation of the client's password. This is logically the same as
HTTP Basic Authentication (there are other mechanisms, but that's out of scope
for a getting started guide). Ok, if either of those checks failed, we pass a
new ldapjs <code>Error</code> back into the server, and it will (1) halt the chain, and (2)
send the proper error code back to the client.</p>
<p>Lastly, assuming that this request was ok, we just end the operation with
<code>res.end()</code>. The <code>return next()</code> isn't strictly necessary, since here we only
have one handler in the chain, but it's good habit to always do that, so if you
add another handler in later you won't get bit by it not being invoked.</p>
<p>Blah blah, let's try running the ldap client again, first with a bad password:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapsearch -H ldap://localhost:1389 -x -D cn=root -w foo -b <span class="hljs-string">"o=myhost"</span> objectclass=*</span>
ldap_bind: Invalid credentials (49)
matched DN: cn=root
additional info: Invalid Credentials
</code></pre>
<p>And again with the correct one:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b <span class="hljs-string">"o=myhost"</span> objectclass=*</span>
No such object (32)
Additional information: No tree found for: o=myhost
</code></pre>
<p>Don't worry about all the flags we're passing into OpenLDAP, that's just to make
their CLI less annonyingly noisy. This time, we got another <code>No such object</code>
error, but it's for the tree <code>o=myhost</code>. That means our bind went through, and
our search failed, since we haven't yet added a search handler. Just one more
small thing to do first.</p>
<p>Remember earlier I said there were no authorization rules baked into LDAP? Well,
we added a bind route, so the only user that can authenticate is <code>cn=root</code>, but
what if the remote end doesn't authenticate at all? Right, nothing says they
<em>have to</em> bind, that's just what the common clients do. Let's add a quick
authorization handler that we'll use in all our subsequent routes:</p>
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">authorize</span>(<span class="hljs-params">req, res, next</span>) {
<span class="hljs-keyword">if</span> (!req.<span class="hljs-property">connection</span>.<span class="hljs-property">ldap</span>.<span class="hljs-property">bindDN</span>.<span class="hljs-title function_">equals</span>(<span class="hljs-string">'cn=root'</span>))
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">InsufficientAccessRightsError</span>());
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>();
}
</code></pre>
<p>Should be pretty self-explanatory, but as a reminder, LDAP is connection
oriented, so we check that the connection remote user was indeed our <code>cn=root</code>
(by default ldapjs will have a DN of <code>cn=anonymous</code> if the client didn't bind).</p>
<h2 id="search">Search</h2>
<p>We said we wanted to allow LDAP operations over /etc/passwd, so let's detour
for a moment to explain an /etc/passwd record.</p>
<pre><code class="language-shell">jsmith:x:1001:1000:Joe Smith,Room 1007,(234)555-8910,(234)555-0044,email:/home/jsmith:/bin/sh
</code></pre>
<p>The sample record above maps to:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>jsmith</td>
<td>Username</td>
</tr>
<tr>
<td>x</td>
<td>Placeholder for password hash</td>
</tr>
<tr>
<td>1001</td>
<td>Numeric UID</td>
</tr>
<tr>
<td>1000</td>
<td>Numeric Primary GID</td>
</tr>
<tr>
<td>'Joe Smith,...'</td>
<td>DisplayName</td>
</tr>
<tr>
<td>/home/jsmith</td>
<td>Home directory</td>
</tr>
<tr>
<td>/bin/sh</td>
<td>Shell</td>
</tr>
</tbody></table>
<p>Let's write some handlers to parse that and transform it into an LDAP search
record (note, you'll need to add <code>const fs = require('fs');</code> at the top of the
source file).</p>
<p>First, make a handler that just loads the "user database" in a "pre" handler:</p>
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">loadPasswdFile</span>(<span class="hljs-params">req, res, next</span>) {
fs.<span class="hljs-title function_">readFile</span>(<span class="hljs-string">'/etc/passwd'</span>, <span class="hljs-string">'utf8'</span>, <span class="hljs-function">(<span class="hljs-params">err, data</span>) =></span> {
<span class="hljs-keyword">if</span> (err)
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">OperationsError</span>(err.<span class="hljs-property">message</span>));
req.<span class="hljs-property">users</span> = {};
<span class="hljs-keyword">const</span> lines = data.<span class="hljs-title function_">split</span>(<span class="hljs-string">'\n'</span>);
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> line <span class="hljs-keyword">of</span> lines) {
<span class="hljs-keyword">if</span> (!line || <span class="hljs-regexp">/^#/</span>.<span class="hljs-title function_">test</span>(line))
<span class="hljs-keyword">continue</span>;
<span class="hljs-keyword">const</span> record = line.<span class="hljs-title function_">split</span>(<span class="hljs-string">':'</span>);
<span class="hljs-keyword">if</span> (!record || !record.<span class="hljs-property">length</span>)
<span class="hljs-keyword">continue</span>;
req.<span class="hljs-property">users</span>[record[<span class="hljs-number">0</span>]] = {
<span class="hljs-attr">dn</span>: <span class="hljs-string">'cn='</span> + record[<span class="hljs-number">0</span>] + <span class="hljs-string">', ou=users, o=myhost'</span>,
<span class="hljs-attr">attributes</span>: {
<span class="hljs-attr">cn</span>: record[<span class="hljs-number">0</span>],
<span class="hljs-attr">uid</span>: record[<span class="hljs-number">2</span>],
<span class="hljs-attr">gid</span>: record[<span class="hljs-number">3</span>],
<span class="hljs-attr">description</span>: record[<span class="hljs-number">4</span>],
<span class="hljs-attr">homedirectory</span>: record[<span class="hljs-number">5</span>],
<span class="hljs-attr">shell</span>: record[<span class="hljs-number">6</span>] || <span class="hljs-string">''</span>,
<span class="hljs-attr">objectclass</span>: <span class="hljs-string">'unixUser'</span>
}
};
}
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>();
});
}
</code></pre>
<p>Ok, all that did is tack the /etc/passwd records onto req.users so that any
subsequent handler doesn't have to reload the file. Next, let's write a search
handler to process that:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> pre = [authorize, loadPasswdFile];
server.<span class="hljs-title function_">search</span>(<span class="hljs-string">'o=myhost'</span>, pre, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =></span> {
<span class="hljs-keyword">const</span> keys = <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">keys</span>(req.<span class="hljs-property">users</span>);
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> k <span class="hljs-keyword">of</span> keys) {
<span class="hljs-keyword">if</span> (req.<span class="hljs-property">filter</span>.<span class="hljs-title function_">matches</span>(req.<span class="hljs-property">users</span>[k].<span class="hljs-property">attributes</span>))
res.<span class="hljs-title function_">send</span>(req.<span class="hljs-property">users</span>[k]);
}
res.<span class="hljs-title function_">end</span>();
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>();
});
</code></pre>
<p>And try running:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b <span class="hljs-string">"o=myhost"</span> cn=root</span>
dn: cn=root, ou=users, o=myhost
cn: root
uid: 0
gid: 0
description: System Administrator
homedirectory: /var/root
shell: /bin/sh
objectclass: unixUser
</code></pre>
<p>Sweet! Try this out too:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b <span class="hljs-string">"o=myhost"</span> objectclass=*</span>
...
</code></pre>
<p>You should have seen an entry for every record in /etc/passwd with the second.
What all did we do here? A lot. Let's break this down...</p>
<h3 id="what-did-i-just-do-on-the-command-line">What did I just do on the command line?</h3>
<p>Let's start with looking at what you even asked for:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b <span class="hljs-string">"o=myhost"</span> cn=root</span>
</code></pre>
<p>We can throw away <code>ldapsearch -H -x -D -w -LLL</code>, as those just specify the URL
to connect to, the bind credentials and the <code>-LLL</code> just quiets down OpenLDAP.
That leaves us with: <code>-b "o=myhost" cn=root</code>.</p>
<p>The <code>-b o=myhost</code> tells our LDAP server where to <em>start</em> looking in
the tree for entries that might match the search filter, which above is
<code>cn=root</code>.</p>
<p>In this little LDAP example, we're mostly throwing out any qualification of the
"tree," since there's not actually a tree in /etc/passwd (we will extend later
with /etc/group). Remember how I said ldapjs gets out of the way and doesn't
force anything on you? Here's an example. If we wanted an LDAP server to run
over the filesystem, we actually would use this, but here, meh.</p>
<p>Next, <code>cn=root</code> is the search "filter". LDAP has a rich specification of
filters, where you can specify <code>and</code>, <code>or</code>, <code>not</code>, <code>>=</code>, <code><=</code>, <code>equal</code>,
<code>wildcard</code>, <code>present</code> and a few other esoteric things. Really, <code>equal</code>,
<code>wildcard</code>, <code>present</code> and the boolean operators are all you'll likely ever need.
So, the filter <code>cn=root</code> is an "equality" filter, and says to only return
entries that have attributes that match that. In the second invocation, we used
a 'presence' filter, to say 'return any entries that have an objectclass'
attribute, which in LDAP parlance is saying "give me everything."</p>
<h3 id="the-code">The code</h3>
<p>In the code above, let's ignore the fs and split stuff, since really all we
did was read in /etc/passwd line by line. After that, we looked at each record
and made the cheesiest transform ever, which is making up a "search entry." A
search entry <em>must</em> have a DN so the client knows what record it is, and a set
of attributes. So that's why we did this:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> entry = {
<span class="hljs-attr">dn</span>: <span class="hljs-string">'cn='</span> + record[<span class="hljs-number">0</span>] + <span class="hljs-string">', ou=users, o=myhost'</span>,
<span class="hljs-attr">attributes</span>: {
<span class="hljs-attr">cn</span>: record[<span class="hljs-number">0</span>],
<span class="hljs-attr">uid</span>: record[<span class="hljs-number">2</span>],
<span class="hljs-attr">gid</span>: record[<span class="hljs-number">3</span>],
<span class="hljs-attr">description</span>: record[<span class="hljs-number">4</span>],
<span class="hljs-attr">homedirectory</span>: record[<span class="hljs-number">5</span>],
<span class="hljs-attr">shell</span>: record[<span class="hljs-number">6</span>] || <span class="hljs-string">''</span>,
<span class="hljs-attr">objectclass</span>: <span class="hljs-string">'unixUser'</span>
}
};
</code></pre>
<p>Next, we let ldapjs do all the hard work of figuring out LDAP search filters
for us by calling <code>req.filter.matches</code>. If it matched, we return the whole
record with <code>res.send</code>. In this little example we're running O(n), so for
something big and/or slow, you'd have to do some work to effectively write a
query planner (or just not support it...). For some reference code, check out
<code>node-ldapjs-riak</code>, which takes on the fairly difficult task of writing a 'full'
LDAP server over riak.</p>
<p>To demonstrate what ldapjs is doing for you, let's find all users who have a
shell set to <code>/bin/false</code> and whose name starts with <code>p</code> (I'm doing this
on Ubuntu). Then, let's say we only care about their login name and primary
group id. We'd do this:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b <span class="hljs-string">"o=myhost"</span> <span class="hljs-string">"(&(shell=/bin/false)(cn=p*))"</span> cn gid</span>
dn: cn=proxy, ou=users, o=myhost
cn: proxy
gid: 13
dn: cn=pulse, ou=users, o=myhost
cn: pulse
gid: 114
</code></pre>
<h2 id="add">Add</h2>
<p>This is going to be a little bit ghetto, since what we're going to do is just
use node's child process module to spawn calls to <code>adduser</code>. Go ahead and add
the following code in as another handler (you'll need a
<code>const { spawn } = require('child_process');</code> at the top of your file):</p>
<pre><code class="language-js">server.<span class="hljs-title function_">add</span>(<span class="hljs-string">'ou=users, o=myhost'</span>, pre, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =></span> {
<span class="hljs-keyword">if</span> (!req.<span class="hljs-property">dn</span>.<span class="hljs-property">rdns</span>[<span class="hljs-number">0</span>].<span class="hljs-property">attrs</span>.<span class="hljs-property">cn</span>)
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">ConstraintViolationError</span>(<span class="hljs-string">'cn required'</span>));
<span class="hljs-keyword">if</span> (req.<span class="hljs-property">users</span>[req.<span class="hljs-property">dn</span>.<span class="hljs-property">rdns</span>[<span class="hljs-number">0</span>].<span class="hljs-property">attrs</span>.<span class="hljs-property">cn</span>.<span class="hljs-property">value</span>])
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">EntryAlreadyExistsError</span>(req.<span class="hljs-property">dn</span>.<span class="hljs-title function_">toString</span>()));
<span class="hljs-keyword">const</span> entry = req.<span class="hljs-title function_">toObject</span>().<span class="hljs-property">attributes</span>;
<span class="hljs-keyword">if</span> (entry.<span class="hljs-property">objectclass</span>.<span class="hljs-title function_">indexOf</span>(<span class="hljs-string">'unixUser'</span>) === -<span class="hljs-number">1</span>)
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">ConstraintViolationError</span>(<span class="hljs-string">'entry must be a unixUser'</span>));
<span class="hljs-keyword">const</span> opts = [<span class="hljs-string">'-m'</span>];
<span class="hljs-keyword">if</span> (entry.<span class="hljs-property">description</span>) {
opts.<span class="hljs-title function_">push</span>(<span class="hljs-string">'-c'</span>);
opts.<span class="hljs-title function_">push</span>(entry.<span class="hljs-property">description</span>[<span class="hljs-number">0</span>]);
}
<span class="hljs-keyword">if</span> (entry.<span class="hljs-property">homedirectory</span>) {
opts.<span class="hljs-title function_">push</span>(<span class="hljs-string">'-d'</span>);
opts.<span class="hljs-title function_">push</span>(entry.<span class="hljs-property">homedirectory</span>[<span class="hljs-number">0</span>]);
}
<span class="hljs-keyword">if</span> (entry.<span class="hljs-property">gid</span>) {
opts.<span class="hljs-title function_">push</span>(<span class="hljs-string">'-g'</span>);
opts.<span class="hljs-title function_">push</span>(entry.<span class="hljs-property">gid</span>[<span class="hljs-number">0</span>]);
}
<span class="hljs-keyword">if</span> (entry.<span class="hljs-property">shell</span>) {
opts.<span class="hljs-title function_">push</span>(<span class="hljs-string">'-s'</span>);
opts.<span class="hljs-title function_">push</span>(entry.<span class="hljs-property">shell</span>[<span class="hljs-number">0</span>]);
}
<span class="hljs-keyword">if</span> (entry.<span class="hljs-property">uid</span>) {
opts.<span class="hljs-title function_">push</span>(<span class="hljs-string">'-u'</span>);
opts.<span class="hljs-title function_">push</span>(entry.<span class="hljs-property">uid</span>[<span class="hljs-number">0</span>]);
}
opts.<span class="hljs-title function_">push</span>(entry.<span class="hljs-property">cn</span>[<span class="hljs-number">0</span>]);
<span class="hljs-keyword">const</span> useradd = <span class="hljs-title function_">spawn</span>(<span class="hljs-string">'useradd'</span>, opts);
<span class="hljs-keyword">const</span> messages = [];
useradd.<span class="hljs-property">stdout</span>.<span class="hljs-title function_">on</span>(<span class="hljs-string">'data'</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =></span> {
messages.<span class="hljs-title function_">push</span>(data.<span class="hljs-title function_">toString</span>());
});
useradd.<span class="hljs-property">stderr</span>.<span class="hljs-title function_">on</span>(<span class="hljs-string">'data'</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =></span> {
messages.<span class="hljs-title function_">push</span>(data.<span class="hljs-title function_">toString</span>());
});
useradd.<span class="hljs-title function_">on</span>(<span class="hljs-string">'exit'</span>, <span class="hljs-function">(<span class="hljs-params">code</span>) =></span> {
<span class="hljs-keyword">if</span> (code !== <span class="hljs-number">0</span>) {
<span class="hljs-keyword">let</span> msg = <span class="hljs-string">''</span> + code;
<span class="hljs-keyword">if</span> (messages.<span class="hljs-property">length</span>)
msg += <span class="hljs-string">': '</span> + messages.<span class="hljs-title function_">join</span>();
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">OperationsError</span>(msg));
}
res.<span class="hljs-title function_">end</span>();
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>();
});
});
</code></pre>
<p>Then, you'll need to be root to have this running, so start your server with
<code>sudo</code> (or be root, whatever). Now, go ahead and create a file called
<code>user.ldif</code> with the following contents:</p>
<pre><code class="language-shell">dn: cn=ldapjs, ou=users, o=myhost
objectClass: unixUser
cn: ldapjs
shell: /bin/bash
description: Created via ldapadd
</code></pre>
<p>Now go ahead and invoke with:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapadd -H ldap://localhost:1389 -x -D cn=root -w secret -f ./user.ldif</span>
adding new entry "cn=ldapjs, ou=users, o=myhost"
</code></pre>
<p>Let's confirm he got added with an ldapsearch:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapsearch -H ldap://localhost:1389 -LLL -x -D cn=root -w secret -b <span class="hljs-string">"ou=users, o=myhost"</span> cn=ldapjs</span>
dn: cn=ldapjs, ou=users, o=myhost
cn: ldapjs
uid: 1001
gid: 1001
description: Created via ldapadd
homedirectory: /home/ldapjs
shell: /bin/bash
objectclass: unixUser
</code></pre>
<p>As before, here's a breakdown of the code:</p>
<pre><code class="language-js">server.<span class="hljs-title function_">add</span>(<span class="hljs-string">'ou=users, o=myhost'</span>, pre, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =></span> {
<span class="hljs-keyword">if</span> (!req.<span class="hljs-property">dn</span>.<span class="hljs-property">rdns</span>[<span class="hljs-number">0</span>].<span class="hljs-property">attrs</span>.<span class="hljs-property">cn</span>)
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">ConstraintViolationError</span>(<span class="hljs-string">'cn required'</span>));
<span class="hljs-keyword">if</span> (req.<span class="hljs-property">users</span>[req.<span class="hljs-property">dn</span>.<span class="hljs-property">rdns</span>[<span class="hljs-number">0</span>].<span class="hljs-property">attrs</span>.<span class="hljs-property">cn</span>.<span class="hljs-property">value</span>])
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">EntryAlreadyExistsError</span>(req.<span class="hljs-property">dn</span>.<span class="hljs-title function_">toString</span>()));
<span class="hljs-keyword">const</span> entry = req.<span class="hljs-title function_">toObject</span>().<span class="hljs-property">attributes</span>;
<span class="hljs-keyword">if</span> (entry.<span class="hljs-property">objectclass</span>.<span class="hljs-title function_">indexOf</span>(<span class="hljs-string">'unixUser'</span>) === -<span class="hljs-number">1</span>)
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">ConstraintViolationError</span>(<span class="hljs-string">'entry must be a unixUser'</span>));
});
</code></pre>
<p>A few new things:</p>
<ul>
<li>We mounted this handler at <code>ou=users, o=myhost</code>. Why? What if we want to
extend this little project with groups? We probably want those under a
different part of the tree.</li>
<li>We did some really minimal schema enforcement by:<ul>
<li>Checking that the leaf RDN (relative distinguished name) was a <em>cn</em>
attribute.</li>
<li>We then did <code>req.toObject()</code>. As mentioned before, each of the req/res
objects have special APIs that make sense for that operation. Without getting
into the details, the LDAP add operation on the wire doesn't look like a JS
object, and we want to support both the LDAP nerd that wants to see what
got sent, and the "easy" case. So use <code>.toObject()</code>. Note we also filtered
out to the <code>attributes</code> portion of the object since that's all we're really
looking at.</li>
<li>Lastly, we did a super minimal check to see if the entry was of type
<code>unixUser</code>. Frankly for this case, it's kind of useless, but it does illustrate
one point: attribute names are case-insensitive, so ldapjs converts them all to
lower case (note the client sent <em>objectClass</em> over the wire).</li>
</ul>
</li>
</ul>
<p>After that, we really just delegated off to the <em>useradd</em> command. As far as I
know, there is not a node.js module that wraps up <code>getpwent</code> and friends,
otherwise we'd use that.</p>
<p>Now, what's missing? Oh, right, we need to let you set a password. Well, let's
support that via the <em>modify</em> command.</p>
<h2 id="modify">Modify</h2>
<p>Unlike HTTP, "partial" document updates are fully specified as part of the
RFC, so appending, removing, or replacing a single attribute is pretty natural.
Go ahead and add the following code into your source file:</p>
<pre><code class="language-js">server.<span class="hljs-title function_">modify</span>(<span class="hljs-string">'ou=users, o=myhost'</span>, pre, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =></span> {
<span class="hljs-keyword">if</span> (!req.<span class="hljs-property">dn</span>.<span class="hljs-property">rdns</span>[<span class="hljs-number">0</span>].<span class="hljs-property">attrs</span>.<span class="hljs-property">cn</span> || !req.<span class="hljs-property">users</span>[req.<span class="hljs-property">dn</span>.<span class="hljs-property">rdns</span>[<span class="hljs-number">0</span>].<span class="hljs-property">attrs</span>.<span class="hljs-property">cn</span>.<span class="hljs-property">value</span>])
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">NoSuchObjectError</span>(req.<span class="hljs-property">dn</span>.<span class="hljs-title function_">toString</span>()));
<span class="hljs-keyword">if</span> (!req.<span class="hljs-property">changes</span>.<span class="hljs-property">length</span>)
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">ProtocolError</span>(<span class="hljs-string">'changes required'</span>));
<span class="hljs-keyword">const</span> user = req.<span class="hljs-property">users</span>[req.<span class="hljs-property">dn</span>.<span class="hljs-property">rdns</span>[<span class="hljs-number">0</span>].<span class="hljs-property">attrs</span>.<span class="hljs-property">cn</span>.<span class="hljs-property">value</span>].<span class="hljs-property">attributes</span>;
<span class="hljs-keyword">let</span> mod;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> i = <span class="hljs-number">0</span>; i < req.<span class="hljs-property">changes</span>.<span class="hljs-property">length</span>; i++) {
mod = req.<span class="hljs-property">changes</span>[i].<span class="hljs-property">modification</span>;
<span class="hljs-keyword">switch</span> (req.<span class="hljs-property">changes</span>[i].<span class="hljs-property">operation</span>) {
<span class="hljs-keyword">case</span> <span class="hljs-string">'replace'</span>:
<span class="hljs-keyword">if</span> (mod.<span class="hljs-property">type</span> !== <span class="hljs-string">'userpassword'</span> || !mod.<span class="hljs-property">vals</span> || !mod.<span class="hljs-property">vals</span>.<span class="hljs-property">length</span>)
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">UnwillingToPerformError</span>(<span class="hljs-string">'only password updates '</span> +
<span class="hljs-string">'allowed'</span>));
<span class="hljs-keyword">break</span>;
<span class="hljs-keyword">case</span> <span class="hljs-string">'add'</span>:
<span class="hljs-keyword">case</span> <span class="hljs-string">'delete'</span>:
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">UnwillingToPerformError</span>(<span class="hljs-string">'only replace allowed'</span>));
}
}
<span class="hljs-keyword">const</span> passwd = <span class="hljs-title function_">spawn</span>(<span class="hljs-string">'chpasswd'</span>, [<span class="hljs-string">'-c'</span>, <span class="hljs-string">'MD5'</span>]);
passwd.<span class="hljs-property">stdin</span>.<span class="hljs-title function_">end</span>(user.<span class="hljs-property">cn</span> + <span class="hljs-string">':'</span> + mod.<span class="hljs-property">vals</span>[<span class="hljs-number">0</span>], <span class="hljs-string">'utf8'</span>);
passwd.<span class="hljs-title function_">on</span>(<span class="hljs-string">'exit'</span>, <span class="hljs-function">(<span class="hljs-params">code</span>) =></span> {
<span class="hljs-keyword">if</span> (code !== <span class="hljs-number">0</span>)
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">OperationsError</span>(code));
res.<span class="hljs-title function_">end</span>();
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>();
});
});
</code></pre>
<p>Basically, we made sure the remote client was targeting an entry that exists,
ensuring that they were asking to "replace" the <code>userPassword</code> attribute (which
is the 'standard' LDAP attribute for passwords; if you think it's easier to use
'password', knock yourself out), and then just delegating to the <code>chpasswd</code>
command (which lets you change a user's password over stdin). Next, go ahead
and create a <code>passwd.ldif</code> file:</p>
<pre><code class="language-shell">dn: cn=ldapjs, ou=users, o=myhost
changetype: modify
replace: userPassword
userPassword: secret
-
</code></pre>
<p>And then run the OpenLDAP CLI:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapmodify -H ldap://localhost:1389 -x -D cn=root -w secret -f ./passwd.ldif</span>
</code></pre>
<p>You should now be able to login to your box as the ldapjs user. Let's get
the last "mainline" piece of work out of the way, and delete the user.</p>
<h2 id="delete">Delete</h2>
<p>Delete is pretty straightforward. The client gives you a dn to delete, and you
delete it :). Add the following code into your server:</p>
<pre><code class="language-js">server.<span class="hljs-title function_">del</span>(<span class="hljs-string">'ou=users, o=myhost'</span>, pre, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =></span> {
<span class="hljs-keyword">if</span> (!req.<span class="hljs-property">dn</span>.<span class="hljs-property">rdns</span>[<span class="hljs-number">0</span>].<span class="hljs-property">attrs</span>.<span class="hljs-property">cn</span> || !req.<span class="hljs-property">users</span>[req.<span class="hljs-property">dn</span>.<span class="hljs-property">rdns</span>[<span class="hljs-number">0</span>].<span class="hljs-property">attrs</span>.<span class="hljs-property">cn</span>.<span class="hljs-property">value</span>])
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">NoSuchObjectError</span>(req.<span class="hljs-property">dn</span>.<span class="hljs-title function_">toString</span>()));
<span class="hljs-keyword">const</span> userdel = <span class="hljs-title function_">spawn</span>(<span class="hljs-string">'userdel'</span>, [<span class="hljs-string">'-f'</span>, req.<span class="hljs-property">dn</span>.<span class="hljs-property">rdns</span>[<span class="hljs-number">0</span>].<span class="hljs-property">attrs</span>.<span class="hljs-property">cn</span>.<span class="hljs-property">value</span>]);
<span class="hljs-keyword">const</span> messages = [];
userdel.<span class="hljs-property">stdout</span>.<span class="hljs-title function_">on</span>(<span class="hljs-string">'data'</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =></span> {
messages.<span class="hljs-title function_">push</span>(data.<span class="hljs-title function_">toString</span>());
});
userdel.<span class="hljs-property">stderr</span>.<span class="hljs-title function_">on</span>(<span class="hljs-string">'data'</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =></span> {
messages.<span class="hljs-title function_">push</span>(data.<span class="hljs-title function_">toString</span>());
});
userdel.<span class="hljs-title function_">on</span>(<span class="hljs-string">'exit'</span>, <span class="hljs-function">(<span class="hljs-params">code</span>) =></span> {
<span class="hljs-keyword">if</span> (code !== <span class="hljs-number">0</span>) {
<span class="hljs-keyword">let</span> msg = <span class="hljs-string">''</span> + code;
<span class="hljs-keyword">if</span> (messages.<span class="hljs-property">length</span>)
msg += <span class="hljs-string">': '</span> + messages.<span class="hljs-title function_">join</span>();
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> ldap.<span class="hljs-title class_">OperationsError</span>(msg));
}
res.<span class="hljs-title function_">end</span>();
<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>();
});
});
</code></pre>
<p>And then run the following command:</p>
<pre><code class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">ldapdelete -H ldap://localhost:1389 -x -D cn=root -w secret <span class="hljs-string">"cn=ldapjs, ou=users, o=myhost"</span></span>
</code></pre>
<h1 id="where-to-go-from-here">Where to go from here</h1>
<p>The complete source code for this example server is available in
<a href="examples.html">examples</a>. Make sure to read up on the <a href="server.html">server</a>
and <a href="client.html">client</a> APIs. If you're looking for a "drop in" solution,
take a look at <a href="https://github.com/mcavage/node-ldapjs-riak">ldapjs-riak</a>.</p>
<p><a href="https://wiki.mozilla.org/Mozilla_LDAP_SDK_Programmer%27s_Guide/Understanding_LDAP">Mozilla</a>
still maintains some web pages with LDAP overviews if you look around, if you're
looking for more tutorials. After that, you'll need to work your way through
the <a href="http://tools.ietf.org/html/rfc4510">RFCs</a> as you work through the APIs in
ldapjs.</p>
</div><!-- end #content -->
</body>
</html>