docs, and minor enhancements to compare/search apis
This commit is contained in:
parent
73f913b2c4
commit
7198c0a516
4
Makefile
4
Makefile
|
@ -52,6 +52,8 @@ endif
|
||||||
doc: dep
|
doc: dep
|
||||||
@rm -rf ${DOCPKGDIR}
|
@rm -rf ${DOCPKGDIR}
|
||||||
@mkdir -p ${DOCPKGDIR}
|
@mkdir -p ${DOCPKGDIR}
|
||||||
|
${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/client.md
|
||||||
|
${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/examples.md
|
||||||
${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/guide.md
|
${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/guide.md
|
||||||
rm docs/*.json
|
rm docs/*.json
|
||||||
mv docs/*.html ${DOCPKGDIR}
|
mv docs/*.html ${DOCPKGDIR}
|
||||||
|
@ -62,4 +64,4 @@ test: dep lint
|
||||||
$(NPM) test
|
$(NPM) test
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@rm -fr ${DOCPKGDIR} node_modules *.log
|
@rm -fr ${DOCPKGDIR} node_modules *.log *.tar.gz
|
||||||
|
|
|
@ -0,0 +1,287 @@
|
||||||
|
---
|
||||||
|
title: ldapjs
|
||||||
|
brand: spartan
|
||||||
|
markdown2extras: wiki-tables
|
||||||
|
logo-color: green
|
||||||
|
logo-font-family: google:Aldrich, Verdana, sans-serif
|
||||||
|
header-font-family: google:Aldrich, Verdana, sans-serif
|
||||||
|
---
|
||||||
|
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
This documents the ldapjs client API; this assumes that you are familiar with
|
||||||
|
LDAP, so if you're not, hit the [guide](http://ldapjs.org/guide.html) first.
|
||||||
|
|
||||||
|
# Create a client
|
||||||
|
|
||||||
|
The code to create a new client liiks like:
|
||||||
|
|
||||||
|
var client = ldap.createClient({
|
||||||
|
url: 'ldap://127.0.0.1:1389'
|
||||||
|
});
|
||||||
|
|
||||||
|
You can use `ldap://` or `ldaps://`; the latter would connect over SSL (note
|
||||||
|
this will not use the LDAP TLS extended operation, but literally an SSL
|
||||||
|
connection to port 636, as in LDAP v2). Full list of options:
|
||||||
|
|
||||||
|
* _url:_ a valid LDAP url
|
||||||
|
* _socketPath:_ If you're running an LDAP server over a Unix Domain Socket, use
|
||||||
|
this.
|
||||||
|
* _log4js:_ You can optionally pass in a log4js instance that the client will
|
||||||
|
get a logger from. You'll need to set the level to `TRACE` To get any output
|
||||||
|
from the client.
|
||||||
|
* _numConnections:_ The size of the connection pool. Default is 1.
|
||||||
|
|
||||||
|
## Connection management
|
||||||
|
|
||||||
|
If you'll recall, the LDAP protocol is connection-oriented, and completely
|
||||||
|
asynchronous on a connection (meaning you can send as many requests as you want
|
||||||
|
without waiting for responses). However, our friend `bind` is a little
|
||||||
|
different in that you generally want to wait for binds to be completed since
|
||||||
|
subsequent operations assume that level of priviledge.
|
||||||
|
|
||||||
|
The ldapjs client deals with this by maintaing a connection pool, and splaying
|
||||||
|
requests across that connection pool, with the exception of `bind` and `unbind`,
|
||||||
|
which it will apply to all connections in the pool. By default a client will
|
||||||
|
have one connection in the pool (since it's async already, you don't always need
|
||||||
|
the complexity of a pool). And after that, the operations in the client are
|
||||||
|
pretty much a mapping of the LDAP C API, but made higher-level, so they make
|
||||||
|
sense in JS.
|
||||||
|
|
||||||
|
## Common patterns
|
||||||
|
|
||||||
|
The last two parameters in every api are `controls` and `callback`. `controls`
|
||||||
|
can be either a single instance of a `Control` or an array of `Control`. You
|
||||||
|
can, and probably will, omit this option.
|
||||||
|
|
||||||
|
Almost every operation has the callback form of `function(err, res)` where err
|
||||||
|
will be an instance of an ldapjs Error (you can use `instanceof` to switch).
|
||||||
|
You probably won't need to check the `res` parameter, but it's there if you do.
|
||||||
|
|
||||||
|
# bind
|
||||||
|
`bind(dn, password, controls,callback)`
|
||||||
|
|
||||||
|
The bind API only allows LDAP 'simple' binds (equivalent to HTTP Basic
|
||||||
|
Authentication) for now. Note that all client APIs can optionally take an array
|
||||||
|
of `Control` objects. You probably don't need them though...
|
||||||
|
|
||||||
|
If you have > 1 connection in the connection pool, you'll be called back after
|
||||||
|
*all* of the connections are bound, not just the first one.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
client.bind('cn=root', 'secret', function(err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
# add
|
||||||
|
`add(dn, entry, controls, callback)`
|
||||||
|
|
||||||
|
Allows you to add an entry (which is just a plain JS object), and as always,
|
||||||
|
controls are optional.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
var entry = {
|
||||||
|
cn: 'foo',
|
||||||
|
sn: 'bar',
|
||||||
|
email: ['foo@bar.com', 'foo1@bar.com'],
|
||||||
|
objectclass: 'fooPerson'
|
||||||
|
};
|
||||||
|
client.add('cn=foo, o=example', entry, function(err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
# compare
|
||||||
|
`compare(dn, attribute, value, controls, callback)`
|
||||||
|
|
||||||
|
Performs an LDAP compare with the given attribute and value against the entry
|
||||||
|
referenced by dn.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
client.compare('cn=foo, o=example', 'sn', 'bar', function(err, matched) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
console.log('matched: ' + matched);
|
||||||
|
});
|
||||||
|
|
||||||
|
# del
|
||||||
|
`del(dn, controls, callbak)`
|
||||||
|
|
||||||
|
|
||||||
|
Deletes an entry from the LDAP server.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
client.del('cn=foo, o=example', function(err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
# exop
|
||||||
|
`exop(name, value, controls, callback)`
|
||||||
|
|
||||||
|
Performs an LDAP extended operation against an LDAP server. `name` is typically
|
||||||
|
going to be an OID (well, the RFC says it must be. ldapjs has no such
|
||||||
|
restriction). Value is completely arbitrary, and is whatever the exop says it
|
||||||
|
should be.
|
||||||
|
|
||||||
|
Example (performs an LDAP 'whois' extended op):
|
||||||
|
|
||||||
|
client.exop('1.3.6.1.4.1.4203.1.11.3', function(err, value, res) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
console.log('whois: ' + value);
|
||||||
|
});
|
||||||
|
|
||||||
|
# modify
|
||||||
|
`modify(name, changes, controls, callback)`
|
||||||
|
|
||||||
|
Performs an LDAP modify operation against the LDAP server. This API requires
|
||||||
|
you to pass in a `Change` object, which is described below. Note you can pass
|
||||||
|
in a single `Change` or an array of `Change`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
var change = new Change({
|
||||||
|
type: 'add',
|
||||||
|
modification: {
|
||||||
|
pets: ['cat', 'dog']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.modify('cn=foo, o=example', change, function(err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
## Change
|
||||||
|
|
||||||
|
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, is equivalent to a delete.
|
||||||
|
* _add:_ Adds the attribute value(s) referenced in `modification`. The
|
||||||
|
attribute may or may not already exist.
|
||||||
|
* _delete:_ Deletes all the attribute (and all values) referenced in
|
||||||
|
`modification`.
|
||||||
|
|
||||||
|
`modification` is just a plain old JS object with the values you want.
|
||||||
|
|
||||||
|
# modifyDN
|
||||||
|
`modifyDN(dn, newDN, controls, callback)`
|
||||||
|
|
||||||
|
Performs an 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 automagically 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).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
client.modifyDN('cn=foo, o=example', 'cn=bar', function(err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
# search
|
||||||
|
`search(base, options, controls, callback)`
|
||||||
|
|
||||||
|
The search operation is more complex than the operations, so this one takes
|
||||||
|
an options object for all the parameters. However, ldapjs makes some defaults
|
||||||
|
for you so that if you pass nothing in, it's pretty much equivalent to an HTTP
|
||||||
|
GET operation (i.e., base search against the DN, filter set to always match).
|
||||||
|
|
||||||
|
Like every other operation, `base` is a DN string. Options has the following
|
||||||
|
fields:
|
||||||
|
|
||||||
|
* _scope:_ One of `base`, `one`, or `sub`. Defaults to `base`.
|
||||||
|
* _filter:_ A string version of an LDAP filter (see below), or a programatically
|
||||||
|
constructed `Filter` object. Defaults to `(objectclass=*)`.
|
||||||
|
* _attributes:_ attributes to select and return (if these are set, the server
|
||||||
|
will return *only* these attributes). Defaults to the empty set, which means all
|
||||||
|
attributes.
|
||||||
|
* _attrsOnly:_ boolean on whether you want the server to only return the names
|
||||||
|
of the attributes, and not their values. Borderline useless. Defaults to
|
||||||
|
false.
|
||||||
|
* _sizeLimit:_ the maximum number of entries to return. Defaults to 0
|
||||||
|
(unlimited).
|
||||||
|
* _timeLimit:_ the maximum amount of time the server should take in responding,
|
||||||
|
in seconds. Defaults to 10. Lots of servers will ignore this.
|
||||||
|
|
||||||
|
Responses from the `search` method are an `EventEmitter` where you will get a
|
||||||
|
notification for each search entry that comes back from the server. You will
|
||||||
|
additionally be able to listen for an `error` and `end` event. Note that the
|
||||||
|
`error` event will only be for client/TCP errors, not LDAP error codes like the
|
||||||
|
other APIs. You'll want to check the LDAP status code (likely for `0`) on the
|
||||||
|
`end` event to assert success. LDAP search results can give you a lot of status
|
||||||
|
codes, such as time or size exceeded, busy, inappropriate matching, etc., etc.,
|
||||||
|
which is why this method doesn't try to wrap up the code matching.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
filter: '(&(l=Seattle)(email=*@foo.com))',
|
||||||
|
scope: 'sub'
|
||||||
|
};
|
||||||
|
|
||||||
|
client.search('o=example', opts, function(err, res) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
res.on('searchEntry', function(entry) {
|
||||||
|
console.log('entry: ' + JSON.stringify(entry.object));
|
||||||
|
});
|
||||||
|
res.on('error', function(err) {
|
||||||
|
console.error('error: ' + err.message);
|
||||||
|
});
|
||||||
|
res.on('end', function(result) {
|
||||||
|
console.log('status: ' + result.status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
## Filter Strings
|
||||||
|
|
||||||
|
The easiest way to write search filters is to write them compliant with RFC2254,
|
||||||
|
which is the "The string representation of LDAP search filters". Note that
|
||||||
|
ldapjs doesn't support extensible matching, since it's one of those features
|
||||||
|
that almost nobody actually uses in practice.
|
||||||
|
|
||||||
|
Assuming you don't really want to read the RFC, search filters in LDAP are
|
||||||
|
basically are a "tree" of attribute/value assertions, with the tree specified
|
||||||
|
in prefix notation. For example, let's start simple, and build up a complicated
|
||||||
|
filter. The most basic filter is equality, so let's assume you want to search
|
||||||
|
for an attribute `email` with a value of `foo@bar.com`. The syntax would be:
|
||||||
|
|
||||||
|
(email=foo@bar.com)
|
||||||
|
|
||||||
|
Note that ldapjs requires all filters to be surrounded by '()' blocks. Ok, that
|
||||||
|
was easy. Let's now assume you want to find all records where the email is
|
||||||
|
actually just anything in the @bar.com domain, and the location attribute is set
|
||||||
|
to Seattle:
|
||||||
|
|
||||||
|
(&(email=*@bar.com)(l=Seattle))
|
||||||
|
|
||||||
|
Ok, so now our filter is actually three LDAP filters. We have an `and` filter,
|
||||||
|
an `equality` filter (the l=Seattle), and a `substring` filter. Substrings are
|
||||||
|
wildcard filters. Now, let's say we want to also set our filter to include a
|
||||||
|
specification that either the employeeType *not* be a manager or a secretary:
|
||||||
|
|
||||||
|
(&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary))))
|
||||||
|
|
||||||
|
It gets a little bit complicated, but it's actually quite powerful, and lets you
|
||||||
|
find almost anything you're looking for.
|
||||||
|
|
||||||
|
# unbind
|
||||||
|
`unbind(callback)`
|
||||||
|
|
||||||
|
The unbind operation takes no parameters other than a callback, and will unbind
|
||||||
|
(and disconnect) *all* of the connections in the pool.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
client.unbind(function(err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
});
|
|
@ -0,0 +1,419 @@
|
||||||
|
---
|
||||||
|
title: ldapjs
|
||||||
|
brand: spartan
|
||||||
|
markdown2extras: wiki-tables
|
||||||
|
logo-color: green
|
||||||
|
logo-font-family: google:Aldrich, Verdana, sans-serif
|
||||||
|
header-font-family: google:Aldrich, Verdana, sans-serif
|
||||||
|
---
|
||||||
|
|
||||||
|
# In-memory server
|
||||||
|
|
||||||
|
var ldap = require('ldapjs');
|
||||||
|
|
||||||
|
|
||||||
|
///--- Shared handlers
|
||||||
|
|
||||||
|
function authorize(req, res, next) {
|
||||||
|
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
||||||
|
return next(new ldap.InsufficientAccessRightsError());
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///--- Globals
|
||||||
|
|
||||||
|
var SUFFIX = 'o=joyent';
|
||||||
|
var db = {};
|
||||||
|
var server = ldap.createServer();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.add(SUFFIX, authorize, function(req, res, next) {
|
||||||
|
var dn = req.dn.toString();
|
||||||
|
|
||||||
|
if (db[dn])
|
||||||
|
return next(new ldap.EntryAlreadyExistsError(dn));
|
||||||
|
|
||||||
|
db[dn] = req.toObject().attributes;
|
||||||
|
res.end();
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.bind(SUFFIX, function(req, res, next) {
|
||||||
|
var dn = req.dn.toString();
|
||||||
|
if (!db[dn])
|
||||||
|
return next(new ldap.NoSuchObjectError(dn));
|
||||||
|
|
||||||
|
if (!dn[dn].userpassword)
|
||||||
|
return next(new ldap.NoSuchAttributeError('userPassword'));
|
||||||
|
|
||||||
|
if (db[dn].userpassword !== req.credentials)
|
||||||
|
return next(new ldap.InvalidCredentialsError());
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.compare(SUFFIX, authorize, function(req, res, next) {
|
||||||
|
var dn = req.dn.toString();
|
||||||
|
if (!db[dn])
|
||||||
|
return next(new ldap.NoSuchObjectError(dn));
|
||||||
|
|
||||||
|
if (!db[dn][req.attribute])
|
||||||
|
return next(new ldap.NoSuchAttributeError(req.attribute));
|
||||||
|
|
||||||
|
var matches = false;
|
||||||
|
var vals = db[dn][req.attribute];
|
||||||
|
for (var i = 0; i < vals.length; i++) {
|
||||||
|
if (vals[i] === req.value) {
|
||||||
|
matches = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end(matches);
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.del(SUFFIX, authorize, function(req, res, next) {
|
||||||
|
var dn = req.dn.toString();
|
||||||
|
if (!db[dn])
|
||||||
|
return next(new ldap.NoSuchObjectError(dn));
|
||||||
|
|
||||||
|
delete db[dn];
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.modify(SUFFIX, authorize, function(req, res, next) {
|
||||||
|
var dn = req.dn.toString();
|
||||||
|
if (!req.changes.length)
|
||||||
|
return next(new ldap.ProtocolError('changes required'));
|
||||||
|
if (!db[dn])
|
||||||
|
return next(new ldap.NoSuchObjectError(dn));
|
||||||
|
|
||||||
|
var entry = db[dn];
|
||||||
|
|
||||||
|
for (var i = 0; i < req.changes.length; i++) {
|
||||||
|
mod = req.changes[i].modification;
|
||||||
|
switch (req.changes[i].operation) {
|
||||||
|
case 'replace':
|
||||||
|
if (!entry[mod.type])
|
||||||
|
return next(new ldap.NoSuchAttributeError(mod.type));
|
||||||
|
|
||||||
|
if (!mod.vals || !mod.vals.length) {
|
||||||
|
delete entry[mod.type];
|
||||||
|
} else {
|
||||||
|
entry[mod.type] = mod.vals;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'add':
|
||||||
|
if (!entry[mod.type]) {
|
||||||
|
entry[mod.type] = mod.vals;
|
||||||
|
} else {
|
||||||
|
mod.vals.forEach(function(v) {
|
||||||
|
if (entry[mod.type].indexOf(v) === -1)
|
||||||
|
entry[mod.type].push(v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
if (!entry[mod.type])
|
||||||
|
return next(new ldap.NoSuchAttributeError(mod.type));
|
||||||
|
|
||||||
|
delete entry[mod.type];
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.search(SUFFIX, authorize, function(req, res, next) {
|
||||||
|
var dn = req.dn.toString();
|
||||||
|
if (!db[dn])
|
||||||
|
return next(new ldap.NoSuchObjectError(dn));
|
||||||
|
|
||||||
|
var scopeCheck;
|
||||||
|
|
||||||
|
switch (req.scope) {
|
||||||
|
case 'base':
|
||||||
|
if (req.filter.matches(db[dn])) {
|
||||||
|
res.send({
|
||||||
|
dn: dn,
|
||||||
|
attributes: db[dn]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
return next();
|
||||||
|
|
||||||
|
case 'one':
|
||||||
|
scopeCheck = function(k) {
|
||||||
|
if (req.dn.equals(k))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var parent = ldap.parseDN(k).parent();
|
||||||
|
return (parent ? parent.equals(req.dn) : false);
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sub':
|
||||||
|
scopeCheck = function(k) {
|
||||||
|
return (req.dn.equals(k) || req.dn.parentOf(k));
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(db).forEach(function(key) {
|
||||||
|
if (!scopeCheck(key))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (req.filter.matches(db[key])) {
|
||||||
|
res.send({
|
||||||
|
dn: key,
|
||||||
|
attributes: db[key]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///--- Fire it up
|
||||||
|
|
||||||
|
server.listen(1389, function() {
|
||||||
|
console.log('LDAP server up at: %s', server.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
# /etc/passwd server
|
||||||
|
|
||||||
|
var fs = require('fs');
|
||||||
|
var ldap = require('ldapjs');
|
||||||
|
var spawn = require('child_process').spawn;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///--- Shared handlers
|
||||||
|
|
||||||
|
function authorize(req, res, next) {
|
||||||
|
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
||||||
|
return next(new ldap.InsufficientAccessRightsError());
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadPasswdFile(req, res, next) {
|
||||||
|
fs.readFile('/etc/passwd', 'utf8', function(err, data) {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var pre = [authorize, loadPasswdFile];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///--- Mainline
|
||||||
|
|
||||||
|
var server = ldap.createServer();
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
server.add('ou=users, o=myhost', pre, function(req, res, next) {
|
||||||
|
if (!req.dn.rdns[0].cn)
|
||||||
|
return next(new ldap.ConstraintViolationError('cn required'));
|
||||||
|
|
||||||
|
if (req.users[req.dn.rdns[0].cn])
|
||||||
|
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
||||||
|
|
||||||
|
var entry = req.toObject().attributes;
|
||||||
|
|
||||||
|
if (entry.objectclass.indexOf('unixUser') === -1)
|
||||||
|
return next(new ldap.ConstraintViolation('entry must be a unixUser'));
|
||||||
|
|
||||||
|
var opts = ['-m'];
|
||||||
|
if (entry.description) {
|
||||||
|
opts.push('-c');
|
||||||
|
opts.push(entry.description[0]);
|
||||||
|
}
|
||||||
|
if (entry.homedirectory) {
|
||||||
|
opts.push('-d');
|
||||||
|
opts.push(entry.homedirectory[0]);
|
||||||
|
}
|
||||||
|
if (entry.gid) {
|
||||||
|
opts.push('-g');
|
||||||
|
opts.push(entry.gid[0]);
|
||||||
|
}
|
||||||
|
if (entry.shell) {
|
||||||
|
opts.push('-s');
|
||||||
|
opts.push(entry.shell[0]);
|
||||||
|
}
|
||||||
|
if (entry.uid) {
|
||||||
|
opts.push('-u');
|
||||||
|
opts.push(entry.uid[0]);
|
||||||
|
}
|
||||||
|
opts.push(entry.cn[0]);
|
||||||
|
var useradd = spawn('useradd', opts);
|
||||||
|
|
||||||
|
var messages = [];
|
||||||
|
|
||||||
|
useradd.stdout.on('data', function(data) {
|
||||||
|
messages.push(data.toString());
|
||||||
|
});
|
||||||
|
useradd.stderr.on('data', function(data) {
|
||||||
|
messages.push(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
useradd.on('exit', function(code) {
|
||||||
|
if (code !== 0) {
|
||||||
|
var msg = '' + code;
|
||||||
|
if (messages.length)
|
||||||
|
msg += ': ' + messages.join();
|
||||||
|
return next(new ldap.OperationsError(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
server.modify('ou=users, o=myhost', pre, function(req, res, next) {
|
||||||
|
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
|
||||||
|
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||||
|
|
||||||
|
if (!req.changes.length)
|
||||||
|
return next(new ldap.ProtocolError('changes required'));
|
||||||
|
|
||||||
|
var user = req.users[req.dn.rdns[0].cn].attributes;
|
||||||
|
var mod;
|
||||||
|
|
||||||
|
for (var i = 0; i < req.changes.length; i++) {
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwd = spawn('chpasswd', ['-c', 'MD5']);
|
||||||
|
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
|
||||||
|
|
||||||
|
passwd.on('exit', function(code) {
|
||||||
|
if (code !== 0)
|
||||||
|
return next(new ldap.OperationsError('' + code));
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
server.del('ou=users, o=myhost', pre, function(req, res, next) {
|
||||||
|
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
|
||||||
|
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||||
|
|
||||||
|
var userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]);
|
||||||
|
|
||||||
|
var messages = [];
|
||||||
|
userdel.stdout.on('data', function(data) {
|
||||||
|
messages.push(data.toString());
|
||||||
|
});
|
||||||
|
userdel.stderr.on('data', function(data) {
|
||||||
|
messages.push(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
userdel.on('exit', function(code) {
|
||||||
|
if (code !== 0) {
|
||||||
|
var msg = '' + code;
|
||||||
|
if (messages.length)
|
||||||
|
msg += ': ' + messages.join();
|
||||||
|
return next(new ldap.OperationsError(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
server.search('o=myhost', pre, function(req, res, next) {
|
||||||
|
Object.keys(req.users).forEach(function(k) {
|
||||||
|
if (req.filter.matches(req.users[k].attributes))
|
||||||
|
res.send(req.users[k]);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// LDAP "standard" listens on 389, but whatever.
|
||||||
|
server.listen(1389, '127.0.0.1', function() {
|
||||||
|
console.log('/etc/passwd LDAP server up at: %s', server.url);
|
||||||
|
});
|
219
docs/guide.md
219
docs/guide.md
|
@ -86,7 +86,7 @@ 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,
|
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
|
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
|
server implementations support arbitrary "backends" for persistence, but really
|
||||||
you'll be using [BDB]: http://www.oracle.com/technetwork/database/berkeleydb/overview/index.html
|
you'll be using [BDB](http://www.oracle.com/technetwork/database/berkeleydb/overview/index.html).
|
||||||
|
|
||||||
Want to run schemaless in ldapjs, or wire it up with some mongoose models? No
|
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
|
problem. Want to back it to redis? Should be able to get some basics up in a
|
||||||
|
@ -132,6 +132,9 @@ 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=*
|
$ 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.
|
||||||
|
|
||||||
## Bind
|
## Bind
|
||||||
|
|
||||||
So, lesson #1 about LDAP: unlike HTTP, it's connection-oriented; that means that
|
So, lesson #1 about LDAP: unlike HTTP, it's connection-oriented; that means that
|
||||||
|
@ -626,217 +629,3 @@ And then run the following command:
|
||||||
|
|
||||||
This should be pretty much self-explanatory by now :)
|
This should be pretty much self-explanatory by now :)
|
||||||
|
|
||||||
# The code in its entirety
|
|
||||||
|
|
||||||
If you got tired of following along (this would be the tl;dr section), here's
|
|
||||||
the complete implementation for what we went through above:
|
|
||||||
|
|
||||||
var fs = require('fs');
|
|
||||||
var ldap = require('ldapjs');
|
|
||||||
var spawn = require('child_process').spawn;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///--- Shared handlers
|
|
||||||
|
|
||||||
function authorize(req, res, next) {
|
|
||||||
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
|
||||||
return next(new ldap.InsufficientAccessRightsError());
|
|
||||||
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function loadPasswdFile(req, res, next) {
|
|
||||||
fs.readFile('/etc/passwd', 'utf8', function(err, data) {
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var pre = [authorize, loadPasswdFile];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///--- Mainline
|
|
||||||
|
|
||||||
var server = ldap.createServer();
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
server.add('ou=users, o=myhost', pre, function(req, res, next) {
|
|
||||||
if (!req.dn.rdns[0].cn)
|
|
||||||
return next(new ldap.ConstraintViolationError('cn required'));
|
|
||||||
|
|
||||||
if (req.users[req.dn.rdns[0].cn])
|
|
||||||
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
|
||||||
|
|
||||||
var entry = req.toObject().attributes;
|
|
||||||
|
|
||||||
if (entry.objectclass.indexOf('unixUser') === -1)
|
|
||||||
return next(new ldap.ConstraintViolation('entry must be a unixUser'));
|
|
||||||
|
|
||||||
var opts = ['-m'];
|
|
||||||
if (entry.description) {
|
|
||||||
opts.push('-c');
|
|
||||||
opts.push(entry.description[0]);
|
|
||||||
}
|
|
||||||
if (entry.homedirectory) {
|
|
||||||
opts.push('-d');
|
|
||||||
opts.push(entry.homedirectory[0]);
|
|
||||||
}
|
|
||||||
if (entry.gid) {
|
|
||||||
opts.push('-g');
|
|
||||||
opts.push(entry.gid[0]);
|
|
||||||
}
|
|
||||||
if (entry.shell) {
|
|
||||||
opts.push('-s');
|
|
||||||
opts.push(entry.shell[0]);
|
|
||||||
}
|
|
||||||
if (entry.uid) {
|
|
||||||
opts.push('-u');
|
|
||||||
opts.push(entry.uid[0]);
|
|
||||||
}
|
|
||||||
opts.push(entry.cn[0]);
|
|
||||||
var useradd = spawn('useradd', opts);
|
|
||||||
|
|
||||||
var messages = [];
|
|
||||||
|
|
||||||
useradd.stdout.on('data', function(data) {
|
|
||||||
messages.push(data.toString());
|
|
||||||
});
|
|
||||||
useradd.stderr.on('data', function(data) {
|
|
||||||
messages.push(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
useradd.on('exit', function(code) {
|
|
||||||
if (code !== 0) {
|
|
||||||
var msg = '' + code;
|
|
||||||
if (messages.length)
|
|
||||||
msg += ': ' + messages.join();
|
|
||||||
return next(new ldap.OperationsError(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
server.modify('ou=users, o=myhost', pre, function(req, res, next) {
|
|
||||||
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
|
|
||||||
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
|
||||||
|
|
||||||
if (!req.changes.length)
|
|
||||||
return next(new ldap.ProtocolError('changes required'));
|
|
||||||
|
|
||||||
var user = req.users[req.dn.rdns[0].cn].attributes;
|
|
||||||
var mod;
|
|
||||||
|
|
||||||
for (var i = 0; i < req.changes.length; i++) {
|
|
||||||
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'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var passwd = spawn('chpasswd', ['-c', 'MD5']);
|
|
||||||
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
|
|
||||||
|
|
||||||
passwd.on('exit', function(code) {
|
|
||||||
if (code !== 0)
|
|
||||||
return next(new ldap.OperationsError('' + code));
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
server.del('ou=users, o=myhost', pre, function(req, res, next) {
|
|
||||||
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
|
|
||||||
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
|
||||||
|
|
||||||
var userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]);
|
|
||||||
|
|
||||||
var messages = [];
|
|
||||||
userdel.stdout.on('data', function(data) {
|
|
||||||
messages.push(data.toString());
|
|
||||||
});
|
|
||||||
userdel.stderr.on('data', function(data) {
|
|
||||||
messages.push(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
userdel.on('exit', function(code) {
|
|
||||||
if (code !== 0) {
|
|
||||||
var msg = '' + code;
|
|
||||||
if (messages.length)
|
|
||||||
msg += ': ' + messages.join();
|
|
||||||
return next(new ldap.OperationsError(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
server.search('o=myhost', pre, function(req, res, next) {
|
|
||||||
Object.keys(req.users).forEach(function(k) {
|
|
||||||
if (req.filter.matches(req.users[k].attributes))
|
|
||||||
res.send(req.users[k]);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// LDAP "standard" listens on 389, but whatever.
|
|
||||||
server.listen(1389, '127.0.0.1', function() {
|
|
||||||
console.log('/etc/passwd LDAP server up at: %s', server.url);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ function Client(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log4js = options.log4js || logStub;
|
this.log4js = options.log4js || logStub;
|
||||||
this.numConnections = Math.abs(options.numConnections) || 3;
|
this.numConnections = Math.abs(options.numConnections) || 1;
|
||||||
this.connections = [];
|
this.connections = [];
|
||||||
this.currentConnection = 0;
|
this.currentConnection = 0;
|
||||||
this.connectOptions = options.socketPath ? options.socketPath : {
|
this.connectOptions = options.socketPath ? options.socketPath : {
|
||||||
|
|
|
@ -21,3 +21,16 @@ function CompareResponse(options) {
|
||||||
}
|
}
|
||||||
util.inherits(CompareResponse, LDAPResult);
|
util.inherits(CompareResponse, LDAPResult);
|
||||||
module.exports = CompareResponse;
|
module.exports = CompareResponse;
|
||||||
|
|
||||||
|
|
||||||
|
CompareResponse.prototype.end = function(matches) {
|
||||||
|
var status = 0x06; // Compare true
|
||||||
|
if (typeof(matches) === 'number') {
|
||||||
|
status = matches;
|
||||||
|
} else if (typeof(matches) === 'boolean') {
|
||||||
|
if (!matches)
|
||||||
|
status = 0x05; // Compare false
|
||||||
|
}
|
||||||
|
|
||||||
|
return LDAPResult.prototype.end.call(this, status);
|
||||||
|
};
|
||||||
|
|
|
@ -63,6 +63,11 @@ SearchEntry.prototype.addAttribute = function(attr) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
SearchEntry.prototype.toObject = function() {
|
||||||
|
return this.object;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
SearchEntry.prototype.fromObject = function(obj) {
|
SearchEntry.prototype.fromObject = function(obj) {
|
||||||
if (typeof(obj) !== 'object')
|
if (typeof(obj) !== 'object')
|
||||||
throw new TypeError('object required');
|
throw new TypeError('object required');
|
||||||
|
@ -95,7 +100,6 @@ SearchEntry.prototype.fromObject = function(obj) {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
SearchEntry.prototype.setAttributes = function(obj) {
|
SearchEntry.prototype.setAttributes = function(obj) {
|
||||||
if (typeof(obj) !== 'object')
|
if (typeof(obj) !== 'object')
|
||||||
throw new TypeError('object required');
|
throw new TypeError('object required');
|
||||||
|
|
|
@ -49,10 +49,7 @@ test('setup', function(t) {
|
||||||
});
|
});
|
||||||
|
|
||||||
server.compare(SUFFIX, function(req, res, next) {
|
server.compare(SUFFIX, function(req, res, next) {
|
||||||
if (req.value !== 'test')
|
res.end(req.value === 'test');
|
||||||
return next(new ldap.CompareFalseError('value was test'));
|
|
||||||
|
|
||||||
res.end(ldap.LDAP_COMPARE_TRUE);
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue