Merge branch 'master' into bugfix_typos

This commit is contained in:
Tony Brix 2021-02-24 22:53:58 -06:00 committed by GitHub
commit be2b8757d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1343 additions and 1143 deletions

View File

@ -1,51 +1,7 @@
</div><!-- end #content -->
<script type="text/javascript" charset="utf-8">
$(function() {
var headerHeight = $("#header").height();
var offsets = [];
var current = -1;
function endpoint(scrollDistance) {
if (scrollDistance < offsets[0]) {
return -1;
} else {
for (var id = offsets.length; id > 0; id--) {
if (scrollDistance > offsets[id - 1]) {
return id - 1;
break;
}
}
}
}
$("h2").each(function(i) {
offsets.push($(this).offset().top - headerHeight)
});
$("#content").append('<h2 class="fixed" style="display: none"><span>&nbsp;</span></h2>');
var fixed_h2 = $("h2.fixed");
var fixed_span = $("h2.fixed span");
$("#content").scroll(function() {
var scrollDistance = $("#content").attr('scrollTop');
var now = endpoint(scrollDistance);
if (now !== current) {
$("#sidebar li").removeClass("current");
current = now;
if (current < 0) {
fixed_h2.hide();
} else if (current >= 0) {
var heading = $($("h2 span")[current]).text();
$("#sidebar a[href|=#" + heading.replace(' ', '-') + "]").parent().addClass("current");
fixed_span.text(heading);
fixed_h2.show();
}
}
});
});
</script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript" src="media/js/script.js"></script>
</body>
</html>

View File

@ -4,7 +4,7 @@
<title>%(title)s</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="media/css/style.css">
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<link rel="stylesheet" type="text/css" href="media/css/highlight.css">
</head>
<body>
<div id="header">

View File

@ -0,0 +1,47 @@
/* eslint-disable */
$(function() {
var headerHeight = $("#header").height();
var offsets = [];
var current = -1;
function endpoint(scrollDistance) {
if (scrollDistance < offsets[0]) {
return -1;
} else {
for (var id = offsets.length; id > 0; id--) {
if (scrollDistance > offsets[id - 1]) {
return id - 1;
break;
}
}
}
}
$("h2").each(function(i) {
offsets.push($(this).offset().top - headerHeight)
});
$("#content").append('<h2 class="fixed" style="display: none"><span>&nbsp;</span></h2>');
var fixed_h2 = $("h2.fixed");
var fixed_span = $("h2.fixed span");
$("#content").scroll(function() {
var scrollDistance = $("#content").attr('scrollTop');
var now = endpoint(scrollDistance);
if (now !== current) {
$("#sidebar li").removeClass("current");
current = now;
if (current < 0) {
fixed_h2.hide();
} else if (current >= 0) {
var heading = $($("h2 span")[current]).text();
$("#sidebar a[href|=#" + heading.replace(' ', '-') + "]").parent().addClass("current");
fixed_span.text(heading);
fixed_h2.show();
}
}
});
});

View File

