From e4b72cde9b7dec3c058333e265f4781833bc171d Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Wed, 24 Feb 2021 16:03:35 -0600 Subject: [PATCH 1/4] docs: update code --- docs/client.md | 280 +++++++------- docs/dn.md | 66 ++-- docs/errors.md | 26 +- docs/examples.md | 955 ++++++++++++++++++++++++----------------------- docs/filters.md | 206 +++++----- docs/guide.md | 541 +++++++++++++++------------ docs/index.md | 44 ++- docs/server.md | 244 ++++++------ 8 files changed, 1272 insertions(+), 1090 deletions(-) diff --git a/docs/client.md b/docs/client.md index 62a55cb..c267dab 100644 --- a/docs/client.md +++ b/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) => { - // handle connection error - }) +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) { - assert.ifError(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 = { - cn: 'foo', - sn: 'bar', - email: ['foo@bar.com', 'foo1@bar.com'], - objectclass: 'fooPerson' - }; - client.add('cn=foo, o=example', entry, function(err) { - assert.ifError(err); - }); +```js +const entry = { + cn: 'foo', + sn: 'bar', + email: ['foo@bar.com', 'foo1@bar.com'], + objectclass: 'fooPerson' +}; +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) { - assert.ifError(err); +```js +client.compare('cn=foo, o=example', 'sn', 'bar', (err, matched) => { + assert.ifError(err); - console.log('matched: ' + matched); - }); + 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) { - assert.ifError(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) { - assert.ifError(err); +```js +client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => { + assert.ifError(err); - console.log('whois: ' + value); - }); + 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({ - operation: 'add', - modification: { - pets: ['cat', 'dog'] - } - }); +```js +const change = new ldap.Change({ + operation: 'add', + modification: { + pets: ['cat', 'dog'] + } +}); - client.modify('cn=foo, o=example', change, function(err) { - assert.ifError(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) { - assert.ifError(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 = { - filter: '(&(l=Seattle)(email=*@foo.com))', - scope: 'sub', - attributes: ['dn', 'sn', 'cn'] - }; +```js +const opts = { + filter: '(&(l=Seattle)(email=*@foo.com))', + scope: 'sub', + attributes: ['dn', 'sn', 'cn'] +}; - client.search('o=example', opts, function(err, res) { - assert.ifError(err); +client.search('o=example', opts, (err, res) => { + assert.ifError(err); - res.on('searchEntry', function(entry) { - console.log('entry: ' + JSON.stringify(entry.object)); - }); - res.on('searchReference', function(referral) { - console.log('referral: ' + referral.uris.join()); - }); - res.on('error', function(err) { - console.error('error: ' + err.message); - }); - res.on('end', function(result) { - console.log('status: ' + result.status); - }); - }); + res.on('searchEntry', (entry) => { + console.log('entry: ' + JSON.stringify(entry.object)); + }); + res.on('searchReference', (referral) => { + console.log('referral: ' + referral.uris.join()); + }); + res.on('error', (err) => { + console.error('error: ' + err.message); + }); + 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 = { - filter: '(objectclass=commonobject)', - scope: 'sub', - paged: true, - sizeLimit: 200 - }; - client.search('o=largedir', opts, function(err, res) { - assert.ifError(err); - res.on('searchEntry', function(entry) { - // do per-entry processing - }); - res.on('page', function(result) { - console.log('page end'); - }); - res.on('error', function(resErr) { - assert.ifError(resErr); - }); - res.on('end', function(result) { - console.log('done '); - }); - }); +```js +const opts = { + filter: '(objectclass=commonobject)', + scope: 'sub', + paged: true, + sizeLimit: 200 +}; +client.search('o=largedir', opts, (err, res) => { + assert.ifError(err); + res.on('searchEntry', (entry) => { + // do per-entry processing + }); + res.on('page', (result) => { + console.log('page end'); + }); + res.on('error', (resErr) => { + assert.ifError(resErr); + }); + 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 = { - filter: '(objectclass=commonobject)', - scope: 'sub', - paged: { - pageSize: 250, - pagePause: true - }, - }; - client.search('o=largerdir', opts, function(err, res) { - assert.ifError(err); - res.on('searchEntry', function(entry) { - // Submit incoming objects to queue - queue.push(entry); - }); - res.on('page', function(result, cb) { - // Allow the queue to flush before fetching next page - queue.cbWhenFlushed(cb); - }); - res.on('error', function(resErr) { - assert.ifError(resErr); - }); - res.on('end', function(result) { - console.log('done'); - }); - }); +```js +const queue = new MyWorkQueue(someSlowWorkFunction); +const opts = { + filter: '(objectclass=commonobject)', + scope: 'sub', + paged: { + pageSize: 250, + pagePause: true + }, +}; +client.search('o=largerdir', opts, (err, res) => { + assert.ifError(err); + res.on('searchEntry', (entry) => { + // Submit incoming objects to queue + queue.push(entry); + }); + res.on('page', (result, cb) => { + // Allow the queue to flush before fetching next page + queue.cbWhenFlushed(cb); + }); + res.on('error', (resErr) => { + assert.ifError(resErr); + }); + 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 = { - ca: [fs.readFileSync('mycacert.pem')] - }; +```js +const opts = { + ca: [fs.readFileSync('mycacert.pem')] +}; - client.starttls(opts, function(err, res) { - assert.ifError(err); +client.starttls(opts, (err, res) => { + assert.ifError(err); - // Client communication now TLS protected - }); + // Client communication now TLS protected +}); +``` # unbind @@ -440,6 +468,8 @@ not have a response. Example: - client.unbind(function(err) { - assert.ifError(err); - }); +```js +client.unbind((err) => { + assert.ifError(err); +}); +``` diff --git a/docs/dn.md b/docs/dn.md index 66f8dac..c95d511 100644 --- a/docs/dn.md +++ b/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) { - if (req.dn.childOf('ou=people, o=example')) { - ... - } else { - ... - } - }); +```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'); - if (dn.parentOf(req.dn)) { - ... - } else { - ... - } - }); +```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) { - if (req.dn.equals('cn=foo, ou=people, o=example')) { - ... - } else { - ... - } - }); +```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) { - console.log(req.dn.toString()); - }); +```js +server.add('o=example', (req, res, next) => { + console.log(req.dn.toString()); +}); +``` diff --git a/docs/errors.md b/docs/errors.md index 48e45bd..b44c5ec 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -17,21 +17,23 @@ 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(); - if (parent) { - if (!db[parent.toString()]) - return next(new ldap.NoSuchObjectError(parent.toString())); - } - if (db[req.dn.toString()]) - return next(new ldap.EntryAlreadyExistsError(req.dn.toString())); +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())); + } + if (db[req.dn.toString()]) + 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. diff --git a/docs/examples.md b/docs/examples.md index 0c8cdad..5c195dc 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -13,416 +13,422 @@ with ldapjs. # In-memory server - var ldap = require('ldapjs'); +```js +const ldap = require('ldapjs'); - ///--- Shared handlers +///--- Shared handlers - function authorize(req, res, next) { - /* Any user may search after bind, only cn=root has full power */ - var isSearch = (req instanceof ldap.SearchRequest); - if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch) - return next(new ldap.InsufficientAccessRightsError()); +function authorize(req, res, next) { + /* Any user may search after bind, only cn=root has full power */ + const isSearch = (req instanceof ldap.SearchRequest); + if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch) + return next(new ldap.InsufficientAccessRightsError()); - return next(); + return next(); +} + + +///--- Globals + +const SUFFIX = 'o=joyent'; +const db = {}; +const server = ldap.createServer(); + + + +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, (req, res, next) => { + const dn = req.dn.toString(); + + if (db[dn]) + return next(new ldap.EntryAlreadyExistsError(dn)); + + db[dn] = req.toObject().attributes; + res.end(); + return next(); +}); + +server.bind(SUFFIX, (req, res, next) => { + const dn = req.dn.toString(); + if (!db[dn]) + return next(new ldap.NoSuchObjectError(dn)); + + if (!db[dn].userpassword) + return next(new ldap.NoSuchAttributeError('userPassword')); + + if (db[dn].userpassword.indexOf(req.credentials) === -1) + return next(new ldap.InvalidCredentialsError()); + + res.end(); + return next(); +}); + +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)); + + const matches = false; + const vals = db[dn][req.attribute]; + for (const value of vals) { + if (value === req.value) { + matches = true; + break; + } + } + + res.end(matches); + return next(); +}); + +server.del(SUFFIX, authorize, (req, res, next) => { + const dn = req.dn.toString(); + if (!db[dn]) + return next(new ldap.NoSuchObjectError(dn)); + + delete db[dn]; + + res.end(); + return next(); +}); + +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)); + + const entry = db[dn]; + + 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)); + + if (!mod.vals || !mod.vals.length) { + delete entry[mod.type]; + } else { + entry[mod.type] = mod.vals; + } + + break; + + case 'add': + if (!entry[mod.type]) { + entry[mod.type] = mod.vals; + } else { + for (const v of mod.vals) { + if (entry[mod.type].indexOf(v) === -1) + entry[mod.type].push(v); + } + } + + break; + + case 'delete': + if (!entry[mod.type]) + return next(new ldap.NoSuchAttributeError(mod.type)); + + delete entry[mod.type]; + + break; + } + } + + res.end(); + return next(); +}); + +server.search(SUFFIX, authorize, (req, res, next) => { + const dn = req.dn.toString(); + if (!db[dn]) + return next(new ldap.NoSuchObjectError(dn)); + + let scopeCheck; + + switch (req.scope) { + case 'base': + if (req.filter.matches(db[dn])) { + res.send({ + dn: dn, + attributes: db[dn] + }); } + res.end(); + return next(); - ///--- Globals + case 'one': + scopeCheck = (k) => { + if (req.dn.equals(k)) + return true; - var SUFFIX = 'o=joyent'; - var db = {}; - var server = ldap.createServer(); + const parent = ldap.parseDN(k).parent(); + return (parent ? parent.equals(req.dn) : false); + }; + break; + case 'sub': + scopeCheck = (k) => { + return (req.dn.equals(k) || req.dn.parentOf(k)); + }; + break; + } - server.bind('cn=root', function(req, res, next) { - if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') - return next(new ldap.InvalidCredentialsError()); + const keys = Object.keys(db); + for (const key of keys) { + if (!scopeCheck(key)) + return; - res.end(); - return next(); - }); - - server.add(SUFFIX, authorize, function(req, res, next) { - var dn = req.dn.toString(); - - if (db[dn]) - return next(new ldap.EntryAlreadyExistsError(dn)); - - db[dn] = req.toObject().attributes; - res.end(); - return next(); - }); - - server.bind(SUFFIX, function(req, res, next) { - var dn = req.dn.toString(); - if (!db[dn]) - return next(new ldap.NoSuchObjectError(dn)); - - if (!db[dn].userpassword) - return next(new ldap.NoSuchAttributeError('userPassword')); - - if (db[dn].userpassword.indexOf(req.credentials) === -1) - return next(new ldap.InvalidCredentialsError()); - - res.end(); - return next(); - }); - - server.compare(SUFFIX, authorize, function(req, res, next) { - var dn = req.dn.toString(); - if (!db[dn]) - return next(new ldap.NoSuchObjectError(dn)); - - if (!db[dn][req.attribute]) - return next(new ldap.NoSuchAttributeError(req.attribute)); - - var matches = false; - var vals = db[dn][req.attribute]; - for (var i = 0; i < vals.length; i++) { - if (vals[i] === req.value) { - matches = true; - break; - } - } - - res.end(matches); - return next(); - }); - - server.del(SUFFIX, authorize, function(req, res, next) { - var dn = req.dn.toString(); - if (!db[dn]) - return next(new ldap.NoSuchObjectError(dn)); - - delete db[dn]; - - res.end(); - return next(); - }); - - server.modify(SUFFIX, authorize, function(req, res, next) { - var dn = req.dn.toString(); - if (!req.changes.length) - return next(new ldap.ProtocolError('changes required')); - if (!db[dn]) - return next(new ldap.NoSuchObjectError(dn)); - - var entry = db[dn]; - - for (var i = 0; i < req.changes.length; i++) { - mod = req.changes[i].modification; - switch (req.changes[i].operation) { - case 'replace': - if (!entry[mod.type]) - return next(new ldap.NoSuchAttributeError(mod.type)); - - if (!mod.vals || !mod.vals.length) { - delete entry[mod.type]; - } else { - entry[mod.type] = mod.vals; - } - - break; - - case 'add': - if (!entry[mod.type]) { - entry[mod.type] = mod.vals; - } else { - mod.vals.forEach(function(v) { - if (entry[mod.type].indexOf(v) === -1) - entry[mod.type].push(v); - }); - } - - break; - - case 'delete': - if (!entry[mod.type]) - return next(new ldap.NoSuchAttributeError(mod.type)); - - delete entry[mod.type]; - - break; - } - } - - res.end(); - return next(); - }); - - server.search(SUFFIX, authorize, function(req, res, next) { - var dn = req.dn.toString(); - if (!db[dn]) - return next(new ldap.NoSuchObjectError(dn)); - - var scopeCheck; - - switch (req.scope) { - case 'base': - if (req.filter.matches(db[dn])) { - res.send({ - dn: dn, - attributes: db[dn] - }); - } - - res.end(); - return next(); - - case 'one': - scopeCheck = function(k) { - if (req.dn.equals(k)) - return true; - - var parent = ldap.parseDN(k).parent(); - return (parent ? parent.equals(req.dn) : false); - }; - break; - - case 'sub': - scopeCheck = function(k) { - return (req.dn.equals(k) || req.dn.parentOf(k)); - }; - - break; - } - - Object.keys(db).forEach(function(key) { - if (!scopeCheck(key)) - return; - - if (req.filter.matches(db[key])) { - res.send({ - dn: key, - attributes: db[key] - }); - } + if (req.filter.matches(db[key])) { + res.send({ + dn: key, + attributes: db[key] }); + } + } - res.end(); - return next(); - }); + res.end(); + return next(); +}); - ///--- Fire it up +///--- Fire it up - server.listen(1389, function() { - console.log('LDAP server up at: %s', server.url); - }); +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) { - if (!req.connection.ldap.bindDN.equals('cn=root')) - return next(new ldap.InsufficientAccessRightsError()); +function authorize(req, res, next) { + if (!req.connection.ldap.bindDN.equals('cn=root')) + return next(new ldap.InsufficientAccessRightsError()); - return next(); + return next(); +} + + +function loadPasswdFile(req, res, next) { + fs.readFile('/etc/passwd', 'utf8', (err, data) => { + if (err) + return next(new ldap.OperationsError(err.message)); + + req.users = {}; + + const lines = data.split('\n'); + for (const line of lines) { + if (!line || /^#/.test(line)) + continue; + + const record = line.split(':'); + if (!record || !record.length) + continue; + + req.users[record[0]] = { + dn: 'cn=' + record[0] + ', ou=users, o=myhost', + attributes: { + cn: record[0], + uid: record[2], + gid: record[3], + description: record[4], + homedirectory: record[5], + shell: record[6] || '', + objectclass: 'unixUser' + } + }; } + return next(); + }); +} - function loadPasswdFile(req, res, next) { - fs.readFile('/etc/passwd', 'utf8', function(err, data) { - if (err) - return next(new ldap.OperationsError(err.message)); - req.users = {}; +const pre = [authorize, loadPasswdFile]; - var lines = data.split('\n'); - for (var i = 0; i < lines.length; i++) { - if (!lines[i] || /^#/.test(lines[i])) - continue; - var record = lines[i].split(':'); - if (!record || !record.length) - continue; - req.users[record[0]] = { - dn: 'cn=' + record[0] + ', ou=users, o=myhost', - attributes: { - cn: record[0], - uid: record[2], - gid: record[3], - description: record[4], - homedirectory: record[5], - shell: record[6] || '', - objectclass: 'unixUser' - } - }; - } +///--- Mainline - return next(); - }); +const server = ldap.createServer(); + +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, (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())); + + const entry = req.toObject().attributes; + + if (entry.objectclass.indexOf('unixUser') === -1) + return next(new ldap.ConstraintViolationError('entry must be a unixUser')); + + const opts = ['-m']; + if (entry.description) { + opts.push('-c'); + opts.push(entry.description[0]); + } + if (entry.homedirectory) { + opts.push('-d'); + opts.push(entry.homedirectory[0]); + } + if (entry.gid) { + opts.push('-g'); + opts.push(entry.gid[0]); + } + if (entry.shell) { + opts.push('-s'); + opts.push(entry.shell[0]); + } + if (entry.uid) { + opts.push('-u'); + opts.push(entry.uid[0]); + } + opts.push(entry.cn[0]); + const useradd = spawn('useradd', opts); + + const messages = []; + + useradd.stdout.on('data', (data) => { + messages.push(data.toString()); + }); + useradd.stderr.on('data', (data) => { + messages.push(data.toString()); + }); + + useradd.on('exit', (code) => { + if (code !== 0) { + let msg = '' + code; + if (messages.length) + msg += ': ' + messages.join(); + return next(new ldap.OperationsError(msg)); } + res.end(); + return next(); + }); +}); - var pre = [authorize, loadPasswdFile]; + +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')); + + const user = req.users[req.dn.rdns[0].cn].attributes; + let mod; + + 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 ' + + 'allowed')); + break; + case 'add': + case 'delete': + return next(new ldap.UnwillingToPerformError('only replace allowed')); + } + } + + const passwd = spawn('chpasswd', ['-c', 'MD5']); + passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8'); + + passwd.on('exit', (code) => { + if (code !== 0) + return next(new ldap.OperationsError('' + code)); + + res.end(); + return 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())); + + const userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]); + + const messages = []; + userdel.stdout.on('data', (data) => { + messages.push(data.toString()); + }); + userdel.stderr.on('data', (data) => { + messages.push(data.toString()); + }); + + userdel.on('exit', (code) => { + if (code !== 0) { + let msg = '' + code; + if (messages.length) + msg += ': ' + messages.join(); + return next(new ldap.OperationsError(msg)); + } + + res.end(); + return next(); + }); +}); + + +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(); +}); - ///--- Mainline - - var server = ldap.createServer(); - - server.bind('cn=root', function(req, res, next) { - if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') - return next(new ldap.InvalidCredentialsError()); - - res.end(); - return next(); - }); - - - server.add('ou=users, o=myhost', pre, function(req, res, next) { - if (!req.dn.rdns[0].cn) - return next(new ldap.ConstraintViolationError('cn required')); - - if (req.users[req.dn.rdns[0].cn]) - return next(new ldap.EntryAlreadyExistsError(req.dn.toString())); - - var entry = req.toObject().attributes; - - if (entry.objectclass.indexOf('unixUser') === -1) - return next(new ldap.ConstraintViolationError('entry must be a unixUser')); - - var opts = ['-m']; - if (entry.description) { - opts.push('-c'); - opts.push(entry.description[0]); - } - if (entry.homedirectory) { - opts.push('-d'); - opts.push(entry.homedirectory[0]); - } - if (entry.gid) { - opts.push('-g'); - opts.push(entry.gid[0]); - } - if (entry.shell) { - opts.push('-s'); - opts.push(entry.shell[0]); - } - if (entry.uid) { - opts.push('-u'); - opts.push(entry.uid[0]); - } - opts.push(entry.cn[0]); - var useradd = spawn('useradd', opts); - - var messages = []; - - useradd.stdout.on('data', function(data) { - messages.push(data.toString()); - }); - useradd.stderr.on('data', function(data) { - messages.push(data.toString()); - }); - - useradd.on('exit', function(code) { - if (code !== 0) { - var msg = '' + code; - if (messages.length) - msg += ': ' + messages.join(); - return next(new ldap.OperationsError(msg)); - } - - res.end(); - return next(); - }); - }); - - - server.modify('ou=users, o=myhost', pre, function(req, res, next) { - if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn]) - return next(new ldap.NoSuchObjectError(req.dn.toString())); - - if (!req.changes.length) - return next(new ldap.ProtocolError('changes required')); - - var user = req.users[req.dn.rdns[0].cn].attributes; - var mod; - - for (var i = 0; i < req.changes.length; i++) { - mod = req.changes[i].modification; - switch (req.changes[i].operation) { - case 'replace': - if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length) - return next(new ldap.UnwillingToPerformError('only password updates ' + - 'allowed')); - break; - case 'add': - case 'delete': - return next(new ldap.UnwillingToPerformError('only replace allowed')); - } - } - - var passwd = spawn('chpasswd', ['-c', 'MD5']); - passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8'); - - passwd.on('exit', function(code) { - if (code !== 0) - return next(new ldap.OperationsError('' + code)); - - res.end(); - return next(); - }); - }); - - - server.del('ou=users, o=myhost', pre, function(req, res, next) { - if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn]) - return next(new ldap.NoSuchObjectError(req.dn.toString())); - - var userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]); - - var messages = []; - userdel.stdout.on('data', function(data) { - messages.push(data.toString()); - }); - userdel.stderr.on('data', function(data) { - messages.push(data.toString()); - }); - - userdel.on('exit', function(code) { - if (code !== 0) { - var msg = '' + code; - if (messages.length) - msg += ': ' + messages.join(); - return next(new ldap.OperationsError(msg)); - } - - res.end(); - return next(); - }); - }); - - - server.search('o=myhost', pre, function(req, res, next) { - Object.keys(req.users).forEach(function(k) { - if (req.filter.matches(req.users[k].attributes)) - res.send(req.users[k]); - }); - - res.end(); - return next(); - }); - - - - // LDAP "standard" listens on 389, but whatever. - server.listen(1389, '127.0.0.1', function() { - console.log('/etc/passwd LDAP server up at: %s', server.url); - }); +// 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,112 +436,117 @@ 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({ - user: "abook", - password: "abook", - database: "abook" - }); +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 " + - "FROM contacts c JOIN users u ON c.user_id=u.id", - function(err, contacts) { - if (err) { - console.log("Error fetching contacts", err); - process.exit(1); +db.query("SELECT c.*,u.username,u.password " + + "FROM contacts c JOIN users u ON c.user_id=u.id", + (err, contacts) => { + if (err) { + console.log("Error fetching contacts", err); + process.exit(1); + } + + 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 + }; + } + + const p = contact.name.indexOf(" "); + if (p != -1) + contact.firstname = contact.name.substr(0, p); + + p = contact.name.lastIndexOf(" "); + if (p != -1) + contact.surname = contact.name.substr(p + 1); + + addrbooks[contact.username].push({ + dn: "cn=" + contact.name + ", " + basedn, + attributes: { + objectclass: [ "top" ], + cn: contact.name, + mail: contact.email, + givenname: contact.firstname, + sn: contact.surname, + ou: company } - - 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 - }; - } - - var p = contacts[i].name.indexOf(" "); - if (p != -1) - contacts[i].firstname = contacts[i].name.substr(0, p); - - p = contacts[i].name.lastIndexOf(" "); - if (p != -1) - contacts[i].surname = contacts[i].name.substr(p + 1); - - addrbooks[contacts[i].username].push({ - dn: "cn=" + contacts[i].name + ", " + basedn, - attributes: { - objectclass: [ "top" ], - cn: contacts[i].name, - mail: contacts[i].email, - givenname: contacts[i].firstname, - sn: contacts[i].surname, - ou: company - } - }); - } - - server.bind(basedn, function (req, res, next) { - var username = req.dn.toString(), - password = req.credentials; - - if (!userinfo.hasOwnProperty(username) || - userinfo[username].pwd != password) { - return next(new ldap.InvalidCredentialsError()); - } - - res.end(); - return next(); - }); - - server.search(basedn, function(req, res, next) { - var 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]); - } - } - res.end(); - }); - - server.listen(ldap_port, function() { - console.log("Addressbook started at %s", server.url); - }); }); + } + + server.bind(basedn, (req, res, next) => { + const username = req.dn.toString(); + const password = req.credentials; + + if (!userinfo.hasOwnProperty(username) || + userinfo[username].pwd != password) { + return next(new ldap.InvalidCredentialsError()); + } + + res.end(); + return next(); + }); + + server.search(basedn, (req, res, next) => { + const binddn = req.connection.ldap.bindDN.toString(); + + if (userinfo.hasOwnProperty(binddn)) { + for (const abook of userinfo[binddn].abook) { + if (req.filter.matches(abook.attributes)) + res.send(abook); + } + } + res.end(); + }); + + 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 \ - -w demo -b "dc=example,dc=com" objectclass=* +```sh +$ ldapsearch -H ldap://localhost:389 -x -D cn=demo,dc=example,dc=com \ + -w demo -b "dc=example,dc=com" objectclass=* +``` diff --git a/docs/filters.md b/docs/filters.md index d83f7cd..302eb18 100644 --- a/docs/filters.md +++ b/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({ - attribute: 'cn', - value: 'foo' - }); +```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({ - attribute: 'cn' - }); +```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: - { - initial: 'foo', - any: ['bar', 'cat'], - final: 'dog' - } +```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({ - attribute: 'cn', - initial: 'foo', - any: ['bar'], - final: 'baz' - }); +```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({ - attribute: 'cn', - value: 'foo', - }); +```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({ - attribute: 'cn', - value: 'foo', - }); +```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,26 +204,30 @@ 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({ - filters: [ - new EqualityFilter({ - attribute: 'cn', - value: 'foo' - }), - new EqualityFilter({ - attribute: 'sn', - value: 'bar' - }) - ] - }); +```js +const f = new AndFilter({ + filters: [ + new EqualityFilter({ + attribute: 'cn', + value: 'foo' + }), + new EqualityFilter({ + attribute: 'sn', + 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,26 +238,30 @@ 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({ - filters: [ - new EqualityFilter({ - attribute: 'cn', - value: 'foo' - }), - new EqualityFilter({ - attribute: 'sn', - value: 'bar' - }) - ] - }); +```js +const f = new OrFilter({ + filters: [ + new EqualityFilter({ + attribute: 'cn', + value: 'foo' + }), + new EqualityFilter({ + attribute: 'sn', + 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({ - filter: new EqualityFilter({ - attribute: 'cn', - value: 'foo' - }) - }); +```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({ - attribute: 'cn', - value: 'foo' - }); +```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 +``` diff --git a/docs/guide.md b/docs/guide.md index 761813f..d737296 100644 --- a/docs/guide.md +++ b/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 - +``` + o=example + / \ + ou=users ou=groups + / | | \ + cn=john cn=jane cn=dudes cn=dudettes + / +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 +```sh +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 +```sh +$ 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() { - console.log('/etc/passwd LDAP server up at: %s', server.url); - }); +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=* +```sh +$ 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) { - if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') - return next(new ldap.InvalidCredentialsError()); +```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(); - }); + 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=* +```sh +$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w foo -b "o=myhost" objectclass=* - ldap_bind: Invalid credentials (49) - matched DN: cn=root - additional info: Invalid Credentials +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=* +```sh +$ 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) { - if (!req.connection.ldap.bindDN.equals('cn=root')) - return next(new ldap.InsufficientAccessRightsError()); +```js +function authorize(req, res, next) { + if (!req.connection.ldap.bindDN.equals('cn=root')) + return next(new ldap.InsufficientAccessRightsError()); - return next(); - } + 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 +```sh +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,77 +269,86 @@ 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) { - if (err) - return next(new ldap.OperationsError(err.message)); +```js +function loadPasswdFile(req, res, next) { + fs.readFile('/etc/passwd', 'utf8', (err, data) => { + if (err) + return next(new ldap.OperationsError(err.message)); - req.users = {}; + req.users = {}; - var lines = data.split('\n'); - for (var i = 0; i < lines.length; i++) { - if (!lines[i] || /^#/.test(lines[i])) - continue; + const lines = data.split('\n'); + for (const line of lines) { + if (!line || /^#/.test(line)) + continue; - var record = lines[i].split(':'); - if (!record || !record.length) - continue; + const record = line.split(':'); + if (!record || !record.length) + continue; - req.users[record[0]] = { - dn: 'cn=' + record[0] + ', ou=users, o=myhost', - attributes: { - cn: record[0], - uid: record[2], - gid: record[3], - description: record[4], - homedirectory: record[5], - shell: record[6] || '', - objectclass: 'unixUser' - } - }; + req.users[record[0]] = { + dn: 'cn=' + record[0] + ', ou=users, o=myhost', + attributes: { + cn: record[0], + uid: record[2], + gid: record[3], + description: record[4], + homedirectory: record[5], + shell: record[6] || '', + objectclass: 'unixUser' } - - return next(); - }); + }; } + 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) { - if (req.filter.matches(req.users[k].attributes)) - res.send(req.users[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(); - }); + 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 +```sh +$ 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=* - ... +```sh +$ 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 +```sh +$ 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,18 +392,20 @@ and made the cheesiest transform ever, which is making up a "search entry." A search entry _must_ have a DN so the client knows what record it is, and a set of attributes. So that's why we did this: - var entry = { - dn: 'cn=' + record[0] + ', ou=users, o=myhost', - attributes: { - cn: record[0], - uid: record[2], - gid: record[3], - description: record[4], - homedirectory: record[5], - shell: record[6] || '', - objectclass: 'unixUser' - } - }; +```js +const entry = { + dn: 'cn=' + record[0] + ', ou=users, o=myhost', + attributes: { + cn: record[0], + uid: record[2], + gid: record[3], + description: record[4], + homedirectory: record[5], + shell: record[6] || '', + objectclass: 'unixUser' + } +}; +``` 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,120 +420,133 @@ 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 +```sh +$ 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) { - if (!req.dn.rdns[0].attrs.cn) - return next(new ldap.ConstraintViolationError('cn required')); +```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())); + 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')); + if (entry.objectclass.indexOf('unixUser') === -1) + return next(new ldap.ConstraintViolationError('entry must be a unixUser')); - var opts = ['-m']; - if (entry.description) { - opts.push('-c'); - opts.push(entry.description[0]); - } - if (entry.homedirectory) { - opts.push('-d'); - opts.push(entry.homedirectory[0]); - } - if (entry.gid) { - opts.push('-g'); - opts.push(entry.gid[0]); - } - if (entry.shell) { - opts.push('-s'); - opts.push(entry.shell[0]); - } - if (entry.uid) { - opts.push('-u'); - opts.push(entry.uid[0]); - } - opts.push(entry.cn[0]); - var useradd = spawn('useradd', opts); + const opts = ['-m']; + if (entry.description) { + opts.push('-c'); + opts.push(entry.description[0]); + } + if (entry.homedirectory) { + opts.push('-d'); + opts.push(entry.homedirectory[0]); + } + if (entry.gid) { + opts.push('-g'); + opts.push(entry.gid[0]); + } + if (entry.shell) { + opts.push('-s'); + opts.push(entry.shell[0]); + } + if (entry.uid) { + opts.push('-u'); + opts.push(entry.uid[0]); + } + opts.push(entry.cn[0]); + const useradd = spawn('useradd', opts); - var messages = []; + const messages = []; - useradd.stdout.on('data', function(data) { - messages.push(data.toString()); - }); - useradd.stderr.on('data', function(data) { - messages.push(data.toString()); - }); + useradd.stdout.on('data', (data) => { + messages.push(data.toString()); + }); + useradd.stderr.on('data', (data) => { + messages.push(data.toString()); + }); - useradd.on('exit', function(code) { - if (code !== 0) { - var msg = '' + code; - if (messages.length) - msg += ': ' + messages.join(); - return next(new ldap.OperationsError(msg)); - } + useradd.on('exit', (code) => { + if (code !== 0) { + let msg = '' + code; + if (messages.length) + msg += ': ' + messages.join(); + return next(new ldap.OperationsError(msg)); + } - res.end(); - return next(); - }); - }); + 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 +```sh +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" +```sh +$ 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 +```sh +$ ldapsearch -H ldap://localhost:1389 -LLL -x -D cn=root -w secret -b "ou=users, o=myhost" cn=ldapjs +dn: cn=ldapjs, ou=users, o=myhost +cn: ldapjs +uid: 1001 +gid: 1001 +description: Created via ldapadd +homedirectory: /home/ldapjs +shell: /bin/bash +objectclass: unixUser +``` As before, here's a breakdown of the code: - server.add('ou=users, o=myhost', pre, function(req, res, next) { - if (!req.dn.rdns[0].attrs.cn) - return next(new ldap.ConstraintViolationError('cn required')); +```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())); + 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')); + if (entry.objectclass.indexOf('unixUser') === -1) + return next(new ldap.ConstraintViolationError('entry must be a unixUser')); +}); +``` A few new things: @@ -534,42 +581,43 @@ Unlike HTTP, "partial" document updates are fully specified as part of the RFC, so appending, removing, or replacing a single attribute is pretty natural. Go ahead and add the following code into your source file: - server.modify('ou=users, o=myhost', pre, function(req, res, next) { - if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value]) - return next(new ldap.NoSuchObjectError(req.dn.toString())); +```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')); + 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++) { - mod = req.changes[i].modification; - switch (req.changes[i].operation) { - case 'replace': - if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length) - return next(new ldap.UnwillingToPerformError('only password updates ' + - 'allowed')); - break; - case 'add': - case 'delete': - return next(new ldap.UnwillingToPerformError('only replace allowed')); - } - } + for (const i = 0; i < req.changes.length; i++) { + mod = req.changes[i].modification; + switch (req.changes[i].operation) { + case 'replace': + if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length) + return next(new ldap.UnwillingToPerformError('only password updates ' + + 'allowed')); + break; + case 'add': + case 'delete': + return next(new ldap.UnwillingToPerformError('only replace allowed')); + } + } - var passwd = spawn('chpasswd', ['-c', 'MD5']); - passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8'); + const passwd = spawn('chpasswd', ['-c', 'MD5']); + passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8'); - passwd.on('exit', function(code) { - if (code !== 0) - return next(new ldap.OperationsError(code)); - - res.end(); - return next(); - }); - }); + 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 - - +```sh +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 +```sh +$ 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,37 +648,40 @@ 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) { - if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value]) - return next(new ldap.NoSuchObjectError(req.dn.toString())); +```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) { - messages.push(data.toString()); - }); - userdel.stderr.on('data', function(data) { - messages.push(data.toString()); - }); + const messages = []; + userdel.stdout.on('data', (data) => { + messages.push(data.toString()); + }); + userdel.stderr.on('data', (data) => { + messages.push(data.toString()); + }); - userdel.on('exit', function(code) { - if (code !== 0) { - var msg = '' + code; - if (messages.length) - msg += ': ' + messages.join(); - return next(new ldap.OperationsError(msg)); - } + userdel.on('exit', (code) => { + if (code !== 0) { + let msg = '' + code; + if (messages.length) + msg += ': ' + messages.join(); + return next(new ldap.OperationsError(msg)); + } - res.end(); - return next(); - }); - }); + 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" - +```sh +$ ldapdelete -H ldap://localhost:1389 -x -D cn=root -w secret "cn=ldapjs, ou=users, o=myhost" +``` # Where to go from here diff --git a/docs/index.md b/docs/index.md index 704483d..8ec12f2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,32 +17,36 @@ with HTTP services in node and [restify](http://restify.com). - 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 = { - dn: req.dn.toString(), - attributes: { - objectclass: ['organization', 'top'], - o: 'example' - } - }; +server.search('o=example', (req, res, next) => { + const obj = { + dn: req.dn.toString(), + attributes: { + objectclass: ['organization', 'top'], + o: 'example' + } + }; - if (req.filter.matches(obj.attributes)) - res.send(obj); + if (req.filter.matches(obj.attributes)) + res.send(obj); - res.end(); - }); + res.end(); +}); - server.listen(1389, function() { - console.log('LDAP server listening at %s', server.url); - }); +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=* +```sh +$ 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 +```sh +$ npm install ldapjs +``` If you're new to LDAP, check out the [guide](guide.html). Otherwise, the API documentation is: diff --git a/docs/server.md b/docs/server.md index 639ff18..05cace1 100644 --- a/docs/server.md +++ b/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,12 @@ 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 +119,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) { - if (!req.connection.ldap.bindDN.equals('cn=root')) - return next(new ldap.InsufficientAccessRightsError()); +```js +function authorize(req, res, next) { + if (!req.connection.ldap.bindDN.equals('cn=root')) + return next(new ldap.InsufficientAccessRightsError()); - return next(); - } + 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 +140,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({ - "host": "localhost", - "port": 8098, - "bucket": "example", - "indexes": ["l", "cn"], - "uniqueIndexes": ["uid"], - "numConnections": 5 - }); +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", - backend, - backend.add()); - ... +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 +171,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) { - console.log('hello world'); - return next(); - }); +```js +server.use(function(req, res, next) { + console.log('hello world'); + return next(); +}); +``` ## Common Request Elements @@ -210,11 +220,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 +234,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) { - console.log('bind DN: ' + req.dn.toString()); - console.log('bind PW: ' + req.credentials); - res.end(); - }); +```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 +273,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) { - console.log('DN: ' + req.dn.toString()); - console.log('Entry attributes: ' + req.toObject().attributes); - res.end(); - }); +```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 +303,16 @@ a standard JavaScript object. This operation will return a plain JavaScript object from the request that looks like: - { - dn: 'cn=foo, o=example', // string, not DN object - attributes: { - cn: ['foo'], - sn: ['bar'], - objectclass: ['person', 'top'] - } - } +```js +{ + dn: 'cn=foo, o=example', // string, not DN object + attributes: { + cn: ['foo'], + sn: ['bar'], + objectclass: ['person', 'top'] + } +} +``` ## AddResponse @@ -304,12 +322,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) { - console.log('base object: ' + req.dn.toString()); - console.log('scope: ' + req.scope); - console.log('filter: ' + req.filter.toString()); - res.end(); - }); +```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,34 +387,38 @@ 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 = { - dn: 'o=example', - attributes: { - objectclass: ['top', 'organization'], - o: ['example'] - } - }; +```js +server.search('o=example', (req, res, next) => { + const obj = { + dn: 'o=example', + attributes: { + objectclass: ['top', 'organization'], + o: ['example'] + } + }; - if (req.filter.matches(obj)) - res.send(obj) + if (req.filter.matches(obj)) + res.send(obj) - res.end(); - }); + res.end(); +}); +``` # modify Allows you to handle an LDAP modify operation. - server.modify('o=example', function(req, res, next) { - console.log('DN: ' + req.dn.toString()); - console.log('changes:'); - req.changes.forEach(function(c) { - console.log(' operation: ' + c.operation); - console.log(' modification: ' + c.modification.toString()); - }); - res.end(); - }); +```js +server.modify('o=example', (req, res, next) => { + console.log('DN: ' + req.dn.toString()); + console.log('changes:'); + for (const c of req.changes) { + console.log(' operation: ' + c.operation); + console.log(' modification: ' + c.modification.toString()); + } + res.end(); +}); +``` ## ModifyRequest @@ -431,10 +455,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) { - console.log('DN: ' + req.dn.toString()); - res.end(); - }); +```js +server.del('o=example', (req, res, next) => { + console.log('DN: ' + req.dn.toString()); + res.end(); +}); +``` ## DeleteRequest @@ -451,12 +477,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) { - console.log('DN: ' + req.dn.toString()); - console.log('attribute name: ' + req.attribute); - console.log('attribute value: ' + req.value); - res.end(req.value === 'foo'); - }); +```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,15 +511,17 @@ 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) { - console.log('DN: ' + req.dn.toString()); - console.log('new RDN: ' + req.newRdn.toString()); - console.log('deleteOldRDN: ' + req.deleteOldRdn); - console.log('new superior: ' + - (req.newSuperior ? req.newSuperior.toString() : '')); +```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); + console.log('new superior: ' + + (req.newSuperior ? req.newSuperior.toString() : '')); - res.end(); - }); + res.end(); +}); +``` ## ModifyDNRequest @@ -525,14 +555,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) { - console.log('name: ' + req.name); - console.log('value: ' + req.value); - res.value = 'u:xxyyz@EXAMPLE.NET'; - res.end(); - return 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 +595,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) { - res.end(); - }); +```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 From 2de077d39f85a68b97dc2cc73a3c3316e799f82c Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Wed, 24 Feb 2021 16:06:13 -0600 Subject: [PATCH 2/4] docs: highlight code --- package.json | 5 +++-- scripts/build-docs.js | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d689f2c..4a1883a 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/build-docs.js b/scripts/build-docs.js index 867cbc8..bbd62e0 100644 --- a/scripts/build-docs.js +++ b/scripts/build-docs.js @@ -2,6 +2,16 @@ const fs = require('fs/promises') const path = require('path') const marked = require('marked') const fm = require('front-matter') +const { highlight, highlightAuto } = require('highlight.js') + +marked.use({ + highlight: (code, lang) => { + if (!lang) { + return highlightAuto(code).value + } + return highlight(lang, code).value + } +}) function tocHTML (toc) { let html = '