Merge branch 'master' into bugfix_typos
This commit is contained in:
commit
be2b8757d6
|
@ -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> </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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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> </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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
152
docs/client.md
152
docs/client.md
|
@ -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);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
|
34
docs/dn.md
34
docs/dn.md
|
@ -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());
|
||||
});
|
||||
});
|
||||
```
|
||||
|
|
|
@ -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.
|
||||
|
|
333
docs/examples.md
333
docs/examples.md
|
@ -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=*
|
||||
```
|
||||
|
|
126
docs/filters.md
126
docs/filters.md
|
@ -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
|
||||
```
|
||||
|
|
279
docs/guide.md
279
docs/guide.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
123
docs/server.md
123
docs/server.md
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue