node-ldapjs/docs/guide.md

630 lines
24 KiB
Markdown
Raw Normal View History

2011-08-15 17:26:55 +00:00
---
title: ldapjs
2011-08-19 04:50:12 +00:00
brand: spartan
2011-08-15 17:26:55 +00:00
markdown2extras: wiki-tables
2011-08-19 04:50:12 +00:00
logo-color: green
logo-font-family: google:Aldrich, Verdana, sans-serif
header-font-family: google:Aldrich, Verdana, sans-serif
2011-08-15 17:26:55 +00:00
---
# This guide
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".
By the end of this guide, we'll have a simple LDAP server that accomplishes a
"real" task.
# What exactly is LDAP?
If you haven't already read the [wikipedia](http://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
entry, LDAP is the "Lightweight Directory Access Protocol". A directory service
basically breaks down as follows:
* A directory is a tree of entries (similar to but different than a FS).
* Every entry has a unique name in the tree.
* An entry is a set of attributes.
* An attribute is a key/value(s) pairing (multival is natural).
It might be helpful to visualize that:
2011-08-15 17:53:57 +00:00
o=example
/ \
ou=users ou=groups
/ | | \
cn=john cn=jane cn=dudes cn=dudettes
/
2011-08-15 17:26:55 +00:00
keyid=foo
And let's say we wanted to look at the record cn=john in that tree:
dn: cn=john, ou=users, o=example
cn: john
sn: smith
email: john@example.com
email: john.smith@example.com
objectClass: person
Then there's a few things to note:
* All names in a directory tree are actually referred to as a _distinguished
name_, or _dn_ for short. A dn is comprised of attributes that lead to that
node in the tree, as shown above (the syntax is foo=bar, ...).
* The root of the tree is at the right of the _dn_, which is inverted from a
2011-08-15 20:50:15 +00:00
filesystem hierarchy.
2011-08-15 17:26:55 +00:00
* Every entry in the tree is an _instance of_ an _objectclass_.
* An _objectclass_ is a schema concept; think of it like a table in a
traditional ORM.
* An _objectclass_ defines what _attributes_ an entry can have (on the ORM
analogy, an _attribute_ would be like a column).
That's really it. LDAP really then is the protocol for interacting with the
directory tree, and it's pretty comprehensively specified for common operations,
like add/update/delete and importantly, search. Really, the power of LDAP
really comes through the search operations defined in the protocol, which are
richer than HTTP query string filtering, but less powerful than full SQL. If it
helps, you can think of LDAP as a NoSQL/document store with a well-defined query
syntax.
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, and 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 really means that a lot of that
baggage has carried through to today. That makes for a high barrier to entry,
when really most applications just don't need most of those features.
## How is ldapjs any different?
Well, on the one hand, since ldapjs has to be 100% wire compatible with LDAP to
be useful, it's not, but on the other hand, there are no forced assumptions
about what you need and don't 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 [BDB](http://www.oracle.com/technetwork/database/berkeleydb/overview/index.html).
2011-08-15 17:26:55 +00:00
Want to run schemaless 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.
Basically, the ldapjs philospohy is to deal with the "muck" of LDAP, and then
get out of the way so you can just use the "good parts".
# Ok, cool. Learn me some LDAP!
Ok, so 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 PAM module to LDAP, so while this is probably not
a super useful real-world use case, it will teach you some of the basics.
Oh, and if it is useful to you, then that's gravy.
## Install
If you don't already have node.js and npm, clearly you need those, so follow
the steps at [nodejs.org](http://nodejs.org) and [npmjs.org](http://npmjs.org),
respectively. After that, run:
$ npm install ldapjs
Also, 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/...
To get started, open some file, and let's get the library loaded and a server
created:
var ldap = require('ldapjs');
var server = ldap.createServer();
server.listen(1389, function() {
console.log('/etc/passwd LDAP server up at: %s', server.url);
});
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:
$ ldapsearch -H ldap://localhost:1389 -x -b "o=myhost" objectclass=*
Before we go any further, note that the complete code for the server we are
about to build up is on the [examples](http://ldapjs.org/examples.html) page.
2011-08-15 17:26:55 +00:00
## Bind
So, lesson #1 about LDAP: unlike HTTP, it's connection-oriented; that means that
you authenticate (in LDAP nomenclature this is called a _bind_), 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 _anonymous_
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.
What we're going to do is add a "root" user to our LDAP server. This root user
has no correspondance 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. Great, so go
ahead and add this code into your file:
server.bind('cn=root', function(req, res, next) {
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
return next(new ldap.InvalidCredentialsError());
res.end();
return next();
});
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 _bind_ 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.
On to the meat of the method. What's up with this?
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
So, the first part `req.dn.toString() !== 'cn=root'`: you're probably thinking
"wtf?!? does ldapjs allow something other than cn=root into this handler?" Sort
of. It allows cn=root *and any children* into that handler. So the entries
`cn=root` and `cn=evil, cn=root` would both match and flow into this handler.
Hence that check. The second check `req.credentials` is probably obvious, but
it brings up an important point, and that is the `req`, `res` 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 `credentials`, 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 `Error` back into the server, and it will (1) halt the chain, and (2)
send the proper error code back to the client.
Lastly, assuming that this request was ok, we just end the operation with
`res.end()`. The `return next()` 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.
Blah blah, let's try running the ldap client again, first with a bad password:
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w foo -b "o=myhost" objectclass=*
ldap_bind: Invalid credentials (49)
2011-08-15 17:53:57 +00:00
matched DN: cn=root
additional info: Invalid Credentials
2011-08-15 17:26:55 +00:00
And again with the correct one:
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
No such object (32)
Additional information: No tree found for: o=myhost
Don't worry about all the flags we're passing into OpenLDAP, that's just to make
their CLI less annonyingly noisy. Note that this time, we got another
`No such object` error, but this time note that it's for the tree
`o=myhost`. 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.
Remember earlier I said there was no authorization rules baked into LDAP? Well,
we added a bind route, so the only user that can authenticate is `cn=root`, but
what if the remote end doesn't authenticate at all? Right, nothing says they
*have to* 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:
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals('cn=root'))
2011-08-15 17:53:57 +00:00
return next(new ldap.InsufficientAccessRightsError());
2011-08-15 17:26:55 +00:00
return next();
}
Should be pretty self-explanatory, but as a reminder, LDAP is connection
oriented, so we check that the connection remote user was indeed our `cn=root`
(by default ldapjs will have a DN of `cn=anonymous` if the client didn't bind).
## Search
Ok, we said we wanted to allow LDAP operations over /etc/passwd, so let's detour
for a moment to explain an /etc/passwd record:
jsmith:x:1001:1000:Joe Smith,Room 1007,(234)555-8910,(234)555-0044,email:/home/jsmith:/bin/sh
That maps to:
2011-08-24 19:38:23 +00:00
||jsmith||user name||
||x||historically this contained the password hash, but that's usually in /etc/shadow now, so you get an 'x'||
||1001||the unix numeric user id||
||1000||the unix numeric group id. (primary)||
||'Joe Smith,...'||the "gecos", which is a description, and is usually a comma separated list of contact details||
||/home/jsmith||the user's home directory||
||/bin/sh||the user's shell||
2011-08-15 17:26:55 +00:00
Great, let's some handlers to parse that and transform it into an LDAP search
record (note, you'll need to add `var fs = require('fs');` at the top of the
source file):
First, let's make a handler that just loads the "user database" for us in a
"pre" handler:
function loadPasswdFile(req, res, next) {
fs.readFile('/etc/passwd', 'utf8', function(err, data) {
2011-08-15 17:53:57 +00:00
if (err)
return next(new ldap.OperationsError(err.message));
req.users = {};
var lines = data.split('\n');
for (var i = 0; i < lines.length; i++) {
if (!lines[i] || /^#/.test(lines[i]))
continue;
var record = lines[i].split(':');
if (!record || !record.length)
continue;
req.users[record[0]] = {
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
attributes: {
cn: record[0],
uid: record[2],
gid: record[3],
description: record[4],
homedirectory: record[5],
shell: record[6] || '',
objectclass: 'unixUser'
}
};
}
return next();
2011-08-15 17:26:55 +00:00
});
}
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:
var pre = [authorize, loadPasswdFile];
server.search('o=myhost', pre, function(req, res, next) {
Object.keys(req.users).forEach(function(k) {
2011-08-15 17:53:57 +00:00
if (req.filter.matches(req.users[k].attributes))
res.send(req.users[k]);
2011-08-15 17:26:55 +00:00
});
res.end();
return next();
});
And try running:
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
dn: cn=root, ou=users, o=myhost
cn: root
uid: 0
gid: 0
description: System Administrator
homedirectory: /var/root
shell: /bin/sh
objectclass: unixUser
Sweet! Try this out too:
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
...
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...
### What did I just do on the command line?
Let's start with looking at what you even asked for:
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
We can throw away `ldapsearch -H -x -D -w -LLL`, as those just specify the URL
to connect to, the bind credentials and the `-LLL` just quiets down OpenLDAP.
That leaves us with: `-b "o=myhost" cn=root`.
The `-b o=myhost` tells our LDAP server where to _start_ looking in
the tree for entries that might match the search filter, which above is
`cn=root`.
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.
Next, "cn=root" is the search 'filter'. LDAP has a rich specification of
filters, where you can specify `and`, `or`, `not`, `>=`, `<=`, `equal`,
`wildcard`, `present` and a few other esoteric things. Really, `equal`,
`wildcard`, `present` and the boolean operators are all you'll likely ever need.
So, the filter `cn=root` 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".
### The code
So 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 _must_ have a DN so the client knows what record it is, and a set
of attributes. So that's why we did this:
var entry = {
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
attributes: {
2011-08-15 17:53:57 +00:00
cn: record[0],
uid: record[2],
gid: record[3],
description: record[4],
homedirectory: record[5],
shell: record[6] || '',
objectclass: 'unixUser'
2011-08-15 17:26:55 +00:00
}
};
Next, we let ldapjs do all the hard work of figuring out LDAP search filters
for us by calling `req.filter.matches`. If it matched, we return the whole
record with `res.send`. Note 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
`node-ldapjs-riak`, which takes on the fairly difficult task of writing a 'full'
LDAP server over riak.
To demonstrate what ldapjs is doing for you, let's find all users who have a
shell set to `/bin/false` and whose name starts with `p` (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:
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" "(&(shell=/bin/false)(cn=p*))" cn gid
dn: cn=proxy, ou=users, o=myhost
cn: proxy
gid: 13
dn: cn=pulse, ou=users, o=myhost
cn: pulse
gid: 114
## Add
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 `adduser`. Go ahead and add
the following code in as another handler (you'll need a
`var spawn = require('child_process').spawn;` at the top of your file):
server.add('ou=users, o=myhost', pre, function(req, res, next) {
if (!req.dn.rdns[0].cn)
2011-08-15 17:53:57 +00:00
return next(new ldap.ConstraintViolationError('cn required'));
2011-08-15 17:26:55 +00:00
if (req.users[req.dn.rdns[0].cn])
2011-08-15 17:53:57 +00:00
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
2011-08-15 17:26:55 +00:00
var entry = req.toObject().attributes;
if (entry.objectclass.indexOf('unixUser') === -1)
2011-08-15 17:53:57 +00:00
return next(new ldap.ConstraintViolation('entry must be a unixUser'));
2011-08-15 17:26:55 +00:00
var opts = ['-m'];
if (entry.description) {
2011-08-15 17:53:57 +00:00
opts.push('-c');
opts.push(entry.description[0]);
2011-08-15 17:26:55 +00:00
}
if (entry.homedirectory) {
2011-08-15 17:53:57 +00:00
opts.push('-d');
opts.push(entry.homedirectory[0]);
2011-08-15 17:26:55 +00:00
}
if (entry.gid) {
2011-08-15 17:53:57 +00:00
opts.push('-g');
opts.push(entry.gid[0]);
2011-08-15 17:26:55 +00:00
}
if (entry.shell) {
2011-08-15 17:53:57 +00:00
opts.push('-s');
opts.push(entry.shell[0]);
2011-08-15 17:26:55 +00:00
}
if (entry.uid) {
2011-08-15 17:53:57 +00:00
opts.push('-u');
opts.push(entry.uid[0]);
2011-08-15 17:26:55 +00:00
}
opts.push(entry.cn[0]);
var useradd = spawn('useradd', opts);
var messages = [];
useradd.stdout.on('data', function(data) {
2011-08-15 17:53:57 +00:00
messages.push(data.toString());
2011-08-15 17:26:55 +00:00
});
useradd.stderr.on('data', function(data) {
2011-08-15 17:53:57 +00:00
messages.push(data.toString());
2011-08-15 17:26:55 +00:00
});
useradd.on('exit', function(code) {
2011-08-15 17:53:57 +00:00
if (code !== 0) {
var msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
}
res.end();
return next();
2011-08-15 17:26:55 +00:00
});
});
Then, you'll need to be root to have this running, so start your server with
`sudo` (or be root, whatever). Now, go ahead and create a file called
`user.ldif` with the following contents:
dn: cn=ldapjs, ou=users, o=myhost
objectClass: unixUser
cn: ldapjs
shell: /bin/bash
description: Created via ldapadd
Now go ahead and invoke like:
$ ldapadd -H ldap://localhost:1389 -x -D cn=root -w secret -f ./user.ldif
adding new entry "cn=ldapjs, ou=users, o=myhost"
Let's confirm he got added with an ldapsearch:
$ ldapsearch -H ldap://localhost:1389 -LLL -x -D cn=root -w secret -b "ou=users, o=myhost" cn=ldapjs
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
As before, here's a breakdown of the code:
server.add('ou=users, o=myhost', pre, function(req, res, next) {
if (!req.dn.rdns[0].cn)
2011-08-15 17:53:57 +00:00
return next(new ldap.ConstraintViolationError('cn required'));
2011-08-15 17:26:55 +00:00
if (req.users[req.dn.rdns[0].cn])
2011-08-15 17:53:57 +00:00
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
2011-08-15 17:26:55 +00:00
var entry = req.toObject().attributes;
if (entry.objectclass.indexOf('unixUser') === -1)
2011-08-15 17:53:57 +00:00
return next(new ldap.ConstraintViolation('entry must be a unixUser'));
2011-08-15 17:26:55 +00:00
Here's a few new things:
* We mounted this handler at `ou=users, o=myhost`. Why? What if we want to
extend this little project with groups? We probably want those under a
different part of the tree.
* We did some really minimal schema enforcement by:
2011-08-15 20:50:15 +00:00
+ Checking that the leaf RDN (relative distinguished name) was a _cn_
2011-08-15 17:26:55 +00:00
attribute.
2011-08-15 20:50:15 +00:00
+ We then did `req.toObject()`. As mentioned before, each of the req/res
2011-08-15 17:26:55 +00:00
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 `.toObject()`. Note we also filtered
out to the `attributes` portion of the object since that's all we're really
looking at.
2011-08-15 20:50:15 +00:00
+ Lastly, we did a super minimal check to see if the entry was of type
2011-08-15 17:26:55 +00:00
`unixUser`. 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 _objectClass_ over the wire).
After that, we really just delegated off to the _useradd_ command. AFAIK there
is not a node.js module that wraps up `getpwent` and friends, otherwise we'd use
that.
Now, what's missing? Oh, right, we need to let you set a password. Well, let's
support that via the _modify_ command.
## Modify
So 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:
server.modify('ou=users, o=myhost', pre, function(req, res, next) {
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
2011-08-15 17:53:57 +00:00
return next(new ldap.NoSuchObjectError(req.dn.toString()));
2011-08-15 17:26:55 +00:00
if (!req.changes.length)
2011-08-15 17:53:57 +00:00
return next(new ldap.ProtocolError('changes required'));
2011-08-15 17:26:55 +00:00
var user = req.users[req.dn.rdns[0].cn].attributes;
var mod;
for (var i = 0; i < req.changes.length; i++) {
2011-08-15 17:53:57 +00:00
mod = req.changes[i].modification;
switch (req.changes[i].operation) {
case 'replace':
if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
return next(new ldap.UnwillingToPerformError('only password updates ' +
'allowed'));
break;
case 'add':
case 'delete':
return next(new ldap.UnwillingToPerformError('only replace allowed'));
}
2011-08-15 17:26:55 +00:00
}
var passwd = spawn('chpasswd', ['-c', 'MD5']);
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
passwd.on('exit', function(code) {
2011-08-15 17:53:57 +00:00
if (code !== 0)
return next(new ldap.OperationsError(code));
2011-08-15 17:26:55 +00:00
2011-08-15 17:53:57 +00:00
res.end();
return next();
2011-08-15 17:26:55 +00:00
});
});
Basically, we made sure the remote client was targeting an entry that exists,
ensuring that they were asking to "replace" the `userPassword` attribute (which
is the 'standard' LDAP attribute for passwords, for whatever that's worth; if
you think it's easier to use 'password', knock yourself out), and then just
delegating to the `chpasswd` command (which lets you change a user's password
over stdin). Next, go ahead and create a `passwd.ldif` file:
dn: cn=ldapjs, ou=users, o=myhost
changetype: modify
replace: userPassword
userPassword: secret
-
And then run the OpenLDAP CLI like:
$ ldapmodify -H ldap://localhost:1389 -x -D cn=root -w secret -f ./passwd.ldif
You should now be able to login to your box as the ldapjs user. Ok, let's get
the last "mainline" piece of work out of the way, and delte the user.
## Delete
Delete is pretty straightforward. The client gives you a dn to delete, and you
delete it :). Go ahead and add the following code into your server:
server.del('ou=users, o=myhost', pre, function(req, res, next) {
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
2011-08-15 17:53:57 +00:00
return next(new ldap.NoSuchObjectError(req.dn.toString()));
2011-08-15 17:26:55 +00:00
var userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]);
var messages = [];
userdel.stdout.on('data', function(data) {
2011-08-15 17:53:57 +00:00
messages.push(data.toString());
2011-08-15 17:26:55 +00:00
});
userdel.stderr.on('data', function(data) {
2011-08-15 17:53:57 +00:00
messages.push(data.toString());
2011-08-15 17:26:55 +00:00
});
userdel.on('exit', function(code) {
2011-08-15 17:53:57 +00:00
if (code !== 0) {
var msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
}
res.end();
return next();
2011-08-15 17:26:55 +00:00
});
});
And then run the following command:
$ ldapdelete -H ldap://localhost:1389 -x -D cn=root -w secret "cn=ldapjs, ou=users, o=myhost"
This should be pretty much self-explanatory by now :)