@ -15,14 +15,17 @@ with LDAP. If you're not, read the [guide](guide.html) first.
The code to create a new client looks like:
var ldap = require('ldapjs');
var client = ldap.createClient({
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
});
```js
const ldap = require('ldapjs');
client.on('error', (err) => {
const client = ldap.createClient({
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
});
client.on('error', (err) => {
// handle connection error
})
})
```
You can use `ldap://` or `ldaps://`; the latter would connect over SSL (note
that this will not use the LDAP TLS extended operation, but literally an SSL
@ -104,9 +107,6 @@ Almost every operation has the callback form of `function(err, res)` where err
will be an instance of an `LDAPError` (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)`
@ -118,9 +118,11 @@ of `Control` objects. You probably don't need them though...
Example:
client.bind('cn=root', 'secret', function(err) {
```js
client.bind('cn=root', 'secret', (err) => {
assert.ifError(err);
});
});
```
# add
`add(dn, entry, controls, callback)`
@ -132,15 +134,17 @@ controls are optional.
Example:
var entry = {
```js
const entry = {
cn: 'foo',
sn: 'bar',
email: ['foo@bar.com', 'foo1@bar.com'],
objectclass: 'fooPerson'
};
client.add('cn=foo, o=example', entry, function(err) {
};
client.add('cn=foo, o=example', entry, (err) => {
assert.ifError(err);
});
});
```
# compare
`compare(dn, attribute, value, controls, callback)`
@ -150,11 +154,13 @@ the entry referenced by dn.
Example:
client.compare('cn=foo, o=example', 'sn', 'bar', function(err, matched) {
```js
client.compare('cn=foo, o=example', 'sn', 'bar', (err, matched) => {
assert.ifError(err);
console.log('matched: ' + matched);
});
});
```
# del
`del(dn, controls, callback)`
@ -164,9 +170,11 @@ Deletes an entry from the LDAP server.
Example:
client.del('cn=foo, o=example', function(err) {
```js
client.del('cn=foo, o=example', (err) => {
assert.ifError(err);
});
});
```
# exop
`exop(name, value, controls, callback)`
@ -178,11 +186,13 @@ 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) {
```js
client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => {
assert.ifError(err);
console.log('whois: ' + value);
});
});
```
# modify
`modify(name, changes, controls, callback)`
@ -193,16 +203,18 @@ pass in a single `Change` or an array of `Change` objects.
Example:
var change = new ldap.Change({
```js
const change = new ldap.Change({
operation: 'add',
modification: {
pets: ['cat', 'dog']
}
});
});
client.modify('cn=foo, o=example', change, function(err) {
client.modify('cn=foo, o=example', change, (err) => {
assert.ifError(err);
});
});
```
## Change
@ -232,9 +244,11 @@ as opposed to just renaming the leaf).
Example:
client.modifyDN('cn=foo, o=example', 'cn=bar', function(err) {
```js
client.modifyDN('cn=foo, o=example', 'cn=bar', (err) => {
assert.ifError(err);
});
});
```
# search
`search(base, options, controls, callback)`
@ -274,28 +288,30 @@ the code matching.
Example:
var opts = {
```js
const opts = {
filter: '(&(l=Seattle)(email=*@foo.com))',
scope: 'sub',
attributes: ['dn', 'sn', 'cn']
};
};
client.search('o=example', opts, function(err, res) {
client.search('o=example', opts, (err, res) => {
assert.ifError(err);
res.on('searchEntry', function(entry) {
res.on('searchEntry', (entry) => {
console.log('entry: ' + JSON.stringify(entry.object));
});
res.on('searchReference', function(referral) {
res.on('searchReference', (referral) => {
console.log('referral: ' + referral.uris.join());
});
res.on('error', function(err) {
res.on('error', (err) => {
console.error('error: ' + err.message);
});
res.on('end', function(result) {
res.on('end', (result) => {
console.log('status: ' + result.status);
});
});
});
```
## Filter Strings
@ -310,14 +326,18 @@ 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)
```
(email=foo@bar.com)
```
ldapjs requires all filters to be surrounded by '()' blocks. Ok, that was easy.
Let's now assume that 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))
```
(&(email=*@bar.com)(l=Seattle))
```
Now our filter is actually three LDAP filters. We have an `and` filter (single
amp `&`), an `equality` filter `(the l=Seattle)`, and a `substring` filter.
@ -328,7 +348,9 @@ to match any email of @bar.com or its subdomains like `"example@foo.bar.com"`.
Now, let's say we also want to set our filter to include a
specification that either the employeeType *not* be a manager nor a secretary:
(&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary))))
```
(&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary))))
```
The `not` character is represented as a `!`, the `or` as a single pipe `|`.
It gets a little bit complicated, but it's actually quite powerful, and lets you
@ -343,27 +365,29 @@ While callers could choose to do this manually via the `controls` parameter to
most simple way to use the paging automation is to set the `paged` option to
true when performing a search:
var opts = {
```js
const opts = {
filter: '(objectclass=commonobject)',
scope: 'sub',
paged: true,
sizeLimit: 200
};
client.search('o=largedir', opts, function(err, res) {
};
client.search('o=largedir', opts, (err, res) => {
assert.ifError(err);
res.on('searchEntry', function(entry) {
res.on('searchEntry', (entry) => {
// do per-entry processing
});
res.on('page', function(result) {
res.on('page', (result) => {
console.log('page end');
});
res.on('error', function(resErr) {
res.on('error', (resErr) => {
assert.ifError(resErr);
});
res.on('end', function(result) {
res.on('end', (result) => {
console.log('done ');
});
});
});
```
This will enable paging with a default page size of 199 (`sizeLimit` - 1) and
will output all of the resulting objects via the `searchEntry` event. At the
@ -381,32 +405,34 @@ client will wait to request the next page until that callback is executed.
Here is an example where both of those parameters are used:
var queue = new MyWorkQueue(someSlowWorkFunction);
var opts = {
```js
const queue = new MyWorkQueue(someSlowWorkFunction);
const opts = {
filter: '(objectclass=commonobject)',
scope: 'sub',
paged: {
pageSize: 250,
pagePause: true
},
};
client.search('o=largerdir', opts, function(err, res) {
};
client.search('o=largerdir', opts, (err, res) => {
assert.ifError(err);
res.on('searchEntry', function(entry) {
res.on('searchEntry', (entry) => {
// Submit incoming objects to queue
queue.push(entry);
});
res.on('page', function(result, cb) {
res.on('page', (result, cb) => {
// Allow the queue to flush before fetching next page
queue.cbWhenFlushed(cb);
});
res.on('error', function(resErr) {
res.on('error', (resErr) => {
assert.ifError(resErr);
});
res.on('end', function(result) {
res.on('end', (result) => {
console.log('done');
});
});
});
```
# starttls
`starttls(options, controls, callback)`
@ -415,15 +441,17 @@ Attempt to secure existing LDAP connection via STARTTLS.
Example:
var opts = {
```js
const opts = {
ca: [fs.readFileSync('mycacert.pem')]
};
};
client.starttls(opts, function(err, res) {
client.starttls(opts, (err, res) => {
assert.ifError(err);
// Client communication now TLS protected
});
});
```
# unbind
@ -440,6 +468,8 @@ not have a response.
Example:
client.unbind(function(err) {
```js
client.unbind((err) => {
assert.ifError(err);
});
});
```

View File

@ -26,10 +26,12 @@ The `parseDN` API converts a string representation of a DN into an ldapjs DN
object; in most cases this will be handled for you under the covers of the
ldapjs framework, but if you need it, it's there.
var parseDN = require('ldapjs').parseDN;
```js
const parseDN = require('ldapjs').parseDN;
var dn = parseDN('cn=foo+sn=bar, ou=people, o=example');
console.log(dn.toString());
const dn = parseDN('cn=foo+sn=bar, ou=people, o=example');
console.log(dn.toString());
```
# DN
@ -41,40 +43,46 @@ APIs are setup to give you a DN object.
Returns a boolean indicating whether 'this' is a child of the passed in dn. The
`dn` argument can be either a string or a DN.
server.add('o=example', function(req, res, next) {
```js
server.add('o=example', (req, res, next) => {
if (req.dn.childOf('ou=people, o=example')) {
...
} else {
...
}
});
});
```
## parentOf(dn)
The inverse of `childOf`; returns a boolean on whether or not `this` is a parent
of the passed in dn. Like `childOf`, can take either a string or a DN.
server.add('o=example', function(req, res, next) {
var dn = parseDN('ou=people, o=example');
```js
server.add('o=example', (req, res, next) => {
const dn = parseDN('ou=people, o=example');
if (dn.parentOf(req.dn)) {
...
} else {
...
}
});
});
```
## equals(dn)
Returns a boolean indicating whether `this` is equivalent to the passed in `dn`
argument. `dn` can be a string or a DN.
server.add('o=example', function(req, res, next) {
```js
server.add('o=example', (req, res, next) => {
if (req.dn.equals('cn=foo, ou=people, o=example')) {
...
} else {
...
}
});
});
```
## parent()
@ -112,6 +120,8 @@ It accepts the same parameters as `format`.
Returns the string representation of `this`.
server.add('o=example', function(req, res, next) {
```js
server.add('o=example', (req, res, next) => {
console.log(req.dn.toString());
});
});
```

View File

@ -17,12 +17,13 @@ a `stack` property correctly set.
In general, you'll be using the errors in ldapjs like:
var ldap = require('ldapjs');
```js
const ldap = require('ldapjs');
var db = {};
const db = {};
server.add('o=example', function(req, res, next) {
var parent = req.dn.parent();
server.add('o=example', (req, res, next) => {
const parent = req.dn.parent();
if (parent) {
if (!db[parent.toString()])
return next(new ldap.NoSuchObjectError(parent.toString()));
@ -31,7 +32,8 @@ In general, you'll be using the errors in ldapjs like:
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
...
});
});
```
I.e., if you just pass them into the `next()` handler, ldapjs will automatically
return the appropriate LDAP error message, and stop the handler chain.

View File

@ -13,39 +13,40 @@ with ldapjs.
# In-memory server
var ldap = require('ldapjs');
```js
const ldap = require('ldapjs');
///--- Shared handlers
///--- Shared handlers
function authorize(req, res, next) {
function authorize(req, res, next) {
/* Any user may search after bind, only cn=root has full power */
var isSearch = (req instanceof ldap.SearchRequest);
const isSearch = (req instanceof ldap.SearchRequest);
if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch)
return next(new ldap.InsufficientAccessRightsError());
return next();
}
}
///--- Globals
///--- Globals
var SUFFIX = 'o=joyent';
var db = {};
var server = ldap.createServer();
const SUFFIX = 'o=joyent';
const db = {};
const server = ldap.createServer();
server.bind('cn=root', function(req, res, next) {
server.bind('cn=root', (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();
server.add(SUFFIX, authorize, (req, res, next) => {
const dn = req.dn.toString();
if (db[dn])
return next(new ldap.EntryAlreadyExistsError(dn));
@ -53,10 +54,10 @@ with ldapjs.
db[dn] = req.toObject().attributes;
res.end();
return next();
});
});
server.bind(SUFFIX, function(req, res, next) {
var dn = req.dn.toString();
server.bind(SUFFIX, (req, res, next) => {
const dn = req.dn.toString();
if (!db[dn])
return next(new ldap.NoSuchObjectError(dn));
@ -68,20 +69,20 @@ with ldapjs.
res.end();
return next();
});
});
server.compare(SUFFIX, authorize, function(req, res, next) {
var dn = req.dn.toString();
server.compare(SUFFIX, authorize, (req, res, next) => {
const 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) {
const matches = false;
const vals = db[dn][req.attribute];
for (const value of vals) {
if (value === req.value) {
matches = true;
break;
}
@ -89,10 +90,10 @@ with ldapjs.
res.end(matches);
return next();
});
});
server.del(SUFFIX, authorize, function(req, res, next) {
var dn = req.dn.toString();
server.del(SUFFIX, authorize, (req, res, next) => {
const dn = req.dn.toString();
if (!db[dn])
return next(new ldap.NoSuchObjectError(dn));
@ -100,20 +101,20 @@ with ldapjs.
res.end();
return next();
});
});
server.modify(SUFFIX, authorize, function(req, res, next) {
var dn = req.dn.toString();
server.modify(SUFFIX, authorize, (req, res, next) => {
const 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];
const entry = db[dn];
for (var i = 0; i < req.changes.length; i++) {
mod = req.changes[i].modification;
switch (req.changes[i].operation) {
for (const change of req.changes) {
mod = change.modification;
switch (change.operation) {
case 'replace':
if (!entry[mod.type])
return next(new ldap.NoSuchAttributeError(mod.type));
@ -130,10 +131,10 @@ with ldapjs.
if (!entry[mod.type]) {
entry[mod.type] = mod.vals;
} else {
mod.vals.forEach(function(v) {
for (const v of mod.vals) {
if (entry[mod.type].indexOf(v) === -1)
entry[mod.type].push(v);
});
}
}
break;
@ -150,14 +151,14 @@ with ldapjs.
res.end();
return next();
});
});
server.search(SUFFIX, authorize, function(req, res, next) {
var dn = req.dn.toString();
server.search(SUFFIX, authorize, (req, res, next) => {
const dn = req.dn.toString();
if (!db[dn])
return next(new ldap.NoSuchObjectError(dn));
var scopeCheck;
let scopeCheck;
switch (req.scope) {
case 'base':
@ -172,24 +173,25 @@ with ldapjs.
return next();
case 'one':
scopeCheck = function(k) {
scopeCheck = (k) => {
if (req.dn.equals(k))
return true;
var parent = ldap.parseDN(k).parent();
const parent = ldap.parseDN(k).parent();
return (parent ? parent.equals(req.dn) : false);
};
break;
case 'sub':
scopeCheck = function(k) {
scopeCheck = (k) => {
return (req.dn.equals(k) || req.dn.parentOf(k));
};
break;
}
Object.keys(db).forEach(function(key) {
const keys = Object.keys(db);
for (const key of keys) {
if (!scopeCheck(key))
return;
@ -199,51 +201,53 @@ with ldapjs.
attributes: db[key]
});
}
});
}
res.end();
return next();
});
});
///--- Fire it up
///--- Fire it up
server.listen(1389, function() {
server.listen(1389, () => {
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;
```js
const fs = require('fs');
const ldap = require('ldapjs');
const { spawn } = require('child_process');
///--- Shared handlers
///--- Shared handlers
function authorize(req, res, next) {
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) {
function loadPasswdFile(req, res, next) {
fs.readFile('/etc/passwd', 'utf8', (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]))
const lines = data.split('\n');
for (const line of lines) {
if (!line || /^#/.test(line))
continue;
var record = lines[i].split(':');
const record = line.split(':');
if (!record || !record.length)
continue;
@ -263,39 +267,39 @@ with ldapjs.
return next();
});
}
}
var pre = [authorize, loadPasswdFile];
const pre = [authorize, loadPasswdFile];
///--- Mainline
///--- Mainline
var server = ldap.createServer();
const server = ldap.createServer();
server.bind('cn=root', function(req, res, next) {
server.bind('cn=root', (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) {
server.add('ou=users, o=myhost', pre, (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;
const entry = req.toObject().attributes;
if (entry.objectclass.indexOf('unixUser') === -1)
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
var opts = ['-m'];
const opts = ['-m'];
if (entry.description) {
opts.push('-c');
opts.push(entry.description[0]);
@ -317,20 +321,20 @@ with ldapjs.
opts.push(entry.uid[0]);
}
opts.push(entry.cn[0]);
var useradd = spawn('useradd', opts);
const useradd = spawn('useradd', opts);
var messages = [];
const messages = [];
useradd.stdout.on('data', function(data) {
useradd.stdout.on('data', (data) => {
messages.push(data.toString());
});
useradd.stderr.on('data', function(data) {
useradd.stderr.on('data', (data) => {
messages.push(data.toString());
});
useradd.on('exit', function(code) {
useradd.on('exit', (code) => {
if (code !== 0) {
var msg = '' + code;
let msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
@ -339,22 +343,22 @@ with ldapjs.
res.end();
return next();
});
});
});
server.modify('ou=users, o=myhost', pre, function(req, res, next) {
server.modify('ou=users, o=myhost', pre, (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;
const user = req.users[req.dn.rdns[0].cn].attributes;
let mod;
for (var i = 0; i < req.changes.length; i++) {
mod = req.changes[i].modification;
switch (req.changes[i].operation) {
for (const change of req.changes) {
mod = change.modification;
switch (change.operation) {
case 'replace':
if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
return next(new ldap.UnwillingToPerformError('only password updates ' +
@ -366,36 +370,36 @@ with ldapjs.
}
}
var passwd = spawn('chpasswd', ['-c', 'MD5']);
const passwd = spawn('chpasswd', ['-c', 'MD5']);
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
passwd.on('exit', function(code) {
passwd.on('exit', (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) {
server.del('ou=users, o=myhost', pre, (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]);
const userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]);
var messages = [];
userdel.stdout.on('data', function(data) {
const messages = [];
userdel.stdout.on('data', (data) => {
messages.push(data.toString());
});
userdel.stderr.on('data', function(data) {
userdel.stderr.on('data', (data) => {
messages.push(data.toString());
});
userdel.on('exit', function(code) {
userdel.on('exit', (code) => {
if (code !== 0) {
var msg = '' + code;
let msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
@ -404,25 +408,27 @@ with ldapjs.
res.end();
return next();
});
});
});
server.search('o=myhost', pre, function(req, res, next) {
Object.keys(req.users).forEach(function(k) {
server.search('o=myhost', pre, (req, res, next) => {
const keys = Object.keys(req.users);
for (const k of keys) {
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() {
// LDAP "standard" listens on 389, but whatever.
server.listen(1389, '127.0.0.1', () => {
console.log('/etc/passwd LDAP server up at: %s', server.url);
});
});
```
# Address Book
@ -430,84 +436,86 @@ This example is courtesy of [Diogo Resende](https://github.com/dresende) and
illustrates setting up an address book for typical mail clients such as
Thunderbird or Evolution over a MySQL database.
// MySQL test: (create on database 'abook' with username 'abook' and password 'abook')
//
// CREATE TABLE IF NOT EXISTS `users` (
// `id` int(5) unsigned NOT NULL AUTO_INCREMENT,
// `username` varchar(50) NOT NULL,
// `password` varchar(50) NOT NULL,
// PRIMARY KEY (`id`),
// KEY `username` (`username`)
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
// INSERT INTO `users` (`username`, `password`) VALUES
// ('demo', 'demo');
// CREATE TABLE IF NOT EXISTS `contacts` (
// `id` int(5) unsigned NOT NULL AUTO_INCREMENT,
// `user_id` int(5) unsigned NOT NULL,
// `name` varchar(100) NOT NULL,
// `email` varchar(255) NOT NULL,
// PRIMARY KEY (`id`),
// KEY `user_id` (`user_id`)
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
// INSERT INTO `contacts` (`user_id`, `name`, `email`) VALUES
// (1, 'John Doe', 'john.doe@example.com'),
// (1, 'Jane Doe', 'jane.doe@example.com');
//
```js
// MySQL test: (create on database 'abook' with username 'abook' and password 'abook')
//
// CREATE TABLE IF NOT EXISTS `users` (
// `id` int(5) unsigned NOT NULL AUTO_INCREMENT,
// `username` varchar(50) NOT NULL,
// `password` varchar(50) NOT NULL,
// PRIMARY KEY (`id`),
// KEY `username` (`username`)
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
// INSERT INTO `users` (`username`, `password`) VALUES
// ('demo', 'demo');
// CREATE TABLE IF NOT EXISTS `contacts` (
// `id` int(5) unsigned NOT NULL AUTO_INCREMENT,
// `user_id` int(5) unsigned NOT NULL,
// `name` varchar(100) NOT NULL,
// `email` varchar(255) NOT NULL,
// PRIMARY KEY (`id`),
// KEY `user_id` (`user_id`)
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
// INSERT INTO `contacts` (`user_id`, `name`, `email`) VALUES
// (1, 'John Doe', 'john.doe@example.com'),
// (1, 'Jane Doe', 'jane.doe@example.com');
//
var ldap = require('ldapjs'),
mysql = require("mysql"),
server = ldap.createServer(),
addrbooks = {}, userinfo = {},
ldap_port = 389,
basedn = "dc=example, dc=com",
company = "Example",
db = mysql.createClient({
const ldap = require('ldapjs');
const mysql = require("mysql");
const server = ldap.createServer();
const addrbooks = {};
const userinfo = {};
const ldap_port = 389;
const basedn = "dc=example, dc=com";
const company = "Example";
const db = mysql.createClient({
user: "abook",
password: "abook",
database: "abook"
});
});
db.query("SELECT c.*,u.username,u.password " +
db.query("SELECT c.*,u.username,u.password " +
"FROM contacts c JOIN users u ON c.user_id=u.id",
function(err, contacts) {
(err, contacts) => {
if (err) {
console.log("Error fetching contacts", err);
process.exit(1);
}
for (var i = 0; i < contacts.length; i++) {
if (!addrbooks.hasOwnProperty(contacts[i].username)) {
addrbooks[contacts[i].username] = [];
userinfo["cn=" + contacts[i].username + ", " + basedn] = {
abook: addrbooks[contacts[i].username],
pwd: contacts[i].password
for (const contact of contacts) {
if (!addrbooks.hasOwnProperty(contact.username)) {
addrbooks[contact.username] = [];
userinfo["cn=" + contact.username + ", " + basedn] = {
abook: addrbooks[contact.username],
pwd: contact.password
};
}
var p = contacts[i].name.indexOf(" ");
const p = contact.name.indexOf(" ");
if (p != -1)
contacts[i].firstname = contacts[i].name.substr(0, p);
contact.firstname = contact.name.substr(0, p);
p = contacts[i].name.lastIndexOf(" ");
p = contact.name.lastIndexOf(" ");
if (p != -1)
contacts[i].surname = contacts[i].name.substr(p + 1);
contact.surname = contact.name.substr(p + 1);
addrbooks[contacts[i].username].push({
dn: "cn=" + contacts[i].name + ", " + basedn,
addrbooks[contact.username].push({
dn: "cn=" + contact.name + ", " + basedn,
attributes: {
objectclass: [ "top" ],
cn: contacts[i].name,
mail: contacts[i].email,
givenname: contacts[i].firstname,
sn: contacts[i].surname,
cn: contact.name,
mail: contact.email,
givenname: contact.firstname,
sn: contact.surname,
ou: company
}
});
}
server.bind(basedn, function (req, res, next) {
var username = req.dn.toString(),
password = req.credentials;
server.bind(basedn, (req, res, next) => {
const username = req.dn.toString();
const password = req.credentials;
if (!userinfo.hasOwnProperty(username) ||
userinfo[username].pwd != password) {
@ -518,24 +526,27 @@ Thunderbird or Evolution over a MySQL database.
return next();
});
server.search(basedn, function(req, res, next) {
var binddn = req.connection.ldap.bindDN.toString();
server.search(basedn, (req, res, next) => {
const binddn = req.connection.ldap.bindDN.toString();
if (userinfo.hasOwnProperty(binddn)) {
for (var i = 0; i < userinfo[binddn].abook.length; i++) {
if (req.filter.matches(userinfo[binddn].abook[i].attributes))
res.send(userinfo[binddn].abook[i]);
for (const abook of userinfo[binddn].abook) {
if (req.filter.matches(abook.attributes))
res.send(abook);
}
}
res.end();
});
server.listen(ldap_port, function() {
server.listen(ldap_port, () => {
console.log("Addressbook started at %s", server.url);
});
});
});
```
To test out this example, try:
$ ldapsearch -H ldap://localhost:389 -x -D cn=demo,dc=example,dc=com \
```shell
$ ldapsearch -H ldap://localhost:389 -x -D cn=demo,dc=example,dc=com \
-w demo -b "dc=example,dc=com" objectclass=*
```

View File

@ -34,13 +34,17 @@ Parses an [RFC2254](http://www.ietf.org/rfc/rfc2254.txt) filter string into an
ldapjs object(s). If the filter is "complex", it will be a "tree" of objects.
For example:
var parseFilter = require('ldapjs').parseFilter;
```js
const parseFilter = require('ldapjs').parseFilter;
var f = parseFilter('(objectclass=*)');
const f = parseFilter('(objectclass=*)');
```
Is a "simple" filter, and would just return a `PresenceFilter` object. However,
var f = parseFilter('(&(employeeType=manager)(l=Seattle))');
```js
const f = parseFilter('(&(employeeType=manager)(l=Seattle))');
```
Would return an `AndFilter`, which would have a `filters` array of two
`EqualityFilter` objects.
@ -59,13 +63,15 @@ The string syntax for an equality filter is `(attr=value)`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and a value matching `value`.
var f = new EqualityFilter({
```js
const f = new EqualityFilter({
attribute: 'cn',
value: 'foo'
});
});
f.matches({cn: 'foo'}); => true
f.matches({cn: 'bar'}); => false
f.matches({cn: 'foo'}); => true
f.matches({cn: 'bar'}); => false
```
Equality matching uses "strict" type JavaScript comparison, and by default
everything in ldapjs (and LDAP) is a UTF-8 string. If you want comparison
@ -83,12 +89,14 @@ The string syntax for a presence filter is `(attr=*)`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute`.
var f = new PresenceFilter({
```js
const f = new PresenceFilter({
attribute: 'cn'
});
});
f.matches({cn: 'foo'}); => true
f.matches({sn: 'foo'}); => false
f.matches({cn: 'foo'}); => true
f.matches({sn: 'foo'}); => false
```
# SubstringFilter
@ -102,24 +110,28 @@ optional. The `name` property will be `substring`.
The string syntax for a presence filter is `(attr=foo*bar*cat*dog)`, which would
map to:
{
```js
{
initial: 'foo',
any: ['bar', 'cat'],
final: 'dog'
}
}
```
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and the "regex" matches the value
var f = new SubstringFilter({
```js
const f = new SubstringFilter({
attribute: 'cn',
initial: 'foo',
any: ['bar'],
final: 'baz'
});
});
f.matches({cn: 'foobigbardogbaz'}); => true
f.matches({sn: 'fobigbardogbaz'}); => false
f.matches({cn: 'foobigbardogbaz'}); => true
f.matches({sn: 'fobigbardogbaz'}); => false
```
# GreaterThanEqualsFilter
@ -135,18 +147,22 @@ property and the `name` property will be `ge`.
The string syntax for a ge filter is:
(cn>=foo)
```
(cn>=foo)
```
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and the value is `>=` this filter's `value`.
var f = new GreaterThanEqualsFilter({
```js
const f = new GreaterThanEqualsFilter({
attribute: 'cn',
value: 'foo',
});
});
f.matches({cn: 'foobar'}); => true
f.matches({cn: 'abc'}); => false
f.matches({cn: 'foobar'}); => true
f.matches({cn: 'abc'}); => false
```
# LessThanEqualsFilter
@ -159,7 +175,9 @@ Note that the ldapjs schema middleware will do this.
The string syntax for a le filter is:
(cn<=foo)
```
(cn<=foo)
```
The LessThanEqualsFilter will have an `attribute` property, a `value`
property and the `name` property will be `le`.
@ -167,13 +185,15 @@ property and the `name` property will be `le`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and the value is `<=` this filter's `value`.
var f = new LessThanEqualsFilter({
```js
const f = new LessThanEqualsFilter({
attribute: 'cn',
value: 'foo',
});
});
f.matches({cn: 'abc'}); => true
f.matches({cn: 'foobar'}); => false
f.matches({cn: 'abc'}); => true
f.matches({cn: 'foobar'}); => false
```
# AndFilter
@ -184,12 +204,15 @@ object will have a `filters` property which is an array of `Filter` objects. The
The string syntax for an and filter is (assuming below we're and'ing two
equality filters):
(&(cn=foo)(sn=bar))
```
(&(cn=foo)(sn=bar))
```
The `matches()` method will return true IFF the passed in object matches all
the filters in the `filters` array.
var f = new AndFilter({
```js
const f = new AndFilter({
filters: [
new EqualityFilter({
attribute: 'cn',
@ -200,10 +223,11 @@ the filters in the `filters` array.
value: 'bar'
})
]
});
});
f.matches({cn: 'foo', sn: 'bar'}); => true
f.matches({cn: 'foo', sn: 'baz'}); => false
f.matches({cn: 'foo', sn: 'bar'}); => true
f.matches({cn: 'foo', sn: 'baz'}); => false
```
# OrFilter
@ -214,12 +238,15 @@ object will have a `filters` property which is an array of `Filter` objects. The
The string syntax for an or filter is (assuming below we're or'ing two
equality filters):
(|(cn=foo)(sn=bar))
```
(|(cn=foo)(sn=bar))
```
The `matches()` method will return true IFF the passed in object matches *any*
of the filters in the `filters` array.
var f = new OrFilter({
```js
const f = new OrFilter({
filters: [
new EqualityFilter({
attribute: 'cn',
@ -230,10 +257,11 @@ of the filters in the `filters` array.
value: 'bar'
})
]
});
});
f.matches({cn: 'foo', sn: 'baz'}); => true
f.matches({cn: 'bar', sn: 'baz'}); => false
f.matches({cn: 'foo', sn: 'baz'}); => true
f.matches({cn: 'bar', sn: 'baz'}); => false
```
# NotFilter
@ -244,20 +272,24 @@ The `name` property will be `not`.
The string syntax for a not filter is (assuming below we're not'ing an
equality filter):
(!(cn=foo))
```
(!(cn=foo))
```
The `matches()` method will return true IFF the passed in object does not match
the filter in the `filter` property.
var f = new NotFilter({
```js
const f = new NotFilter({
filter: new EqualityFilter({
attribute: 'cn',
value: 'foo'
})
});
});
f.matches({cn: 'bar'}); => true
f.matches({cn: 'foo'}); => false
f.matches({cn: 'bar'}); => true
f.matches({cn: 'foo'}); => false
```
# ApproximateFilter
@ -274,10 +306,12 @@ The string syntax for an equality filter is `(attr~=value)`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and a value exactly matching `value`.
var f = new ApproximateFilter({
```js
const f = new ApproximateFilter({
attribute: 'cn',
value: 'foo'
});
});
f.matches({cn: 'foo'}); => true
f.matches({cn: 'bar'}); => false
f.matches({cn: 'foo'}); => true
f.matches({cn: 'bar'}); => false
```

View File

@ -31,23 +31,26 @@ Access Protocol". A directory service basically breaks down as follows:
It might be helpful to visualize:
```
o=example
/ \
ou=users ou=groups
/ | | \
cn=john cn=jane cn=dudes cn=dudettes
/
keyid=foo
keyid=foo
```
Let's say we wanted to look at the record cn=john:
dn: cn=john, ou=users, o=example
cn: john
sn: smith
email: john@example.com
email: john.smith@example.com
objectClass: person
```shell
dn: cn=john, ou=users, o=example
cn: john
sn: smith
email: john@example.com
email: john.smith@example.com
objectClass: person
```
A few things to note:
@ -111,7 +114,9 @@ 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
```shell
$ npm install ldapjs
```
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
@ -121,18 +126,22 @@ package manager here.
To get started, open some file, and let's get the library loaded and a server
created:
var ldap = require('ldapjs');
```js
const ldap = require('ldapjs');
var server = ldap.createServer();
const server = ldap.createServer();
server.listen(1389, function() {
server.listen(1389, () => {
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=*
```shell
$ 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](examples.html) page.
@ -153,13 +162,15 @@ 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:
server.bind('cn=root', function(req, res, next) {
```js
server.bind('cn=root', (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
@ -168,7 +179,9 @@ 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')
```js
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
```
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
@ -192,18 +205,22 @@ 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=*
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w foo -b "o=myhost" objectclass=*
ldap_bind: Invalid credentials (49)
ldap_bind: Invalid credentials (49)
matched DN: cn=root
additional info: Invalid Credentials
```
And again with the correct one:
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
```shell
$ 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
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. This time, we got another `No such object`
@ -217,12 +234,14 @@ 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) {
```js
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals('cn=root'))
return next(new ldap.InsufficientAccessRightsError());
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`
@ -233,7 +252,9 @@ oriented, so we check that the connection remote user was indeed our `cn=root`
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
```shell
jsmith:x:1001:1000:Joe Smith,Room 1007,(234)555-8910,(234)555-0044,email:/home/jsmith:/bin/sh
```
The sample record above maps to:
@ -248,24 +269,25 @@ The sample record above maps to:
|/bin/sh |Shell |
Let's write 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
record (note, you'll need to add `const fs = require('fs');` at the top of the
source file).
First, make a handler that just loads the "user database" in a "pre" handler:
function loadPasswdFile(req, res, next) {
fs.readFile('/etc/passwd', 'utf8', function(err, data) {
```js
function loadPasswdFile(req, res, next) {
fs.readFile('/etc/passwd', 'utf8', (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]))
const lines = data.split('\n');
for (const line of lines) {
if (!line || /^#/.test(line))
continue;
var record = lines[i].split(':');
const record = line.split(':');
if (!record || !record.length)
continue;
@ -285,40 +307,48 @@ First, make a handler that just loads the "user database" in a "pre" handler:
return next();
});
}
}
```
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];
```js
const pre = [authorize, loadPasswdFile];
server.search('o=myhost', pre, function(req, res, next) {
Object.keys(req.users).forEach(function(k) {
server.search('o=myhost', pre, (req, res, next) => {
const keys = Object.keys(req.users);
for (const k of keys) {
if (req.filter.matches(req.users[k].attributes))
res.send(req.users[k]);
});
}
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
```shell
$ 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=*
...
```shell
$ 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...
@ -327,7 +357,9 @@ What all did we do here? A lot. Let's break this down...
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
```shell
$ 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.
@ -360,7 +392,8 @@ 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 = {
```js
const entry = {
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
attributes: {
cn: record[0],
@ -371,7 +404,8 @@ of attributes. So that's why we did this:
shell: record[6] || '',
objectclass: 'unixUser'
}
};
};
```
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
@ -386,35 +420,38 @@ 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
```shell
$ 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
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):
`const { spawn } = require('child_process');` at the top of your file):
server.add('ou=users, o=myhost', pre, function(req, res, next) {
```js
server.add('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn)
return next(new ldap.ConstraintViolationError('cn required'));
if (req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
var entry = req.toObject().attributes;
const entry = req.toObject().attributes;
if (entry.objectclass.indexOf('unixUser') === -1)
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
var opts = ['-m'];
const opts = ['-m'];
if (entry.description) {
opts.push('-c');
opts.push(entry.description[0]);
@ -436,20 +473,20 @@ the following code in as another handler (you'll need a
opts.push(entry.uid[0]);
}
opts.push(entry.cn[0]);
var useradd = spawn('useradd', opts);
const useradd = spawn('useradd', opts);
var messages = [];
const messages = [];
useradd.stdout.on('data', function(data) {
useradd.stdout.on('data', (data) => {
messages.push(data.toString());
});
useradd.stderr.on('data', function(data) {
useradd.stderr.on('data', (data) => {
messages.push(data.toString());
});
useradd.on('exit', function(code) {
useradd.on('exit', (code) => {
if (code !== 0) {
var msg = '' + code;
let msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
@ -458,48 +495,58 @@ the following code in as another handler (you'll need a
res.end();
return next();
});
});
});
```
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
```shell
dn: cn=ldapjs, ou=users, o=myhost
objectClass: unixUser
cn: ldapjs
shell: /bin/bash
description: Created via ldapadd
```
Now go ahead and invoke with:
$ ldapadd -H ldap://localhost:1389 -x -D cn=root -w secret -f ./user.ldif
adding new entry "cn=ldapjs, ou=users, o=myhost"
```shell
$ 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
```shell
$ 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) {
```js
server.add('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn)
return next(new ldap.ConstraintViolationError('cn required'));
if (req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
var entry = req.toObject().attributes;
const entry = req.toObject().attributes;
if (entry.objectclass.indexOf('unixUser') === -1)
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
});
```
A few new things:
@ -534,17 +581,18 @@ 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) {
```js
server.modify('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value])
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].attrs.cn.value].attributes;
var mod;
const user = req.users[req.dn.rdns[0].attrs.cn.value].attributes;
let mod;
for (var i = 0; i < req.changes.length; i++) {
for (const i = 0; i < req.changes.length; i++) {
mod = req.changes[i].modification;
switch (req.changes[i].operation) {
case 'replace':
@ -558,18 +606,18 @@ Go ahead and add the following code into your source file:
}
}
var passwd = spawn('chpasswd', ['-c', 'MD5']);
const passwd = spawn('chpasswd', ['-c', 'MD5']);
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
passwd.on('exit', function(code) {
passwd.on('exit', (code) => {
if (code !== 0)
return next(new ldap.OperationsError(code));
res.end();
return next();
});
});
});
```
Basically, we made sure the remote client was targeting an entry that exists,
ensuring that they were asking to "replace" the `userPassword` attribute (which
@ -578,15 +626,19 @@ is the 'standard' LDAP attribute for passwords; if you think it's easier to use
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
-
```shell
dn: cn=ldapjs, ou=users, o=myhost
changetype: modify
replace: userPassword
userPassword: secret
-
```
And then run the OpenLDAP CLI:
$ ldapmodify -H ldap://localhost:1389 -x -D cn=root -w secret -f ./passwd.ldif
```shell
$ 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. Let's get
the last "mainline" piece of work out of the way, and delete the user.
@ -596,23 +648,24 @@ the last "mainline" piece of work out of the way, and delete the user.
Delete is pretty straightforward. The client gives you a dn to delete, and you
delete it :). Add the following code into your server:
server.del('ou=users, o=myhost', pre, function(req, res, next) {
```js
server.del('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.NoSuchObjectError(req.dn.toString()));
var userdel = spawn('userdel', ['-f', req.dn.rdns[0].attrs.cn.value]);
const userdel = spawn('userdel', ['-f', req.dn.rdns[0].attrs.cn.value]);
var messages = [];
userdel.stdout.on('data', function(data) {
const messages = [];
userdel.stdout.on('data', (data) => {
messages.push(data.toString());
});
userdel.stderr.on('data', function(data) {
userdel.stderr.on('data', (data) => {
messages.push(data.toString());
});
userdel.on('exit', function(code) {
userdel.on('exit', (code) => {
if (code !== 0) {
var msg = '' + code;
let msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
@ -621,12 +674,14 @@ delete it :). Add the following code into your server:
res.end();
return next();
});
});
});
```
And then run the following command:
$ ldapdelete -H ldap://localhost:1389 -x -D cn=root -w secret "cn=ldapjs, ou=users, o=myhost"
```shell
$ ldapdelete -H ldap://localhost:1389 -x -D cn=root -w secret "cn=ldapjs, ou=users, o=myhost"
```
# Where to go from here

View File

@ -17,12 +17,13 @@ with HTTP services in node and [restify](http://restify.com).
</div>
var ldap = require('ldapjs');
```js
const ldap = require('ldapjs');
var server = ldap.createServer();
const server = ldap.createServer();
server.search('o=example', function(req, res, next) {
var obj = {
server.search('o=example', (req, res, next) => {
const obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
@ -34,15 +35,18 @@ with HTTP services in node and [restify](http://restify.com).
res.send(obj);
res.end();
});
});
server.listen(1389, function() {
server.listen(1389, () => {
console.log('LDAP server listening at %s', server.url);
});
});
```
Try hitting that with:
$ ldapsearch -H ldap://localhost:1389 -x -b o=example objectclass=*
```shell
$ ldapsearch -H ldap://localhost:1389 -x -b o=example objectclass=*
```
# Features
@ -55,7 +59,9 @@ that you can build LDAP over anything you want, not just traditional databases.
# Getting started
$ npm install ldapjs
```shell
$ npm install ldapjs
```
If you're new to LDAP, check out the [guide](guide.html). Otherwise, the
API documentation is:

View File

@ -15,7 +15,9 @@ with LDAP. If you're not, read the [guide](guide.html) first.
The code to create a new server looks like:
var server = ldap.createServer();
```js
const server = ldap.createServer();
```
The full list of options is:
@ -68,10 +70,11 @@ available.
Example:
```js
server.listen(389, '127.0.0.1', function() {
console.log('LDAP server listening at: ' + server.url);
});
```
### Port and Host
`listen(port, [host], [callback])`
@ -115,14 +118,16 @@ paradigm of programming. Essentially every method is of the form
handlers together by calling `next()` and ordering your functions in the
definition of the route. For example:
function authorize(req, res, next) {
```js
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals('cn=root'))
return next(new ldap.InsufficientAccessRightsError());
return next();
}
}
server.search('o=example', authorize, function(req, res, next) { ... });
server.search('o=example', authorize, function(req, res, next) { ... });
```
Note that ldapjs is also slightly different, since it's often going to be backed
to a DB-like entity, in that it also has an API where you can pass in a
@ -134,23 +139,25 @@ complete implementation of the LDAP protocol over
[Riak](https://github.com/basho/riak). Getting an LDAP server up with riak
looks like:
var ldap = require('ldapjs');
var ldapRiak = require('ldapjs-riak');
```js
const ldap = require('ldapjs');
const ldapRiak = require('ldapjs-riak');
var server = ldap.createServer();
var backend = ldapRiak.createBackend({
const server = ldap.createServer();
const backend = ldapRiak.createBackend({
"host": "localhost",
"port": 8098,
"bucket": "example",
"indexes": ["l", "cn"],
"uniqueIndexes": ["uid"],
"numConnections": 5
});
});
server.add("o=example",
server.add("o=example",
backend,
backend.add());
...
...
```
The first parameter to an ldapjs route is always the point in the
tree to mount the handler chain at. The second argument is _optionally_ a
@ -163,10 +170,12 @@ operation requires specific methods/fields on the request/response
objects. However, there is a `.use()` method availabe, similar to
that on express/connect, allowing you to chain up "middleware":
server.use(function(req, res, next) {
```js
server.use(function(req, res, next) {
console.log('hello world');
return next();
});
});
```
## Common Request Elements
@ -210,11 +219,13 @@ the paradigm is something defined like CONSTRAINT\_VIOLATION in the RFC would be
`ConstraintViolationError` in ldapjs. Upon calling `next(new LDAPError())`,
ldapjs will _stop_ calling your handler chain. For example:
server.search('o=example',
function(req, res, next) { return next(); },
function(req, res, next) { return next(new ldap.OperationsError()); },
function(req, res, next) { res.end(); }
);
```js
server.search('o=example',
(req, res, next) => { return next(); },
(req, res, next) => { return next(new ldap.OperationsError()); },
(req, res, next) => { res.end(); }
);
```
In the code snipped above, the third handler would never get invoked.
@ -222,11 +233,13 @@ In the code snipped above, the third handler would never get invoked.
Adds a mount in the tree to perform LDAP binds with. Example:
server.bind('ou=people, o=example', function(req, res, next) {
```js
server.bind('ou=people, o=example', (req, res, next) => {
console.log('bind DN: ' + req.dn.toString());
console.log('bind PW: ' + req.credentials);
res.end();
});
});
```
## BindRequest
@ -259,11 +272,13 @@ No extra methods above an `LDAPResult` API call.
Adds a mount in the tree to perform LDAP adds with.
server.add('ou=people, o=example', function(req, res, next) {
```js
server.add('ou=people, o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('Entry attributes: ' + req.toObject().attributes);
res.end();
});
});
```
## AddRequest
@ -287,14 +302,16 @@ a standard JavaScript object.
This operation will return a plain JavaScript object from the request that looks
like:
{
```js
{
dn: 'cn=foo, o=example', // string, not DN object
attributes: {
cn: ['foo'],
sn: ['bar'],
objectclass: ['person', 'top']
}
}
}
```
## AddResponse
@ -304,12 +321,14 @@ No extra methods above an `LDAPResult` API call.
Adds a handler for the LDAP search operation.
server.search('o=example', function(req, res, next) {
```js
server.search('o=example', (req, res, next) => {
console.log('base object: ' + req.dn.toString());
console.log('scope: ' + req.scope);
console.log('filter: ' + req.filter.toString());
res.end();
});
});
```
## SearchRequest
@ -367,8 +386,9 @@ explicitly pass in a `SearchEntry` object, and can instead just send a plain
JavaScript object that matches the format used from `AddRequest.toObject()`.
server.search('o=example', function(req, res, next) {
var obj = {
```js
server.search('o=example', (req, res, next) => {
const obj = {
dn: 'o=example',
attributes: {
objectclass: ['top', 'organization'],
@ -380,21 +400,24 @@ JavaScript object that matches the format used from `AddRequest.toObject()`.
res.send(obj)
res.end();
});
});
```
# modify
Allows you to handle an LDAP modify operation.
server.modify('o=example', function(req, res, next) {
```js
server.modify('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('changes:');
req.changes.forEach(function(c) {
for (const c of req.changes) {
console.log(' operation: ' + c.operation);
console.log(' modification: ' + c.modification.toString());
});
}
res.end();
});
});
```
## ModifyRequest
@ -431,10 +454,12 @@ No extra methods above an `LDAPResult` API call.
Allows you to handle an LDAP delete operation.
server.del('o=example', function(req, res, next) {
```js
server.del('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
res.end();
});
});
```
## DeleteRequest
@ -451,12 +476,14 @@ No extra methods above an `LDAPResult` API call.
Allows you to handle an LDAP compare operation.
server.compare('o=example', function(req, res, next) {
```js
server.compare('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('attribute name: ' + req.attribute);
console.log('attribute value: ' + req.value);
res.end(req.value === 'foo');
});
});
```
## CompareRequest
@ -483,7 +510,8 @@ that, there are no extra methods above an `LDAPResult` API call.
Allows you to handle an LDAP modifyDN operation.
server.modifyDN('o=example', function(req, res, next) {
```js
server.modifyDN('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('new RDN: ' + req.newRdn.toString());
console.log('deleteOldRDN: ' + req.deleteOldRdn);
@ -491,7 +519,8 @@ Allows you to handle an LDAP modifyDN operation.
(req.newSuperior ? req.newSuperior.toString() : ''));
res.end();
});
});
```
## ModifyDNRequest
@ -525,14 +554,16 @@ OID, but ldapjs makes no such restrictions; it just needs to be a string.
Unlike the other operations, extended operations don't map to any location in
the tree, so routing here will be exact match, as opposed to subtree.
// LDAP whoami
server.exop('1.3.6.1.4.1.4203.1.11.3', function(req, res, next) {
```js
// LDAP whoami
server.exop('1.3.6.1.4.1.4203.1.11.3', (req, res, next) => {
console.log('name: ' + req.name);
console.log('value: ' + req.value);
res.value = 'u:xxyyz@EXAMPLE.NET';
res.end();
return next();
});
});
```
## ExtendedRequest
@ -563,9 +594,11 @@ and cleans up any internals (in ldapjs core). You can override this handler
if you need to clean up any items in your backend, or perform any other cleanup
tasks you need to.
server.unbind(function(req, res, next) {
```js
server.unbind((req, res, next) => {
res.end();
});
});
```
Note that the LDAP unbind operation actually doesn't send any response (by
definition in the RFC), so the UnbindResponse is really just a stub that

View File

@ -27,13 +27,14 @@
"verror": "^1.8.1"
},
"devDependencies": {
"eslint": "^7.14.0",
"eslint": "^7.20.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-promise": "^4.3.1",
"front-matter": "^4.0.2",
"get-port": "^5.1.1",
"highlight.js": "^10.6.0",
"husky": "^4.2.5",
"marked": "^2.0.0",
"tap": "14.11.0"

View File

@ -2,6 +2,17 @@ const fs = require('fs/promises')
const path = require('path')
const marked = require('marked')
const fm = require('front-matter')
const { highlight } = require('highlight.js')
marked.use({
highlight: (code, lang) => {
if (lang) {
return highlight(lang, code).value
}
return code
}
})
function tocHTML (toc) {
let html = '<ul>\n'
@ -92,10 +103,14 @@ async function createDocs () {
const dest = path.join(dist, 'media')
const src = path.join(branding, 'media')
const highlightjsStyles = path.resolve(__dirname, '..', 'node_modules', 'highlight.js', 'styles')
await fs.mkdir(dest)
await fs.mkdir(path.join(dest, 'css'))
await fs.mkdir(path.join(dest, 'js'))
await fs.mkdir(path.join(dest, 'img'))
await fs.copyFile(path.join(src, 'css', 'style.css'), path.join(dest, 'css', 'style.css'))
await fs.copyFile(path.join(highlightjsStyles, 'default.css'), path.join(dest, 'css', 'highlight.css'))
await fs.copyFile(path.join(src, 'js', 'script.js'), path.join(dest, 'js', 'script.js'))
await fs.copyFile(path.join(src, 'img', 'logo.svg'), path.join(dest, 'img', 'logo.svg'))
await fs.copyFile(path.join(branding, 'CNAME'), path.join(dist, 'CNAME'))
}