diff --git a/Makefile b/Makefile index 2df4cdb..9881c94 100644 --- a/Makefile +++ b/Makefile @@ -53,8 +53,12 @@ doc: dep @rm -rf ${DOCPKGDIR} @mkdir -p ${DOCPKGDIR} ${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/client.md + ${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/dn.md + ${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/errors.md ${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/examples.md + ${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/filters.md ${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/guide.md + ${RESTDOWN} -m ${DOCPKGDIR} -D mediaroot=media ./docs/server.md rm docs/*.json mv docs/*.html ${DOCPKGDIR} (cd ${DOCPKGDIR} && $(TAR) -czf ${SRC}/${NAME}-docs-`git log -1 --pretty='format:%h'`.tar.gz *) diff --git a/docs/client.md b/docs/client.md index 0fda458..4955e08 100644 --- a/docs/client.md +++ b/docs/client.md @@ -14,7 +14,7 @@ LDAP, so if you're not, hit the [guide](http://ldapjs.org/guide.html) first. # Create a client -The code to create a new client liiks like: +The code to create a new client looks like: var client = ldap.createClient({ url: 'ldap://127.0.0.1:1389' diff --git a/docs/dn.md b/docs/dn.md new file mode 100644 index 0000000..2c43d04 --- /dev/null +++ b/docs/dn.md @@ -0,0 +1,91 @@ +--- +title: ldapjs +brand: spartan +markdown2extras: wiki-tables +logo-color: green +logo-font-family: google:Aldrich, Verdana, sans-serif +header-font-family: google:Aldrich, Verdana, sans-serif +--- + +# Overview + +This documents the ldapjs DN API; this assumes that you are familiar with +LDAP, so if you're not, hit the [guide](http://ldapjs.org/guide.html) first. + +DNs are LDAP distinguished names, and are composed of a set of RDNs (relative +distinguished names). [RFC2253](http://www.ietf.org/rfc/rfc2253.txt) has the +complete specification, but basically an RDN is an attribute value assertion +with `=` as the seperator, like: `cn=foo` where 'cn' is 'commonName' and 'foo' +is the value. You can have compound RDNs by using the `+` character: +`cn=foo+sn=bar`. As stated above, DNs are a set of RDNs, typically separated +with the `,` character, like: `cn=foo, ou=people, o=example`. This uniquely +identifies an entry in the tree, and is read "bottom up". + +# parseDN(dnString) + +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; + + var dn = parseDN('cn=foo+sn=bar, ou=people, o=example'); + console.log(dn.toString()); + +# DN + +The DN object is largely what you'll be interacting with, since all the server +APIs are setup to give you a DN object. + +## childOf(dn) + +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 { + ... + } + }); + +## 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 { + ... + } + }); + +## 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 { + ... + } + }); + +## parent() + +Returns a DN object that is the direct parent of `this`. If there is no parent +this can return `null` (e.g. `parseDN('o=example').parent()` will return null). + +## toString() + +Returns the string representation of `this`. + + server.add('o=example', function(req, res, next) { + console.log(req.dn.toString()); + }); diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..748615a --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,93 @@ +--- +title: ldapjs +brand: spartan +markdown2extras: wiki-tables +logo-color: green +logo-font-family: google:Aldrich, Verdana, sans-serif +header-font-family: google:Aldrich, Verdana, sans-serif +--- + +# Overview + +This documents the ldapjs Errors API; this assumes that you are familiar with +LDAP, so if you're not, hit the [guide](http://ldapjs.org/guide.html) first. + +All errors in the ldapjs framework extend from an abstract error type called +`LDAPError`. In addition to the properties listed below, all errors will have +a `stack` property correctly set. + +In general, you'll be using the errors in ldapjs like: + + var ldap = require('ldapjs'); + + var 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())); + + ... + }); + +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. + +All errors will have the following properties: + +## code + +Returns the LDAP status code associated with this error. + +## name + +The name of this error. + +## message + +The message that will be returned to the client. + +# Complete list of LDAPError subclasses + +* OperationsError +* ProtocolError +* TimeLimitExceededError +* SizeLimitExceededError +* CompareFalseError +* CompareTrueError +* AuthMethodNotSupportedError +* StrongAuthRequiredError +* ReferralError +* AdminLimitExceededError +* UnavailableCriticalExtensionError +* ConfidentialityRequiredError +* SaslBindInProgressError +* NoSuchAttributeError +* UndefinedAttributeTypeError +* InappropriateMatchingError +* ConstraintViolationError +* AttributeOrValueExistsError +* InvalidAttriubteSyntaxError +* NoSuchObjectError +* AliasProblemError +* InvalidDnSyntaxError +* AliasDerefProblemError +* InappropriateAuthenticationError +* InvalidCredentialsError +* InsufficientAccessRightsError +* BusyError +* UnavailableError +* UnwillingToPerformError +* LoopDetectError +* NamingViolationError +* ObjectclassViolationError +* NotAllowedOnNonLeafError +* NotAllowedOnRdnError +* EntryAlreadyExistsError +* ObjectclassModsProhibitedError +* AffectsMultipleDsasError +* OtherError diff --git a/docs/examples.md b/docs/examples.md index 9de6627..edc05af 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -16,7 +16,7 @@ header-font-family: google:Aldrich, Verdana, sans-serif function authorize(req, res, next) { if (!req.connection.ldap.bindDN.equals('cn=root')) - return next(new ldap.InsufficientAccessRightsError()); + return next(new ldap.InsufficientAccessRightsError()); return next(); } @@ -32,7 +32,7 @@ header-font-family: google:Aldrich, Verdana, sans-serif server.bind('cn=root', function(req, res, next) { if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') - return next(new ldap.InvalidCredentialsError()); + return next(new ldap.InvalidCredentialsError()); res.end(); return next(); @@ -42,7 +42,7 @@ header-font-family: google:Aldrich, Verdana, sans-serif var dn = req.dn.toString(); if (db[dn]) - return next(new ldap.EntryAlreadyExistsError(dn)); + return next(new ldap.EntryAlreadyExistsError(dn)); db[dn] = req.toObject().attributes; res.end(); @@ -52,13 +52,13 @@ header-font-family: google:Aldrich, Verdana, sans-serif server.bind(SUFFIX, function(req, res, next) { var dn = req.dn.toString(); if (!db[dn]) - return next(new ldap.NoSuchObjectError(dn)); + return next(new ldap.NoSuchObjectError(dn)); if (!dn[dn].userpassword) - return next(new ldap.NoSuchAttributeError('userPassword')); + return next(new ldap.NoSuchAttributeError('userPassword')); if (db[dn].userpassword !== req.credentials) - return next(new ldap.InvalidCredentialsError()); + return next(new ldap.InvalidCredentialsError()); res.end(); return next(); @@ -67,18 +67,18 @@ header-font-family: google:Aldrich, Verdana, sans-serif server.compare(SUFFIX, authorize, function(req, res, next) { var dn = req.dn.toString(); if (!db[dn]) - return next(new ldap.NoSuchObjectError(dn)); + return next(new ldap.NoSuchObjectError(dn)); if (!db[dn][req.attribute]) - return next(new ldap.NoSuchAttributeError(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; - } + if (vals[i] === req.value) { + matches = true; + break; + } } res.end(matches); @@ -88,7 +88,7 @@ header-font-family: google:Aldrich, Verdana, sans-serif server.del(SUFFIX, authorize, function(req, res, next) { var dn = req.dn.toString(); if (!db[dn]) - return next(new ldap.NoSuchObjectError(dn)); + return next(new ldap.NoSuchObjectError(dn)); delete db[dn]; @@ -99,47 +99,47 @@ header-font-family: google:Aldrich, Verdana, sans-serif server.modify(SUFFIX, authorize, function(req, res, next) { var dn = req.dn.toString(); if (!req.changes.length) - return next(new ldap.ProtocolError('changes required')); + return next(new ldap.ProtocolError('changes required')); if (!db[dn]) - return next(new ldap.NoSuchObjectError(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)); + 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; - } + if (!mod.vals || !mod.vals.length) { + delete entry[mod.type]; + } else { + entry[mod.type] = mod.vals; + } - break; + 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); - }); - } + 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; + break; - case 'delete': - if (!entry[mod.type]) - return next(new ldap.NoSuchAttributeError(mod.type)); + case 'delete': + if (!entry[mod.type]) + return next(new ldap.NoSuchAttributeError(mod.type)); - delete entry[mod.type]; + delete entry[mod.type]; - break; - } + break; + } } res.end(); @@ -149,50 +149,50 @@ header-font-family: google:Aldrich, Verdana, sans-serif server.search(SUFFIX, authorize, function(req, res, next) { var dn = req.dn.toString(); if (!db[dn]) - return next(new ldap.NoSuchObjectError(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] - }); - } + if (req.filter.matches(db[dn])) { + res.send({ + dn: dn, + attributes: db[dn] + }); + } - res.end(); - return next(); + res.end(); + return next(); case 'one': - scopeCheck = function(k) { - if (req.dn.equals(k)) - return true; + scopeCheck = function(k) { + if (req.dn.equals(k)) + return true; - var parent = ldap.parseDN(k).parent(); - return (parent ? parent.equals(req.dn) : false); - }; - break; + 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)); - }; + scopeCheck = function(k) { + return (req.dn.equals(k) || req.dn.parentOf(k)); + }; - break; + break; } Object.keys(db).forEach(function(key) { - if (!scopeCheck(key)) - return; + 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(); diff --git a/docs/filters.md b/docs/filters.md new file mode 100644 index 0000000..8a09559 --- /dev/null +++ b/docs/filters.md @@ -0,0 +1,295 @@ +--- +title: ldapjs +brand: spartan +markdown2extras: wiki-tables +logo-color: green +logo-font-family: google:Aldrich, Verdana, sans-serif +header-font-family: google:Aldrich, Verdana, sans-serif +--- + +# Overview + +This documents the ldapjs server API; this assumes that you are familiar with +LDAP, so if you're not, hit the [guide](http://ldapjs.org/guide.html) first. + +LDAP search filters are really the backbone of LDAP search operations, and +ldapjs tries to get you in "easy" with them if your dataset is small, and also +lets you introspect them if you want to write a "query planner". For reference, +make sure to read over [RFC2254](http://www.ietf.org/rfc/rfc2254.txt), as this +explains the LDAPv3 text filter representation. + +Basically, ldapjs gives you a distinct object type mapping to each filter that +is context-sensitive. However, _all_ filters have a `matches()` api on them, if +that's all you need. Most filters will have an `attribute` property on them, +since "simple" filters all operate on an attribute/value assertion. The +"complex" filters are really aggregations of other filters (i.e. 'and'), and so +these don't provide that property. + +All Filters in the ldapjs framework extend from `Filter`, which wil have the +property `type` available; this will return a string name for the filter, and +will be one of: + +* _equal:_ an `EqualityFilter` +* _present:_ a `PresenceFilter` +* _substring:_ a `SubstringFilter` +* _ge:_ a `GreaterThanEqualsFilter` +* _le:_ a `LessThanEqualsFilter` +* _and:_ an `AndFilter` +* _or:_ an `OrFilter` +* _not:_ a `NotFilter` +* _approx:_ an `ApproximateMatchFilter` (quasi-supported in ldapjs) +* _ext:_ an `ExtensibleMatchFilter` (not supported in ldapjs) + +# parseFilter(filterString) + +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; + + var f = parseFilter('(objectclass=*)'); + +Is a "simple" filter, and would just return a `PresenceFilter` object. However, + + var f = parseFilter('(&(employeeType=manager)(l=Seattle))'); + +Would return an `AndFilter`, which would have a `filters` array of two +`EqualityFilter` objects. + +Note that `parseFilter` will throw if an invalid string is passed in +(that is, a syntactically invalid string). All filter objects in th + +# EqualityFilter + +The equality filter is used to check exact matching of attribute/value +assertions. This object will have an `attribute` and `value` property, and the +`name` proerty will be `equal`. + +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' + }); + + f.matches({cn: 'foo'}); => true + f.matches({cn: 'bar'}); => false + +Note that "strict" equality matching is used, and by default everything in +ldapjs (and LDAP) is a UTF-8 string. If you want comparison of numbers, or +something else, you'll need to use a middleware interceptor that transforms +values of objects. + +# PresenceFilter + +The presence filter is used to check if an object has an attribute at all, with +any value. This object will have an `attribute` property, and the `name` +property will be `present`. + +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' + }); + + f.matches({cn: 'foo'}); => true + f.matches({sn: 'foo'}); => false + +# SubstringFilter + +The substring filter is used to do wildcard matching of a string value. This +object will have an `attribute` property and then it will have an `initial` +property, which is the prefix match, an `any` which will be an array of strings +that are to be found _somewhere_ in the target string, and a `final` property, +which will be the suffix match of the string. `any` and `final` are both +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' + } + +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' + }); + + f.matches({cn: 'foobigbardogbaz'}); => true + f.matches({sn: 'fobigbardogbaz'}); => false + +# GreaterThanEqualsFilter + +The ge filter is used to do comparisons and ordering based on the value type. As +mentioned elsewhere, by default everything in LDAP and ldapjs is a string, so +this filter's `matches()` would be using lexicographical ordering of strings. +If you wanted `>=` semantics over numeric values, you would need to add some +middleware to convert values before comparison (and the value of the filter). +Note that the ldapjs schema middleware will do this. + +The GreaterThanEqualsFilter will have an `attribute` property, a `value` +property and the `name` property will be `ge`. + +The string syntax for a ge filter is: + + (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', + }); + + f.matches({cn: 'foobar'}); => true + f.matches({cn: 'abc'}); => false + +# LessThanEqualsFilter + +The le filter is used to do comparisons and ordering based on the value type. As +mentioned elsewhere, by default everything in LDAP and ldapjs is a string, so +this filter's `matches()` would be using lexicographical ordering of strings. +If you wanted `<=` semantics over numeric values, you would need to add some +middleware to convert values before comparison (and the value of the filter). +Note that the ldapjs schema middleware will do this. + +The string syntax for a le filter is: + + (cn<=foo) + +The LessThanEqualsFilter will have an `attribute` property, a `value` +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', + }); + + f.matches({cn: 'abc'}); => true + f.matches({cn: 'foo'}); => false + +# AndFilter + +The and filter is a complex filter that simply contains "child" filters. The +object will have a `filters` property which is an array of `Filter` objects. The +`name` property will be `and`. + +The string syntax for an and filter is (assuming below we're and'ing two +equality filters): + + (&(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' + }) + ] + }); + + f.matches({cn: 'foo', sn: 'bar'}); => true + f.matches({cn: 'foo', sn: 'baz'}); => false + +# OrFilter + +The or filter is a complex filter that simply contains "child" filters. The +object will have a `filters` property which is an array of `Filter` objects. The +`name` property will be `or`. + +The string syntax for an or filter is (assuming below we're or'ing two +equality filters): + + (|(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' + }) + ] + }); + + f.matches({cn: 'foo', sn: 'baz'}); => true + f.matches({cn: 'bar', sn: 'baz'}); => false + +# NotFilter + +The not filter is a complex filter that contains a single "child" filter. The +object will have a `filter` property which is an instance of a `Filter` object. +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)) + +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' + }) + }); + + f.matches({cn: 'bar'}); => true + f.matches({cn: 'foo'}); => false + +# ApproximateFilter + +The approximate filter is used to check "approximate" matching of +attribute/value assertions. This object will have an `attribute` and +`value` property, and the `name` proerty will be `approx`. + +As a side point, this is a useless filter. It's really only here if you have +some whacky client that's sending this. It just does an exact match (which +is what ActiveDirectory does too). + +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' + }); + + f.matches({cn: 'foo'}); => true + f.matches({cn: 'bar'}); => false + diff --git a/docs/server.md b/docs/server.md new file mode 100644 index 0000000..d00021c --- /dev/null +++ b/docs/server.md @@ -0,0 +1,562 @@ +--- +title: ldapjs +brand: spartan +markdown2extras: wiki-tables +logo-color: green +logo-font-family: google:Aldrich, Verdana, sans-serif +header-font-family: google:Aldrich, Verdana, sans-serif +--- + +# Overview + +This documents the ldapjs server API; this assumes that you are familiar with +LDAP, so if you're not, hit the [guide](http://ldapjs.org/guide.html) first. + +# Create a server + +The code to create a new server looks like: + + var server = ldap.createServer(); + +Full list of options: + +* _log4js:_ You can optionally pass in a log4js instance that the client will +get a logger from. You'll need to set the level to `TRACE` To get any output +from the client. +* _certificate:_ A PEM-encoded X.509 certificate; will cause this server to +run in TLS mode. +* _key:_ A PEM-encoded private key that corresponds to _certificate_ for SSL. + +## Properties on server + +### maxConnections + +Set this property to reject connections when the server's connection count gets +high. + +### connections (getter only) + +The number of concurrent connections on the server. + +### url + +Returns the fully qualified URL this server is listening on. For example: +`ldaps://10.1.2.3:1636`. If you haven't yet called `listen`, it will always +return `ldap://localhost:389`. + +### Event: 'close' +`function() {}` + +Emitted when the server closes. + +## Listening for requests + +The LDAP server API wraps up and mirrors the node +[listen](http://nodejs.org/docs/v0.4.11/api/net.html#server.listen) family of +APIs. + +After calling `listen`, the property `url` on the server object itself will be +available. + +Example: + + server.listen(389, '127.0.0.1', function() { + console.log(LDAP server listening at: ' + server.url); + }); + + +### Port and Host +`listen(port, [host], [callback])` + +Begin accepting connections on the specified port and host. If the host is +omitted, the server will accept connections directed to any IPv4 address +(INADDR_ANY). + +This function is asynchronous. The last parameter callback will be called when +the server has been bound. + +### Unix Domain Socket +`listen(path, [callback])` + +Start a UNIX socket server listening for connections on the given path. + +This function is asynchronous. The last parameter callback will be called when +the server has been bound. + +### File descriptor +`listenFD(fd)` + +Start a server listening for connections on the given file descriptor. + +This file descriptor must have already had the `bind(2)` and `listen(2)` system +calls invoked on it. Additionally, it must be set non-blocking; try +`fcntl(fd, F_SETFL, O_NONBLOCK)`. + +# Routes + +The LDAP server API is meant to be the LDAP-equivalent of the express/sinatra +paradigm of programming. Essentially every method is of the form +`OP(req, res, next)` where OP is one of bind, add, del, etc. You can chain +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()); + + return 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 +'backend' object. This is necessary if there are persistent connection pools, +caching, etc. that need to be placed in an object. + +For example [ldapjs-riak](https://github.com/mcavage/node-ldapjs-riak) is a +complete implementation of the LDAP protocol over +[Riak](http://www.basho.com/products_riak_overview.php). Getting an LDAP +server up with riak looks like: + + var ldap = require('ldapjs'); + var 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 + }); + + server.add("o=example", + backend, + backend.add()); + ... + +Basically, 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 +backend object, if applicable. After that you can pass in an arbitrary +combination of functions in the form `f(req, res, next)` or Arrays of functions +of the same signature (ldapjs will unroll them). + +Unlike HTTP, LDAP operations do not have a heterogenous format, so each +operation in the rest of the document includes documentation for the +request/response objects appropriate to that operation type. + +## Common Request Elements + +All request objects has the `dn` getter on it, which is "context-sensitive" +and returns the point in the tree that the operation wants to operate on. The +LDAP protocol itself sadly doesn't define operations this way, and has a unique +name for just about every op. So, ldapjs calls it `dn`. The DN object itself +is documented at [DN](/dn.html). + +All requests have an optional array of `Control` objects. `Control` will have +the properties `type` (string), `criticality` (boolean), and optionally a string +`value`. + +All request objects will have a `connection` object, which is the `net.Socket` +associated to this request. Off the `connection` object is an `ldap` object. +The most important property to pay attention to there is the `bindDN` property +which will be an instance of an `ldap.DN` object. This is what the client +authenticated as on this connection. If the client didn't bind, then a DN object +will be there defaulted to `cn=anonymous`. + +Additionally, request will have a `logId` parameter you can use to uniquely +identify the request/connection pair in logs (includes the LDAP messageID). + +## Common Response Elements + +All response objects will have an `end` method on them. By default, calling +`res.end()` with no arguments will return SUCCESS (0x00) to the client +(with the exception of `compare` which will return COMPARE_TRUE (0x06)). You +can pass in a status code to the `end()` method to return an alternate status +code. + +However, it's more common/easier to use the `return next(new LDAPError())` +pattern, since ldapjs will fill in the extra LDAPResult fields like matchedDN +and error message for you. + +## Errors + +ldapjs includes an exception hierarchy that directly corresponds to the RFC list +of error codes. The complete list is documented in [errors](/errors.html). But +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(); } + ); + +In the code snipped above, the third handler would never get invoked. + +# Bind + +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(); + }); + +## BindRequest + +BindRequest objects have the following properties: + +### version + +The LDAP protocol version the client is requesting to run this connection on. +Note that ldapjs only supports LDAP version 3. + +### name + +The DN the client is attempting to bind as (note this is the same as the `dn` +property). + +### authentication + +The method of authentication. Right now only `simple` is supported. + +### credentials + +The credentials to go with the `name/authentication` pair. For `simple`, this +will be the plain-text password. + +## BindResponse + +No extra methods above an `LDAPResult` API call. + +# Add + +Adds a mount in the tree to perform LDAP adds with. Example: + + 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(); + }); + +## AddRequest + +AddRequest objects have the following properties: + +### entry + +The DN the client is attempting to add (note this is the same as the `dn` +property). + +### attributes + +The set of attributes in this entry. Note that this will be an array of +`Attribute` objects (which have a type and an array of values). This directly +maps to how the request came in off the wire. It's likely you'll want to use +`toObject()` and iterate that way, since that will transform an AddRequest into +a standard JavaScript object. + +### toObject() + +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'] + } + } + +## AddResponse + +No extra methods above an `LDAPResult` API call. + +# Search + +Adds a handler for the LDAP search operation. Example: + + 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(); + }); + + + this.derefAliases = options.derefAliases || Protocol.NEVER_DEREF_ALIASES; + this.sizeLimit = options.sizeLimit || 0; + this.timeLimit = options.timeLimit || 00; + this.typesOnly = options.typesOnly || false; + this.filter = options.filter || null; + this.attributes = options.attributes ? options.attributes.slice(0) : []; + +## SearchRequest + +SearchRequest objects have the following properties: + +### baseObject + +The DN the client is attempting to start the search at (equivalent to `dn`). + +### scope + +(string) one of: + +* base +* one +* sub + +### derefAliases + +Will be an integer (defined in the LDAP protocol). Defaults to '0' (meaning +never deref). + +### sizeLimit + +The number of entries to return. Defaults to '0' (unlimited). ldapjs doesn't +currently automatically enforce this, but probably will at some point. + +### timeLimit + +Maximum amount of time the server should take in sending search entries. +Defaults to '0' (unlimited). + +### typesOnly + +Whether to return only the names of attributes, and not the values. Defaults to +false. Note that ldapjs will take care of this for you. + +### filter + +The [filter](/filters.html) object that the client requested. Notably this has +a `matches()` api on it that you can leverage. For an example of introspecting +a filter, take a look at the ldapjs-riak source. + +### attributes + +An optional list of attributes to restrict the returned result sets to. ldapjs +will automatically handle this for you. + +## SearchResponse + +### send(entry) + +Allows you to send a `SearchEntry` object. Note that you do not need to +explicitly pass in a `SearchEntry` object, and can instead just send a plain +JavaScript object that matches the format used from `AddRequest.toObject()`. +Example: + + server.search('o=example', function(req, res, next) { + var obj = { + dn: 'o=example', + attributes: { + objectclass: ['top', 'organization'], + o: ['example'] + } + }; + + if (req.filter.matches(obj)) + res.send(obj) + + res.end(); + }); + +# modify + +Allows you to handle an LDAP modify operation. Example: + + 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(); + }); + +## ModifyRequest + +ModifyRequest objects have the following properties: + +### object + +The DN the client is attempting to update (note this is the same as the `dn` +property). + +### changes + +An array of `Change` objects the client is attempting to perform. See below for +details on the `Change` object. + +## Change + +The Change object will have the following properties: + +### operation + +A string, and will be one of: 'add', 'delete', 'replace'. + +### modification + +Will be an `Attribute` object, which will have a 'type' (string) field, and +'vals', which will be an array of string values. + +## ModifyResponse + +No extra methods above an `LDAPResult` API call. + +# del + +Allows you to handle an LDAP delete operation. Example: + + server.delete('o=example', function(req, res, next) { + console.log('DN: ' + req.dn.toString()); + res.end(); + }); + +## DeleteRequest + +### entry + +The DN the client is attempting to delete (note this is the same as the `dn` +property). + +## DeleteResponse + +No extra methods above an `LDAPResult` API call. + +# compare + +Allows you to handle an LDAP compare operation. Example: + + 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'); + }); + +## CompareRequest + +### entry + +The DN the client is attempting to compare (note this is the same as the `dn` +property). + +### attribute + +Will be the string name of the attribute to compare values of. + +### value + +The string value of the attribute to compare. + +## CompareResponse + +The `end()` method for compare takes a boolean, as opposed to a numeric code +(note you can still pass in a numeric LDAP status code if you want). Beyond +that, there are no extra methods above an `LDAPResult` API call. + +# modifyDN + +Allows you to handle an LDAP modifyDN operation. Example: + + 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() : '')); + + res.end(); + }); + +## ModifyDNRequest + +### entry + +The DN the client is attempting to rename (note this is the same as the `dn` +property). + +### newRdn + +The leaf RDN the client wants to rename this entry to. This will be a DN object. + +### deleteOldRdn + +boolean. Whether or not to delete the old RDN (i.e., rename vs copy). Defaults +to true. + +### newSuperior + +Optional (DN). If the modifyDN operation wishes to relocate the entry in the +tree, the newSuperior field will contain the new parent. + +## ModifyDNResponse + +No extra methods above an `LDAPResult` API call. + +# exop + +Allows you to handle an LDAP extended operation. Extended operations are pretty +much arbitrary extensions, by definition. Typically the extended 'name' is an +OID, but ldapjs makes no such restrictions; it just needs to be a string. Note +that 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. +Example: + + // 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(); + }); + +## ExtendedRequest + +### name + +String. Will always be a match to the route-defined name. But the client +includes this in their requests. + +### value + +Optional string. The arbitrary blob the client sends for this extended +operation. + +## ExtendedResponse + +### name + +The name of the extended operation. ldapjs will automatically set this. + +### value + +The arbitrary (string) value to send back as part of the response. + +# unbind + +ldapjs by default provides an unbind handler that just disconnects the client +and cleans up any internals (in ldapjs core). You can override this handler +if you need to clean up any items in your backend, for example. + + server.unbind(function(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 +ultimately calls `net.Socket.end()` for you. There are no properties available +on either the request or response objects, except of course for `end()` on the +response. diff --git a/lib/messages/moddn_request.js b/lib/messages/moddn_request.js index 3dab4f6..e87681c 100644 --- a/lib/messages/moddn_request.js +++ b/lib/messages/moddn_request.js @@ -44,7 +44,7 @@ function ModifyDNRequest(options) { this.entry = options.entry || null; this.newRdn = options.newRdn || null; - this.deleteOldRdn = options.deleteOldRdn || false; + this.deleteOldRdn = options.deleteOldRdn || true; this.newSuperior = options.newSuperior || null; var self = this; diff --git a/lib/messages/search_request.js b/lib/messages/search_request.js index 3300378..5696bf1 100644 --- a/lib/messages/search_request.js +++ b/lib/messages/search_request.js @@ -71,7 +71,7 @@ function SearchRequest(options) { this.scope = options.scope || 'base'; this.derefAliases = options.derefAliases || Protocol.NEVER_DEREF_ALIASES; this.sizeLimit = options.sizeLimit || 0; - this.timeLimit = options.timeLimit || 00; + this.timeLimit = options.timeLimit || 0; this.typesOnly = options.typesOnly || false; this.filter = options.filter || null; this.attributes = options.attributes ? options.attributes.slice(0) : []; diff --git a/lib/messages/unbind_request.js b/lib/messages/unbind_request.js index cc449bf..98a9d18 100644 --- a/lib/messages/unbind_request.js +++ b/lib/messages/unbind_request.js @@ -8,6 +8,7 @@ var asn1 = require('asn1'); var LDAPMessage = require('./message'); var LDAPResult = require('./result'); +var DN = require('../dn').DN; var Protocol = require('../protocol'); @@ -32,7 +33,7 @@ function UnbindRequest(options) { LDAPMessage.call(this, options); this.__defineGetter__('type', function() { return 'UnbindRequest'; }); - this.__defineGetter__('_dn', function() { return ''; }); + this.__defineGetter__('_dn', function() { return new DN([{}]); }); } util.inherits(UnbindRequest, LDAPMessage); module.exports = UnbindRequest; diff --git a/lib/server.js b/lib/server.js index f9674c8..346cda1 100644 --- a/lib/server.js +++ b/lib/server.js @@ -237,7 +237,7 @@ function Server(options) { }); c.ldap.__defineGetter__('bindDN', function() { - return c.ldap._bindDN || 'cn=anonymous'; + return c.ldap._bindDN || new DN([{cn: 'anonymous'}]); }); c.ldap.__defineSetter__('bindDN', function(val) { if (!(val instanceof DN))