├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── docs ├── backend.md ├── branding │ ├── footer.html.in │ ├── header.html.in │ └── media │ │ ├── css │ │ └── style.css │ │ └── js │ │ └── jquery-1.4.2.min.js └── index.md ├── lib ├── add.js ├── bind.js ├── cache.js ├── common.js ├── compare.js ├── del.js ├── index.js ├── modify.js ├── persistent_search.js ├── riak.js └── search.js ├── package.json └── tst ├── add.test.js ├── bind.test.js ├── changelog.test.js ├── compare.test.js ├── del.test.js ├── modify.test.js ├── persistent_search.test.js ├── riak.test.js └── search.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | *.ldif 4 | *.tar* 5 | docs/pkg 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Mark Cavage, All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=ldapjs-riak 2 | 3 | ifeq ($(VERSION), "") 4 | @echo "Use gmake" 5 | endif 6 | 7 | 8 | SRC := $(shell pwd) 9 | TAR = tar 10 | UNAME := $(shell uname) 11 | ifeq ($(UNAME), SunOS) 12 | TAR = gtar 13 | endif 14 | 15 | HAVE_GJSLINT := $(shell which gjslint >/dev/null && echo yes || echo no) 16 | NPM := npm_config_tar=$(TAR) npm 17 | 18 | RESTDOWN_VERSION=1.2.13 19 | DOCPKGDIR = ./docs/pkg 20 | 21 | RESTDOWN = ./node_modules/.restdown/bin/restdown \ 22 | -b ./docs/branding \ 23 | -m ${DOCPKGDIR} \ 24 | -D mediaroot=media 25 | 26 | .PHONY: dep lint test doc clean all install 27 | 28 | all:: test doc 29 | 30 | node_modules/.npm.installed: 31 | $(NPM) install 32 | if [[ ! -d node_modules/.restdown ]]; then \ 33 | git clone git://github.com/trentm/restdown.git node_modules/.restdown; \ 34 | else \ 35 | (cd node_modules/.restdown && git fetch origin); \ 36 | fi 37 | @(cd ./node_modules/.restdown && git checkout $(RESTDOWN_VERSION)) 38 | @touch ./node_modules/.npm.installed 39 | 40 | dep: ./node_modules/.npm.installed 41 | install: dep 42 | 43 | gjslint: 44 | gjslint --nojsdoc -r lib -r tst 45 | 46 | ifeq ($(HAVE_GJSLINT), yes) 47 | lint: gjslint 48 | else 49 | lint: 50 | @echo "* * *" 51 | @echo "* Warning: Cannot lint with gjslint. Install it from:" 52 | @echo "* http://code.google.com/closure/utilities/docs/linter_howto.html" 53 | @echo "* * *" 54 | endif 55 | 56 | doc: dep 57 | @rm -rf ${DOCPKGDIR} 58 | @mkdir -p ${DOCPKGDIR} 59 | ${RESTDOWN} ./docs/index.md 60 | ${RESTDOWN} ./docs/backend.md 61 | rm docs/*.json 62 | mv docs/*.html ${DOCPKGDIR} 63 | (cd ${DOCPKGDIR} && $(TAR) -czf ${SRC}/${NAME}-docs-`git log -1 --pretty='format:%h'`.tar.gz *) 64 | 65 | 66 | test: dep lint 67 | $(NPM) test 68 | 69 | clean: 70 | @rm -fr ${DOCPKGDIR} node_modules *.log *.tar.gz 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A fully backend for [ldapjs](http://ldapjs.org) built over [Riak](http://wiki.basho.com). 2 | 3 | ## Usage 4 | 5 | var ldap = require('ldapjs'); 6 | var ldapRiak = require('ldapjs-riak'); 7 | 8 | var SUFFIX = 'o=example'; 9 | 10 | var server = ldap.createServer(); 11 | var backend = ldapRiak.createBackend({ 12 | "host": "localhost", 13 | "port": 8098, 14 | "bucket": "o_example", 15 | "indexes": ["l", "cn"], 16 | "uniqueIndexes": ["uid"], 17 | "numConnections": 5 18 | }); 19 | 20 | server.add(SUFFIX, backend, backend.add()); 21 | server.modify(SUFFIX, backend, backend.modify()); 22 | server.bind(SUFFIX, backend, backend.bind()); 23 | server.compare(SUFFIX, backend, backend.compare()); 24 | server.del(SUFFIX, backend, backend.del()); 25 | server.search(SUFFIX, backend, backend.search(searchSalt)); 26 | 27 | server.listen(config.port, config.host, function() { 28 | console.log('ldap-riak listening at: %s', server.url); 29 | }); 30 | 31 | More docs to follow... 32 | 33 | ## Installation 34 | 35 | npm install ldapjs-riak 36 | 37 | ## License 38 | 39 | MIT. 40 | 41 | ## Bugs 42 | 43 | See . 44 | -------------------------------------------------------------------------------- /docs/backend.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Backend | ldapjs-riak 3 | markdown2extras: wiki-tables 4 | logo-color: green 5 | logo-font-family: google:Aldrich, Verdana, sans-serif 6 | header-font-family: google:Aldrich, Verdana, sans-serif 7 | --- 8 | 9 | # Backend Configuration and Tuning 10 | 11 | This document describes how to create an use a ldapjs-riak backend, 12 | how it works, tuning, and setting up Riak. 13 | 14 | # Riak Backend Overview 15 | 16 | The ldapjs-riak package stores all data in Riak, and uses Riak's 2i 17 | feature (present in the 1.0+ version of Riak) to support fast querying 18 | at search time. There is no additional dependency on other 19 | database/caching components (like Redis). However, the backend is 20 | designed around (relatively) infrequent writes, with frequent reads, 21 | and specifically reads where you know you're going to be searching 22 | against an indexed attribute. Non-indexed queries are basically going 23 | to be really bad at small scale, and not work at all at large scale. 24 | 25 | The backend supports "normal" indexing, which means that you can have 26 | multiple entries in the directory with the same attribute/value 27 | pairs. In addition unique indexing is supported, but to do so, the 28 | backend maintains a separate bucket in Riak to keep track of seen 29 | attribute/value pairs (i.e., unique indexes are maintained 30 | "manually"). Note this means that in failure modes it is possible to 31 | write an entry while failing to write unique index records. This is 32 | why it's important to tune retry/backoff setting appropriately. 33 | 34 | Also, the backend can optionally be configure to write LDAP changelog 35 | records on all updates. The changelog records are _almost_ compliant 36 | with the [http://tools.ietf.org/html/draft-good-ldap-changelog-04](LDAP 37 | Changelog RFC Draft), but differ in that (1) changes are written as 38 | JSON, not LDIF, and (2) DNs are up to you to sequence/define. 39 | ldapjs-riak changelog records are written to yet another bucket, and 40 | notably are written *after* responding to the client, so it is 41 | possible for the client to see `LDAP_SUCCESS` but the changelog 42 | recording action to fail. 43 | 44 | It's pretty straight-forward to think about how this would work, but 45 | here's a quick break down of the work done by each operation: 46 | 47 | - *add(dn, entry):* 48 | 1. Check if `dn` exists 49 | 2. Check if the parent of `dn` exists 50 | 3. Add _operational_ attributes (like ctime/mtime/etc.). 51 | 4. Generate list of unique indexes, and ensure they are indeed 52 | unique 53 | 5. Save the entry 54 | 6. Save the unique indexes 55 | 7. (optional) Write a changelog record 56 | - *bind(dn, credentials):* 57 | 1. Lookup entry 58 | 2. Check credentials 59 | - *compare(dn, attr, val):* 60 | 1. Lookup entry 61 | 2. Compare attribute/value 62 | - *delete(dn):* 63 | 1. Load entry 64 | 2. Check if children exist 65 | 3. Delete the main record 66 | 4. Delete any unique indexes 67 | 5. (optional) Write a changelog record. 68 | - *modifyDN(dn, newDN):* 69 | 1. Load entry 70 | 2. Check if children exist 71 | 3. Check if new parent exists 72 | 4. Delete existing record 73 | 5. Delete unique indexes 74 | 6. Save new record 75 | 7. Resave unique indexes 76 | 8. (optional) Write a changelog record 77 | - *modify(dn, changes):* 78 | 1. Load entry 79 | 2. Make changes 80 | 3. Check uniqueness of changes 81 | 4. Delete old unique indexes 82 | 5. Save entry 83 | 6. Save new unique indexes 84 | 7. (optional) Write a changelog record 85 | - *search(baseDN, scope, filter):* 86 | 1. If scope=base, just resolve as a Riak GET 87 | 2. Otherwise, introspect the filter, and try to use an indexed 88 | attribute 89 | 3. As keys come in, load records, and check against the search 90 | filter to send back 91 | 92 | Note that the search operation will _not_ return results sorted by DN; 93 | results are streamed back as we get them from Riak. This is different 94 | than most every other LDAP server out there, but is fine for most 95 | cases, as you get data faster. Sort client-side if you need to do so. 96 | 97 | # Setup and Creation 98 | 99 | ## Configure Riak to use leveldb 100 | 101 | Obviously, to leverage Riak, you need to install Riak. Grab a 1.0.x 102 | release from [Basho](http://basho.com), and follow their setup 103 | instructions. Post-install, you'll need to edit Riak's `app.config` 104 | `storage_backend` setting to: 105 | 106 | {storage_backend, riak_kv_eleveldb_backend}, 107 | 108 | The default will have been `bitcask`. ldapjs-riak basically doesn't 109 | work, at all, without Riak's 2i feature, so this is required. 110 | 111 | Other than that, do whatever you would do with Riak to setup a 112 | cluster, tune memory setttings, add a load balancer, etc. It's out of 113 | scope for this document to tell you how to deploy Riak to production... 114 | 115 | ## Determine how to configure the backend 116 | 117 | The Riak backend has the following configurations: 118 | 119 | * Cluster information 120 | * CAP tuning 121 | * Indexes/Unique Indexes 122 | * Changelog 123 | 124 | ### Riak Cluster 125 | 126 | You configure the backend to point at a single IP/port combination, so 127 | really you should setup a load balancer in front of your Riak cluster, 128 | or do IP-takeovers, or something. But you also configure retry/backoff 129 | settings, which uses [node-retry](https://github.com/tim-kos/node-retry); note 130 | that these retry settings kick in on *every* request to Riak, so you 131 | probably want to keep this bounded, as a single add for example 132 | will hit Riak at minimum for the save, plus once for each unique 133 | index. Modify/Delete/ModifyDN are worse. 134 | 135 | 136 | "client": { 137 | "url": "http://localhost:8098", 138 | "clientId": "my-laptop", 139 | "retry": { 140 | "retries": 3, 141 | "factor": 2, 142 | "minTimeout": 1000, 143 | "maxTimeout": 10000 144 | } 145 | } 146 | 147 | And `clientId` is the Riak identifier for this client. Just make 148 | something up. 149 | 150 | ### CAP Tuning 151 | 152 | As Riak nicely allows you to tune the replication/consistency/availability 153 | settings for each bucket, this backend allows you to tune the CAP 154 | settings for all three buckets (data, unique indexing, and 155 | changelog). 156 | 157 | The recommended tuning is to use the default "quorum" on the data 158 | bucket, use strong consistency on the unique index bucket (this means 159 | that in the event of a partition you won't be able to take writes), 160 | and do whatever you want on changelog (probably quorum makes sense). 161 | 162 | # Create a Backend 163 | 164 | If you're not familiar wth [ldapjs](http://ldapjs.org), get familiar, 165 | as the rest of this won't make any sense otherwise. ldapjs includes 166 | the ability to keep a "backend" object that is stateful, and this 167 | module leverages that functionality. The bare minimum you need to get 168 | going is the following: 169 | 170 | var ldapRiak = require('ldapjs-riak'); 171 | var backend = ldapRiak.createBackend({ 172 | "bucket": { 173 | "name": "ldapjs_riak", 174 | }, 175 | "uniqueIndexBucket": { 176 | "name": ldapjs_riak_uindex", 177 | }, 178 | "client": { 179 | "url": "http://localhost:8098", 180 | "clientId": "ldapjs_riak" 181 | } 182 | }); 183 | 184 | Which will create a backend, and point it at the specified Riak 185 | host/port/buckets, with no indexes. Once you have that, you can mount 186 | the backend "as normal" in ldapjs: 187 | 188 | var ldap = require('ldapjs'); 189 | 190 | var SUFFIX = 'dc=example, dc=com'; 191 | 192 | var server = ldap.createServer({}); 193 | 194 | server.add(SUFFIX, backend, backend.add()); 195 | server.modify(SUFFIX, backend, backend.modify()); 196 | server.bind(SUFFIX, backend, backend.bind()); 197 | server.compare(SUFFIX, backend, backend.compare()); 198 | server.del(SUFFIX, backend, backend.del()); 199 | server.modifyDN(SUFFIX, backend, backend.modifyDN()); 200 | server.search(SUFFIX, backend, backend.search()); 201 | 202 | While that's kind of annoyingly verbose, each of the operations takes 203 | the ability to inject handlers that run after backend 204 | intiialization has been run, but before "real work" gets kicked 205 | off. So for example: 206 | 207 | server.compare(SUFFIX, backend, function(req, res, next) { 208 | return next(); 209 | }, backend.compare(function(req, res, next) { 210 | req.riak.log('hello world'); 211 | })); 212 | 213 | While that does nothing interesting, it does show that you can still 214 | use "normal" handlers with ldapjs, as well as special "ldapjs-riak" handlers. 215 | 216 | ## createBackend(options) 217 | 218 | The full list of options (options is a plain JS object) to `createBackend` is: 219 | 220 | ||bucket||Object||required||A configuration of the Riak bucket name and CAP tunings for entries. || 221 | ||log4js||Log4JS Instance||required||`require('log4js')` or other configured instance.|| 222 | ||client||Object||required||Connection information for the actual Riak cluster.|| 223 | ||uniqueIndexBucket||Object||optional||A configuration of the Riak bucket name and CAP tunings for unique indexes.|| 224 | ||changelogBucket||Object||optional||A configuration of the Riak bucket name and CAP tunings for changelogging.|| 225 | ||indexes||Object||optional||A listing of attributes to index in an entry, and whether or not uniquness should be enforced.|| 226 | 227 | -------------------------------------------------------------------------------- /docs/branding/footer.html.in: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/branding/header.html.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %(title)s 5 | 6 | 7 | %(doc_style)s 8 | 9 | 20 | 21 | 22 |
23 |
24 |
25 | 51 | -------------------------------------------------------------------------------- /docs/branding/media/css/style.css: -------------------------------------------------------------------------------- 1 | /* Trent Mick's blog's CSS. Based heavily (at least initially) on 2 | * and on . 3 | */ 4 | 5 | /* ---- reset 6 | * html5doctor.com Reset Stylesheet (Eric Meyer's Reset Reloaded + HTML5 baseline) 7 | * v1.4 2009-07-27 | Authors: Eric Meyer & Richard Clark 8 | * html5doctor.com/html-5-reset-stylesheet/ 9 | */ 10 | html, body, div, span, object, iframe, 11 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 12 | abbr, address, cite, code, 13 | del, dfn, em, img, ins, kbd, q, samp, 14 | small, strong, sub, sup, var, 15 | b, i, 16 | dl, dt, dd, ol, ul, li, 17 | fieldset, form, label, legend, 18 | table, caption, tbody, tfoot, thead, tr, th, td, 19 | article, aside, figure, footer, header, 20 | hgroup, menu, nav, section, 21 | time, mark, audio, video { 22 | margin:0; 23 | padding:0; 24 | border:0; 25 | outline:0; 26 | font-size:100%; 27 | vertical-align:baseline; 28 | background:transparent; 29 | } 30 | 31 | article, aside, figure, footer, header, 32 | hgroup, nav, section { display:block; } 33 | 34 | nav ul { list-style:none; } 35 | 36 | blockquote, q { quotes:none; } 37 | 38 | blockquote:before, blockquote:after, 39 | q:before, q:after { content:''; content:none; } 40 | 41 | a { margin:0; padding:0; font-size:100%; vertical-align:baseline; background:transparent; } 42 | ins { background-color:#ff9; color:#000; text-decoration:none; } 43 | mark { background-color:#ff9; color:#000; font-style:italic; font-weight:bold; } 44 | del { text-decoration: line-through; } 45 | abbr[title], dfn[title] { border-bottom:1px dotted #000; cursor:help; } 46 | sub { vertical-align: sub; } 47 | sup { vertical-align: super; } 48 | 49 | /* tables still need cellspacing="0" in the markup */ 50 | table { border-collapse:collapse; border-spacing:0; } 51 | 52 | input, select { vertical-align:middle; } 53 | 54 | 55 | 56 | /* ---- 24px vertical rhythm 57 | * 58 | * All tags except 'pre' because 24px line-height is way too much, want 18px. 59 | * Tough. 60 | */ 61 | body { 62 | line-height: 24px; 63 | font-family: helvetica, arial, freesans, clean, sans-serif; 64 | font-size: 16px; 65 | } 66 | h1,h2,h3,h4,h5,h6 { 67 | font-family: Georgia,serif; 68 | font-weight: bold; 69 | /* http://www.aestheticallyloyal.com/public/optimize-legibility/ */ 70 | text-rendering: optimizeLegibility; 71 | } 72 | h1 { 73 | font-size: 28px; 74 | line-height: 48px; 75 | padding: 24px 0 24px 0; 76 | } 77 | h2 { 78 | font-size: 21px; 79 | line-height: 24px; 80 | padding: 24px 0 24px 0; 81 | } 82 | h3 { 83 | font-size: 16px; 84 | line-height: 24px; 85 | padding: 24px 0 0 0; 86 | } 87 | h4 { 88 | font-size: 14px; 89 | line-height: 24px; 90 | padding: 24px 0 0 0; 91 | } 92 | h5 { 93 | color: #666; 94 | font-size: 14px; 95 | line-height: 24px; 96 | padding: 24px 0 0 0; 97 | } 98 | h6 { 99 | color: #666; 100 | font-size: 12px; 101 | line-height: 24px; 102 | padding: 0; 103 | } 104 | hr { 105 | padding-bottom: 24px; 106 | margin: 24px 0; 107 | height: 0; 108 | border: none; 109 | text-align: center; 110 | color: #333; 111 | } 112 | hr:after { 113 | content: "\2767"; 114 | } 115 | p + p { 116 | padding-top: 24px; 117 | } 118 | dl { 119 | margin: 24px 0; 120 | } 121 | dt { 122 | font-weight: bold; 123 | margin-top: 24px; 124 | } 125 | dd { 126 | margin: 0 0 0 48px; 127 | } 128 | 129 | 130 | 131 | /* ---- layout */ 132 | 133 | body { 134 | margin: 12px; 135 | } 136 | #wrapper { 137 | margin: 0 auto; 138 | max-width: 700px; 139 | position: relative; 140 | } 141 | #main { 142 | min-height: 300px; 143 | } 144 | #fadeout { 145 | z-index: 50; 146 | position: fixed; 147 | left: 0; 148 | top: 0; 149 | right: 0; 150 | height: 210px; 151 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 255)), to(rgba(255, 255, 255, 0))); 152 | background: -moz-linear-gradient(top, rgba(255, 255, 255, 255), rgba(255, 255, 255, 0)); 153 | } 154 | #header { 155 | position: fixed; 156 | z-index: 100; 157 | padding: 48px 0 24px 0; 158 | top: 0; 159 | left: auto; 160 | right: auto; 161 | width: 100%; 162 | background: white; 163 | } 164 | #content { 165 | position: relative; 166 | top: 150px; 167 | overflow: auto; 168 | padding-bottom: 200px; 169 | } 170 | #footer { 171 | position: relative; 172 | padding-top: 60px; 173 | } 174 | 175 | /* ---- base styles */ 176 | 177 | blockquote { 178 | font-family: Georgia,serif; 179 | font-size: 18px; 180 | color: #555; 181 | margin: 24px 15% 24px 15%; 182 | margin: 24px 10% 24px 10%; 183 | } 184 | blockquote cite:before { 185 | content: "\2014 "; 186 | } 187 | blockquote p.hangquotes, blockquote.hangquotes { 188 | text-indent: -0.5em; 189 | } 190 | 191 | code, 192 | pre { 193 | font-family: Consolas, Monaco, "Lucida Console", "Courier New", monospace; 194 | font-size: 12px; 195 | background-color: #f5f5f5; 196 | border: 1px solid #ccc; 197 | } 198 | code { 199 | padding: 0 0.2em; 200 | } 201 | pre { 202 | font-size: 80%; 203 | line-height: 18px; 204 | padding: 5px; 205 | margin: 18px 0; 206 | 207 | /* "overflow:auto" does NOT yield scrollbars in mobile browsers. iPhone 208 | * *does* support two-finger scrolling inside the pre-block, but that 209 | * is undiscoverable. Let's pre-wrap. 210 | */ 211 | white-space: pre; 212 | white-space: -moz-pre-wrap; 213 | white-space: -hp-pre-wrap; 214 | white-space: -o-pre-wrap; 215 | white-space: pre-wrap; 216 | word-wrap: break-word; 217 | } 218 | pre code { 219 | border: medium none; 220 | padding: 0; 221 | } 222 | a code { 223 | text-decoration: underline; 224 | } 225 | h1 + pre, 226 | h2 + pre, 227 | h3 + pre, 228 | h4 + pre, 229 | h5 + pre, 230 | h6 + pre { 231 | margin-top: 0; 232 | } 233 | 234 | table { 235 | margin: 24px 0; 236 | } 237 | th, 238 | td { 239 | border: solid #aaa; 240 | border-width: 1px 0; 241 | line-height: 23px; 242 | padding: 0 12px; 243 | text-align: left; 244 | } 245 | th { 246 | border-collapse: separate; 247 | } 248 | tbody tr:nth-child(odd) { 249 | background-color: #f6f6f6; 250 | } 251 | 252 | ol, ul { 253 | padding: 12px 0; 254 | margin: 0 0 0 24px; 255 | } 256 | ol ol, ul ul, ul ol, ol ul { 257 | margin-left: 24px; 258 | } 259 | ol { list-style-type: decimal; } 260 | ol ol { list-style-type: lower-alpha; } 261 | ol ol ol { list-style-type: upper-roman; } 262 | ol ol ol ol { list-style-type: lower-roman; } 263 | ul { list-style-type: circle; } 264 | ul ul { list-style-type: disc; } 265 | ul ul ul { list-style-type: circle; } 266 | 267 | :link { color: hsl(206, 100%, 23%); } 268 | :visited { color: hsl(240, 20%, 50%); } 269 | :link:hover, :visited:hover { color: hsl(206, 100%, 38%); } 270 | 271 | 272 | /* ---- larger screens */ 273 | 274 | @media only screen and (min-width: 481px) { 275 | pre { 276 | overflow: auto; 277 | white-space: pre; 278 | word-wrap: normal; 279 | } 280 | } 281 | 282 | @media only screen and (min-width: 700px) { 283 | ol, ul { 284 | margin: 0; 285 | } 286 | } 287 | 288 | 289 | /* ---- custom classes */ 290 | 291 | pre.shell, 292 | pre.shell code { 293 | background:#444; 294 | color:#fff; 295 | border-width:0px; 296 | } 297 | pre.shell code::before { 298 | content: '$ '; 299 | display: inline; 300 | } 301 | 302 | .copy { 303 | font-size: 90%; 304 | text-align: center; 305 | color: #777; 306 | } 307 | 308 | /* ----- top header */ 309 | a#homelink:link { 310 | color: #008000; 311 | text-decoration: none; 312 | } 313 | a#homelink:visited { 314 | color: #008000; 315 | text-decoration: none; 316 | } 317 | a#homelink:active { 318 | color: #008000; 319 | text-decoration: none; 320 | } 321 | a#homelink:hover { 322 | color: #008000; 323 | text-decoration: none; 324 | } 325 | #logo { 326 | font-family: Georgia,serif; 327 | font-weight: bold; 328 | font-size: 60px; 329 | top: 55px; 330 | } 331 | #apibox, 332 | #tocbox { 333 | position: relative; 334 | } 335 | #tocbox { 336 | margin-left: 24px; 337 | } 338 | .navbutton { 339 | border: 1px solid #ccc; 340 | position: relative; 341 | display: inline-block; 342 | width: 120px; 343 | text-align: center; 344 | top: -7px; 345 | font-size: 70%; 346 | font-weight: bold; 347 | text-transform: uppercase; 348 | text-decoration: none; 349 | color: black; 350 | } 351 | .navbutton:hover { 352 | cursor: pointer; 353 | background-color: #f3f3f3; 354 | } 355 | .popup { 356 | border: 1px solid #ccc; 357 | padding: 5px 10px; 358 | background-color: white; 359 | font-size: 80%; 360 | line-height: 1.6em; 361 | position: absolute; 362 | top: 16px; 363 | left: 0px; 364 | width: 200px; 365 | } 366 | .popup a { 367 | text-decoration: none; 368 | } 369 | .popup a:hover { 370 | text-decoration: underline; 371 | } 372 | .popup ul { 373 | margin: 0; 374 | padding: 0px; 375 | list-style-type: none; 376 | } 377 | .popup ul ul { 378 | margin-left: 24px; 379 | } 380 | #githubfork { 381 | position: fixed; 382 | top: 0; 383 | right: 0px; 384 | } 385 | #indextagline { 386 | font-size: 24px; 387 | font-style: italic; 388 | text-align: center; 389 | color: green; 390 | } 391 | a#indextaglink:link { 392 | color: #008000; 393 | text-decoration: none; 394 | } 395 | a#indextaglink:visited { 396 | color: #008000; 397 | text-decoration: none; 398 | } 399 | a#indextaglink:active { 400 | color: #008000; 401 | text-decoration: none; 402 | } 403 | a#indextaglink:hover { 404 | color: #008000; 405 | text-decoration: none; 406 | } 407 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ldapjs-riak 3 | markdown2extras: wiki-tables 4 | logo-color: green 5 | logo-font-family: google:Aldrich, Verdana, sans-serif 6 | header-font-family: google:Aldrich, Verdana, sans-serif 7 | --- 8 | 9 |
10 | High-Availability LDAP using ldapjs and Riak 14 |
15 | 16 | # Overview 17 | 18 | ldapjs-riak is a full LDAP backend implementation for 19 | [ldapjs](http://ldapjs.org) and [Riak](http://basho.com). 20 | Using ldapjs-riak, you can easily stand up a highly available LDAP 21 | cluster. To get bootstrapped, the following code will give you a 22 | v3-compliant LDAP server that allows a user to make changes to their 23 | own entry, and any child entries (as an example): 24 | 25 | var ldap = require('ldapjs'); 26 | var ldapRiak = require('ldapjs-riak'); 27 | var log4js = require('log4js'); 28 | 29 | var SUFFIX = 'o=example'; 30 | 31 | function authorize(req, res, next) { 32 | var bindDN = req.connection.ldap.bindDN; 33 | 34 | if (req.type === 'BindRequest' || 35 | bindDN.parentOf(req.dn) || 36 | bindDN.equals(req.dn)) 37 | return next(); 38 | 39 | return next(new ldap.InsufficientAccessRightsError()); 40 | } 41 | 42 | 43 | var server = ldap.createServer(); 44 | var backend = ldapRiak.createBackend({ 45 | "log4js": log4js, 46 | "bucket": { 47 | "name": "ldapjs_riak", 48 | }, 49 | "uniqueIndexBucket": { 50 | "name": ldapjs_ldapjs_riak", 51 | }, 52 | "indexes": { 53 | "email": true, 54 | "uuid": true, 55 | "cn": false, 56 | "sn": false 57 | }, 58 | "client": { 59 | "url": "http://localhost:8098", 60 | "clientId": "ldapjs_riak_1", 61 | "retry": { 62 | "retries": 3, 63 | "factor": 2, 64 | "minTimeout": 1000, 65 | "maxTimeout": 10000 66 | } 67 | } 68 | }); 69 | 70 | server.bind('cn=root', function(req, res, next) { 71 | if (req.version !== 3) 72 | return next(new ldap.ProtocolError(req.version + ' is not v3')); 73 | 74 | if (req.credentials !== 'secret') 75 | return next(new ldap.InvalidCredentialsError(req.dn.toString())); 76 | 77 | res.end(); 78 | return next(); 79 | }); 80 | 81 | server.add(SUFFIX, backend, authorize, backend.add()); 82 | server.modify(SUFFIX, backend, authorize, backend.modify()); 83 | server.bind(SUFFIX, backend, authorize, backend.bind()); 84 | server.compare(SUFFIX, backend, authorize, backend.compare()); 85 | server.del(SUFFIX, backend, authorize, backend.del()); 86 | server.modifyDN(SUFFIX, backend, authorize, backend.modifyDN()); 87 | server.search(SUFFIX, backend, authorize, backend.search()); 88 | 89 | server.listen(1389, function() { 90 | console.log('ldap-riak listening at: %s', server.url); 91 | }); 92 | 93 | Note that ldapjs-riak requires Riak 1.0 with the `eleveldb_backend`, 94 | as it makes heavy use of Riak's secondary indexing feature. Once you 95 | have a Riak instance running, and are running that code, try: 96 | 97 | $ ldapadd -x -D cn=root -w secret -H ldap://localhost:1389 -f data.ldif 98 | 99 | Where data.ldif has: 100 | 101 | dn: o=example 102 | o: example 103 | objectclass: organization 104 | 105 | dn: email=nobody@acme.com, o=example 106 | cn: Sample 107 | sn: User 108 | email: nobody@acme.com 109 | userpassword: secret2 110 | objectclass: person 111 | 112 | Now you can try searching as the child user: 113 | 114 | $ ldapsearch -x -D email=nobody@acme.com,o=example -w secret2 -H ldap://localhost:1389 -b email=nobody@acme.com,o=example email=* 115 | 116 | All of the standard LDAP operations: 117 | 118 | - Add 119 | - Bind 120 | - Compare 121 | - Delete 122 | - Modify 123 | - ModifyDN 124 | - Search 125 | 126 | are supported by the Riak backend. In addition, it supports an 127 | "almost" compliant changelog implementation (there are a few 128 | differences, like instead of storing changes in LDIF, they are stored 129 | in JSON). 130 | 131 | # Why shouldn't you use ldapjs-riak? 132 | 133 | Because it (and Riak itself) are not a perfect fit for all use cases. 134 | That's true of any technology. Specifically, what this is aimed at is 135 | a "big data" use case, where the workload breakdown is skewed to be 136 | very read-heavy, and you can plan the queries you'll need (mostly) in 137 | advance. Because ldapjs-riak leverages Riak's 2i feature heavily, if 138 | you're planning to do a lot of ad-hoc type information finding (i.e., 139 | on non-indexed data), you're going to be off the reservation. 140 | 141 | # What else do I need to do? 142 | 143 | The sample code above will actually get you a fully-functional LDAP 144 | server. That said, you probably want to look at writing some of your 145 | own code for: 146 | 147 | - *Admin Users/Authentication:* You probably want a 'root' user 148 | configured that's not stored in Riak, so you can authenticate when 149 | Riak is unavailable. 150 | - *Authorization:* In the example above I just assert that a DN can do 151 | anything to entries below it, or at it. It would not, for example 152 | stop a modifyDN from happening, and putting itself somewhere else in 153 | the tree. You probably want something richer here. 154 | - *Auditing:* I just write out some "w3c style" logs that I can post-process. 155 | - *Schema:* While I sort of loathe standard LDAP schema, it does serve 156 | a purpose. I just use a simple "validations" framework that's sort 157 | of similar to Rails/Django modeling, but simpler. 158 | - *Password salting:* Yeah, this is pretty important if you're storing 159 | credentials. You'll need to intercept each request you care about 160 | to make it behave correctly. 161 | - *Extra indexing/transforms:* If you wanted to store a "parent" 162 | attribute, for example, that was filled in server-side. Basically, 163 | anything you'd use an SQL trigger for. 164 | 165 | # Installing 166 | 167 | $ npm install ldapjs-riak 168 | 169 | `Nuff said. 170 | 171 | # More information 172 | 173 | ||[backend](/node-ldapjs-riak/backend.html)||Reference for creating and configuring the backend.|| 174 | 175 | ||License||[MIT](http://opensource.org/licenses/mit-license.php)|| 176 | ||Code||[mcavage/node-ldapjs-riak](https://github.com/mcavage/node-ldapjs-riak)|| 177 | ||node.js version||0.4.x and 0.5.x|| 178 | ||Twitter||[@mcavage](http://twitter.com/mcavage)|| 179 | -------------------------------------------------------------------------------- /lib/add.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var assert = require('assert'); 4 | 5 | var ldap = require('ldapjs'); 6 | 7 | var common = require('./common'); 8 | 9 | 10 | 11 | ///--- Handlers 12 | 13 | function exists(req, res, next) { 14 | var bucket = req.riak.bucket; 15 | var key = req.riak.key; 16 | 17 | common.exists(req, bucket, key, function(err, exists) { 18 | if (err) 19 | return next(err); 20 | 21 | if (exists) 22 | return next(new ldap.EntryAlreadyExistsError(key)); 23 | 24 | return next(); 25 | }); 26 | } 27 | 28 | 29 | function changelog(req, res, next) { 30 | if (!req.riak.changelogBucket) 31 | return next(); 32 | 33 | var bucket = req.riak.changelogBucket; 34 | var client = req.riak.client; 35 | var log = req.riak.log; 36 | 37 | log.debug('%s changelogging %s', req.logId, req.dn.toString()); 38 | 39 | var key = req.riak.changelog.nextChangeNumber; 40 | var now = new Date(); 41 | var entry = { 42 | dn: key.dn, 43 | attributes: { 44 | targetdn: req.dn.toString(), 45 | changetime: common.ISODateString(), 46 | changenumber: key.changeNumber, 47 | changetype: 'add' 48 | } 49 | }; 50 | var opts = { 51 | indexes: req.riak.changelog.indexes 52 | }; 53 | var obj = req.toObject(); 54 | if (obj.attributes.userpassword) 55 | obj.attributes.userpassword = 'XXXXXX'; 56 | entry.attributes.changes = JSON.stringify(obj.attributes); 57 | entry.attributes.objectclass = 'changeLogEntry'; 58 | 59 | // tack changelog entry to the response object 60 | res.changelog = entry; 61 | 62 | return client.put(bucket, key.dn, entry, opts, function(err, obj) { 63 | if (err) 64 | return next(operationsError(err)); 65 | 66 | log.debug('%s changelogged %s', req.logId, req.dn.toString()); 67 | return next(); 68 | }); 69 | } 70 | 71 | 72 | ///--- API 73 | 74 | module.exports = { 75 | 76 | chain: function(handlers) { 77 | assert.ok(handlers); 78 | 79 | [ 80 | exists, 81 | common.parentExists, 82 | common.operationalAttributes, 83 | common.buildIndexKeys, 84 | common.indexesExist, 85 | changelog, 86 | common.save, 87 | common.index, 88 | common.done, 89 | common.updatePersistentSearchClients 90 | ].forEach(function(h) { 91 | handlers.push(h); 92 | }); 93 | 94 | return handlers; 95 | } 96 | 97 | }; 98 | -------------------------------------------------------------------------------- /lib/bind.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var assert = require('assert'); 4 | 5 | var ldap = require('ldapjs'); 6 | 7 | var common = require('./common'); 8 | 9 | 10 | 11 | ///--- Handlers 12 | 13 | function check(req, res, next) { 14 | if (req.version !== 3) 15 | return next(new ldap.ProtocolError(req.version + ' is not v3')); 16 | 17 | if (req.authentication !== 'simple') 18 | return next(new ldap.AuthMethodNotSupportedError(req.authentication)); 19 | 20 | return next(); 21 | } 22 | 23 | 24 | function bind(req, res, next) { 25 | assert.ok(req.riak.entry); 26 | 27 | var obj = req.riak.entry.attributes; 28 | if (!obj.userpassword) 29 | return next(new ldap.NoSuchAttributeError('userPassword')); 30 | 31 | if (obj.userpassword[0] !== req.credentials) 32 | return next(new ldap.InvalidCredentialsError()); 33 | 34 | return next(); 35 | } 36 | 37 | 38 | 39 | ///--- Exported API 40 | 41 | module.exports = { 42 | 43 | chain: function(handlers) { 44 | assert.ok(handlers); 45 | 46 | [ 47 | check, 48 | common.load, 49 | bind, 50 | common.done 51 | ].forEach(function(h) { 52 | handlers.push(h); 53 | }); 54 | 55 | return handlers; 56 | } 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage. All rights reserved. 2 | 3 | var assert = require('assert'); 4 | 5 | var LRU = require('lru-cache'); 6 | 7 | 8 | 9 | ///--- API 10 | 11 | function Cache(options) { 12 | if (typeof(options) !== 'object') 13 | throw new TypeError('options (object) required'); 14 | 15 | this._cache = LRU(options.size || 1000); 16 | this._age = (options.age || 300) * 1000; 17 | 18 | var self = this; 19 | this.__defineGetter__('age', function() { return self._age / 1000; }); 20 | this.__defineSetter__('age', function(a) { 21 | self._age = a * 1000; 22 | }); 23 | } 24 | 25 | 26 | Cache.prototype.get = function(key) { 27 | assert.ok(key); 28 | 29 | var entry = this._cache.get(key); 30 | if (!entry) 31 | return null; 32 | 33 | var now = new Date(); 34 | if ((now.getTime() - entry.ctime) > this._age) 35 | return null; 36 | 37 | return entry.value; 38 | }; 39 | 40 | 41 | Cache.prototype.put = function(key, value) { 42 | assert.ok(key); 43 | 44 | var entry = { 45 | ctime: new Date().getTime(), 46 | value: value 47 | }; 48 | 49 | this._cache.set(key, entry); 50 | return value; 51 | }; 52 | 53 | 54 | 55 | ///--- Exports 56 | 57 | module.exports = { 58 | 59 | createCache: function(options) { 60 | return new Cache(options); 61 | } 62 | 63 | }; 64 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var assert = require('assert'); 4 | var qs = require('querystring'); 5 | 6 | var ldap = require('ldapjs'); 7 | 8 | 9 | 10 | ///--- Globals 11 | 12 | var parseDN = ldap.parseDN; 13 | 14 | var EntryChangeNotificationControl = ldap.EntryChangeNotificationControl; 15 | var PersistentSearch = require('./persistent_search'); 16 | 17 | var PS = new PersistentSearch(); 18 | 19 | var CHANGELOG_DN = parseDN('cn=changelog'); 20 | 21 | ///--- API 22 | 23 | function ISODateString() { 24 | function pad(n) { 25 | return n < 10 ? '0' + n : n; 26 | } 27 | 28 | var d = new Date(); 29 | 30 | return d.getUTCFullYear() + '-' + 31 | pad(d.getUTCMonth() + 1) + '-' + 32 | pad(d.getUTCDate()) + 'T' + 33 | pad(d.getUTCHours()) + ':' + 34 | pad(d.getUTCMinutes()) + ':' + 35 | pad(d.getUTCSeconds()) + 'Z'; 36 | } 37 | 38 | 39 | function operationsError(err) { 40 | var msg = err && err.message ? err.message : ''; 41 | return new ldap.OperationsError('riak failure: ' + msg, 42 | null, operationsError); 43 | } 44 | 45 | 46 | function exists(req, bucket, key, callback) { 47 | assert.ok(req); 48 | assert.ok(bucket); 49 | assert.ok(key); 50 | assert.ok(callback); 51 | 52 | var log = req.riak.log; 53 | 54 | log.debug('%s exists(%s/%s) entered', req.logId, bucket, key); 55 | return req.riak.client.head(bucket, key, function(err, obj, headers) { 56 | var exists = true; 57 | if (err) { 58 | if (err.code !== 404) 59 | return callback(operationsError(err)); 60 | 61 | exists = false; 62 | } 63 | 64 | log.debug('%s exists(%s/%s) %s', req.logId, bucket, key, exists); 65 | return callback(null, exists); 66 | }); 67 | } 68 | 69 | 70 | function childExists(req, res, next) { 71 | var bucket = req.riak.bucket; 72 | var client = req.riak.client; 73 | var key = qs.escape(req.riak.key); 74 | var log = req.riak.log; 75 | 76 | log.debug('%s looking for children of %s', req.logId, key); 77 | return client.find(bucket, '_parent', key, true, function(err, entries) { 78 | if (err) 79 | return next(operationsError(err)); 80 | 81 | if (entries && entries.length) 82 | return next(new ldap.NotAllowedOnNonLeafError(entries[0])); 83 | 84 | log.debug('%s %s has no children', req.logId, key); 85 | return next(); 86 | }); 87 | } 88 | 89 | 90 | function parentExists(req, res, next) { 91 | var bucket = req.riak.bucket; 92 | var log = req.riak.log; 93 | 94 | if (req.dn.equals(req.suffix)) { 95 | log.debug('%s adding suffix (%s)', req.logId, req.riak.key); 96 | return next(); 97 | } 98 | 99 | var parent = req.dn.parent(); 100 | assert.ok(parent); 101 | 102 | var key = parent.toString(); 103 | return exists(req, bucket, key, function(err, success) { 104 | if (err) 105 | return next(err); 106 | 107 | if (!success) 108 | return next(new ldap.NoSuchObjectError(key)); 109 | 110 | return next(); 111 | }); 112 | } 113 | 114 | 115 | function load(req, res, next) { 116 | var bucket = req.riak.bucket; 117 | var client = req.riak.client; 118 | var key = req.riak.key; 119 | var log = req.riak.log; 120 | 121 | log.debug('%s loading %s', req.logId, bucket, key); 122 | return client.get(bucket, key, function(err, obj) { 123 | if (err) { 124 | if (err.code !== 404) 125 | return next(operationsError(err)); 126 | 127 | return next(new ldap.NoSuchObjectError(key)); 128 | } 129 | 130 | log.debug('%s loaded %s -> %j', req.logId, bucket, key, obj); 131 | req.riak.entry = obj; 132 | return next(); 133 | }); 134 | } 135 | 136 | 137 | function buildIndexKeys(req, res, next) { 138 | assert.ok(req.riak.entry); 139 | 140 | req.riak.uniqueIndexKeys = []; 141 | 142 | if (!req.riak.uniqueIndexes || !req.riak.uniqueIndexes.length) 143 | return next(); 144 | 145 | var entry = req.riak.entry.attributes; 146 | req.riak.uniqueIndexes.forEach(function(i) { 147 | if (!entry.hasOwnProperty(i)) 148 | return; 149 | 150 | entry[i].forEach(function(v) { 151 | var key = i + ': ' + v; 152 | if (req.riak.uniqueIndexKeys.indexOf(key) === -1) 153 | req.riak.uniqueIndexKeys.push(key); 154 | }); 155 | }); 156 | 157 | return next(); 158 | } 159 | 160 | 161 | function indexesExist(req, res, next) { 162 | assert.ok(req.riak.entry); 163 | assert.ok(req.riak.uniqueIndexKeys); 164 | 165 | var log = req.riak.log; 166 | 167 | if (!req.riak.uniqueIndexKeys.length) { 168 | log.debug('%s indexesExist(%s) no-op', req.logId, req.riak.key); 169 | return next(); 170 | } 171 | 172 | log.debug('%s indexesExist(%s) entered %j', 173 | req.logId, req.riak.key, req.riak.uniqueIndexKeys); 174 | 175 | var done = false; 176 | var bucket = req.riak.uniqueIndexBucket; 177 | var entry = req.riak.entry.attributes; 178 | 179 | var finished = 0; 180 | return req.riak.uniqueIndexKeys.forEach(function(k) { 181 | exists(req, bucket, k, function(err, exists) { 182 | if (err && !done) { 183 | done = true; 184 | return next(err); 185 | } 186 | 187 | if (exists && !done) { 188 | done = true; 189 | return next(new ldap.ConstraintViolationError(k)); 190 | } 191 | 192 | if (++finished >= req.riak.uniqueIndexKeys.length && !done) { 193 | done = true; 194 | log.debug('%s uniqueIndexesExist(%s) ok', req.logId, req.riak.key); 195 | return next(); 196 | } 197 | }); 198 | }); 199 | } 200 | 201 | function index(req, res, next) { 202 | assert.ok(req.riak.uniqueIndexKeys); 203 | 204 | var bucket = req.riak.uniqueIndexBucket; 205 | var client = req.riak.client; 206 | var done = 0; 207 | var finished = 0; 208 | var log = req.riak.log; 209 | 210 | if (!req.riak.uniqueIndexKeys.length) 211 | return next(); 212 | 213 | log.debug('%s saving unique indexes for %s: %j', 214 | req.logId, req.riak.key, req.riak.uniqueIndexKeys); 215 | 216 | req.riak.uniqueIndexKeys.forEach(function(k) { 217 | var obj = { bucket: req.riak.bucket, key: req.riak.key }; 218 | return client.put(bucket, k, obj, {}, function(err) { 219 | if (err && !done) { 220 | done = true; 221 | return next(operationsError(err)); 222 | } 223 | 224 | if (++finished === req.riak.uniqueIndexKeys.length && !done) { 225 | done = true; 226 | log.debug('%s unique indexes for %s saved', req.logId, req.riak.key); 227 | return next(); 228 | } 229 | }); 230 | }); 231 | } 232 | 233 | 234 | function unindex(req, res, next) { 235 | assert.ok(req.riak.entry); 236 | assert.ok(req.riak.uniqueIndexKeys); 237 | 238 | var bucket = req.riak.uniqueIndexBucket; 239 | var client = req.riak.client; 240 | var entry = req.riak.entry.attributes; 241 | var keys = req.riak.uniqueIndexKeys; 242 | var log = req.riak.log; 243 | 244 | log.debug('%s indexes to purge: %j', req.logId, keys); 245 | if (!keys.length) 246 | return next(); 247 | 248 | var done = false; 249 | var finished = 0; 250 | keys.forEach(function(k) { 251 | log.debug('%s deleting index %s', req.logId, k); 252 | return client.del(bucket, k, function(err) { 253 | if (err && err.code !== 404 && !done) { 254 | done = true; 255 | return next(operationsError(err)); 256 | } 257 | 258 | if (err && err.code === 404) { 259 | log.warn('%s unable to purge unique index /riak/%s/%s', 260 | req.logId, bucket, k); 261 | } 262 | 263 | log.debug('%s deleted index %s', req.logId, k); 264 | if ((++finished === keys.length) && !done) { 265 | done = true; 266 | return next(); 267 | } 268 | }); 269 | }); 270 | } 271 | 272 | 273 | function save(req, res, next) { 274 | assert.ok(req.riak.entry); 275 | 276 | var bucket = req.riak.bucket; 277 | var client = req.riak.client; 278 | var entry = req.riak.entry; 279 | if (req.riak.changelogBucket) 280 | entry.attributes.changenumber = res.changelog.attributes.changenumber; 281 | var key = req.riak.key; 282 | var log = req.riak.log; 283 | 284 | if (entry.attributes.objectclass) { 285 | for (var i = 0; i < entry.attributes.objectclass.length; i++) 286 | entry.attributes.objectclass[i] = 287 | entry.attributes.objectclass[i].toLowerCase(); 288 | } 289 | 290 | 291 | log.debug('%s saving %s: %j', req.logId, key, entry); 292 | // cache the entry 293 | res.psentry = entry; 294 | 295 | var opts = { 296 | indexes: req.riak.indexes 297 | }; 298 | return client.put(bucket, key, entry, opts, function(err, obj) { 299 | if (err) 300 | return next(operationsError(err)); 301 | 302 | log.debug('%s %s saved', req.logId, key); 303 | return next(); 304 | }); 305 | } 306 | 307 | 308 | function done(req, res, next) { 309 | var log = req.riak.log; 310 | log.debug('%s key=%s done', req.logId, req.riak.key); 311 | if (req.persistentSearch) { 312 | // do not close the connection and register the req and res 313 | PS.addClient(req, res); 314 | res.connection.addListener('end', function() { 315 | // deregister the connection 316 | PS.removeClient(req, res); 317 | }); 318 | } else { 319 | res.end(); 320 | return next(); 321 | } 322 | } 323 | 324 | 325 | function operationalAttributes(req, res, next) { 326 | var log = req.riak.log; 327 | log.debug('%s operationAttributes(%s) entered', req.logId, req.riak.key); 328 | 329 | if (!req.riak.entry) 330 | req.riak.entry = req.toObject(); 331 | 332 | var attributes = req.riak.entry.attributes; 333 | if (!attributes._ctime) 334 | attributes._ctime = [ISODateString()]; 335 | if (!attributes._createdfrom) 336 | attributes._createdfrom = [req.logId]; 337 | if (!attributes._createdby) 338 | attributes._createdby = [req.connection.ldap.bindDN.toString()]; 339 | attributes._mtime = [ISODateString()]; 340 | attributes._modifiedfrom = [req.logId]; 341 | attributes._modifiedby = [req.connection.ldap.bindDN.toString()]; 342 | 343 | if (!req.dn.equals(req.suffix)) 344 | attributes._parent = [req.dn.parent().toString()]; 345 | 346 | log.debug('%s operationalAttributes(%s) -> %j', 347 | req.logId, req.riak.key, req.riak.entry); 348 | return next(); 349 | } 350 | 351 | 352 | function updatePersistentSearchClients(req, res, next) { 353 | // notify all pertinent clients of change 354 | // also check that the request.dn is for the changelog, 355 | // if so, handle differently 356 | PS.clientList.forEach(function(client) { 357 | // see if the change type of the PS request is the same as the current req 358 | if (PersistentSearch.checkChangeType(client.req, req.type)) { 359 | var control = 360 | PersistentSearch.getEntryChangeNotificationControl(client.req, 361 | res.changelog); 362 | var entry; 363 | if (client.req.dn.equals(CHANGELOG_DN)) { 364 | entry = res.changelog; 365 | } else { 366 | entry = res.psentry; 367 | } 368 | 369 | return sendSearchRequest(client.req, client.res, entry, control, next); 370 | } 371 | }); 372 | 373 | return next(); 374 | } 375 | 376 | 377 | function sendSearchRequest(req, res, entry, controls, callback) { 378 | assert.ok(req); 379 | assert.ok(res); 380 | assert.ok(entry); 381 | if (typeof(controls) === 'function') { 382 | callback = controls; 383 | controls = false; 384 | } 385 | assert.equal(typeof(callback), 'function'); 386 | 387 | var log = req.riak.log; 388 | var dn = parseDN(entry.dn); 389 | var attrs = entry.attributes; 390 | 391 | var send = false; 392 | 393 | switch (req.scope) { 394 | case 'base': 395 | if (req.dn.equals(dn) && req.filter.matches(attrs)) 396 | send = true; 397 | break; 398 | case 'one': 399 | if ((req.dn.parentOf(dn) || req.dn.equals(dn)) && 400 | ((dn.rdns.length - req.dn.rdns.length) <= 1) && 401 | req.filter.matches(attrs)) 402 | send = true; 403 | break; 404 | case 'sub': 405 | if ((req.dn.parentOf(dn) || req.dn.equals(dn)) && 406 | req.filter.matches(attrs)) 407 | send = true; 408 | break; 409 | } 410 | 411 | if (send) { 412 | if (controls) { 413 | // deep copy the obj so we can tack on the control, since the obj maybe 414 | // used by another request 415 | entry = JSON.parse(JSON.stringify(entry)); 416 | if (controls.isArray) { 417 | entry.controls = controls; 418 | } else { 419 | entry.controls = []; 420 | entry.controls.push(controls); 421 | } 422 | } 423 | 424 | log.debug('%s sending: %j', req.logId, entry); 425 | res.send(entry, req.hidden); 426 | } 427 | 428 | return callback(); 429 | } 430 | 431 | 432 | ///--- Exports 433 | 434 | module.exports = { 435 | ISODateString: ISODateString, 436 | operationsError: operationsError, 437 | exists: exists, 438 | childExists: childExists, 439 | parentExists: parentExists, 440 | load: load, 441 | buildIndexKeys: buildIndexKeys, 442 | indexesExist: indexesExist, 443 | index: index, 444 | unindex: unindex, 445 | save: save, 446 | done: done, 447 | operationalAttributes: operationalAttributes, 448 | updatePersistentSearchClients: updatePersistentSearchClients, 449 | sendSearchRequest: sendSearchRequest 450 | }; 451 | 452 | 453 | -------------------------------------------------------------------------------- /lib/compare.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var assert = require('assert'); 4 | 5 | var ldap = require('ldapjs'); 6 | 7 | var common = require('./common'); 8 | 9 | 10 | 11 | ///--- Handlers 12 | 13 | function compare(req, res, next) { 14 | assert.ok(req.riak.entry); 15 | 16 | var attribute = req.riak.entry.attributes[req.attribute]; 17 | if (!attribute) 18 | return next(new ldap.NoSuchAttributeError(req.attribute)); 19 | 20 | var found = false; 21 | for (var i = 0; i < attribute.length; i++) { 22 | if (req.value === attribute[i]) { 23 | found = true; 24 | break; 25 | } 26 | } 27 | 28 | res.end(found); 29 | return next(); 30 | } 31 | 32 | 33 | 34 | ///--- Exported API 35 | 36 | module.exports = { 37 | 38 | chain: function(handlers) { 39 | assert.ok(handlers); 40 | 41 | [ 42 | common.load, 43 | compare 44 | ].forEach(function(h) { 45 | handlers.push(h); 46 | }); 47 | 48 | return handlers; 49 | } 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /lib/del.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var assert = require('assert'); 4 | 5 | var ldap = require('ldapjs'); 6 | 7 | var common = require('./common'); 8 | 9 | 10 | 11 | ///--- Globals 12 | 13 | var operationsError = common.operationsError; 14 | 15 | 16 | 17 | ///--- Handlers 18 | 19 | function del(req, res, next) { 20 | var bucket = req.riak.bucket; 21 | var client = req.riak.client; 22 | var key = req.riak.key; 23 | var log = req.riak.log; 24 | 25 | log.debug('%s removing %s', req.logId, key); 26 | return client.del(bucket, key, function(err) { 27 | if (err) 28 | return next(operationsError(err)); 29 | 30 | res.psentry = { 31 | dn: req.dn.toString(), 32 | attributes: { 33 | objectclass: '*' 34 | } 35 | }; 36 | log.debug('%s removed %s', req.logId, key); 37 | return next(); 38 | }); 39 | } 40 | 41 | 42 | function changelog(req, res, next) { 43 | if (!req.riak.changelogBucket) 44 | return next(); 45 | 46 | var bucket = req.riak.changelogBucket; 47 | var client = req.riak.client; 48 | var log = req.riak.log; 49 | 50 | log.debug('%s changelogging %s', req.logId, req.dn.toString()); 51 | 52 | var key = req.riak.changelog.nextChangeNumber; 53 | var now = new Date(); 54 | var entry = { 55 | dn: key.dn, 56 | attributes: { 57 | targetdn: req.dn.toString(), 58 | changetime: common.ISODateString(), 59 | changenumber: key.changeNumber, 60 | changetype: 'delete' 61 | } 62 | }; 63 | var opts = { 64 | indexes: req.riak.changelog.indexes 65 | }; 66 | var obj = req.riak.entry; 67 | if (obj.attributes.userpassword) 68 | obj.attributes.userpassword = 'XXXXXX'; 69 | entry.attributes.changes = JSON.stringify(obj.attributes); 70 | entry.attributes.objectclass = 'changeLogEntry'; 71 | 72 | // tack changelog entry to the response object 73 | res.changelog = entry; 74 | 75 | return client.put(bucket, key.dn, entry, opts, function(err, obj) { 76 | if (err) 77 | return next(operationsError(err)); 78 | 79 | log.debug('%s changelogging %s', req.logId, req.dn.toString()); 80 | return next(); 81 | }); 82 | } 83 | 84 | 85 | 86 | ///--- Exported API 87 | 88 | module.exports = { 89 | 90 | chain: function(handlers) { 91 | assert.ok(handlers); 92 | 93 | [ 94 | common.load, 95 | common.childExists, 96 | changelog, 97 | del, 98 | common.buildIndexKeys, 99 | common.unindex, 100 | common.done, 101 | common.updatePersistentSearchClients 102 | ].forEach(function(h) { 103 | handlers.push(h); 104 | }); 105 | 106 | return handlers; 107 | } 108 | 109 | }; 110 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var assert = require('assert'); 4 | var util = require('util'); 5 | 6 | var ldap = require('ldapjs'); 7 | var uuid = require('node-uuid'); 8 | 9 | var add = require('./add'); 10 | var bind = require('./bind'); 11 | var compare = require('./compare'); 12 | var del = require('./del'); 13 | var modify = require('./modify'); 14 | var search = require('./search'); 15 | 16 | var Riak = require('./riak'); 17 | 18 | 19 | 20 | ///--- Backend 21 | 22 | /** 23 | * Constructs a new Riak backend for ldapjs. 24 | * 25 | * Options takes: 26 | * { 27 | * "bucket": { 28 | * "name": "ufds", 29 | * "props": { // These get set at startup 30 | * "n_val": 1, 31 | * "allow_mult": false, 32 | * "last_write_wins": false, 33 | * "r": 1, 34 | * "w": "quorum", 35 | * "dw": 1, 36 | * "rw": "quorum" 37 | * } 38 | * }, 39 | * "uniqueIndexBucket": { 40 | * "name": "ufds_uindex", 41 | * "props": { 42 | * "n_val": 2, 43 | * "allow_mult": false, 44 | * "last_write_wins": false, 45 | * "r": "all", 46 | * "w": "all", 47 | * "dw": 0, 48 | * "rw": "all" 49 | * } 50 | * }, 51 | * "changelogBucket": { 52 | * "name": "ufds_changelog", 53 | * "props": { ... }, 54 | * "suffix": "dc=changelog", 55 | * "sequenceCallback": function() { 56 | * return new Date().getTime() + ''; 57 | * } 58 | * }, 59 | * "indexes": { 60 | * "login": true, // true means unique index. false means not unique 61 | * "email": false, 62 | * "uuid": true, 63 | * }, 64 | * "client": { 65 | * "url": "http://localhost:8098", 66 | * "clientId": "coal-dev", // set this to `uname -n` 67 | * "retry": { 68 | * "retries": 3, 69 | * "factor": 2, 70 | * "minTimeout": 1000, 71 | * "maxTimeout": 10000 72 | * } 73 | * }, 74 | * "log4js": $(configured log4js object) 75 | * } 76 | * 77 | * @param {Object} options configuration object. 78 | * @throws {TypeError} on bad input. 79 | */ 80 | function RiakBackend(options) { 81 | if (!options || typeof(options) !== 'object') 82 | throw new TypeError('options (object) required'); 83 | if (typeof(options.log4js) !== 'object') 84 | throw new TypeError('options.log4js (object) required'); 85 | if (typeof(options.bucket) !== 'object') 86 | throw new TypeError('options.bucket (object) required'); 87 | if (typeof(options.client) !== 'object') 88 | throw new TypeError('options.client (object) required'); 89 | if (options.indexes && typeof(options.indexes) !== 'object') 90 | throw new TypeError('options.indexes must be an object'); 91 | if (options.changelogBucket) { 92 | var clb = options.changelogBucket; 93 | 94 | if (typeof(clb) !== 'object') 95 | throw new TypeError('options.changelogBucket must be an object'); 96 | if (clb.changeNumberCallback && 97 | typeof(clb.changeNumberCallback) !== 'function') 98 | throw new TypeError('sequenceCallback must be a function'); 99 | 100 | if (!clb.changeNumberCallback) { 101 | clb.changeNumberCallback = function() { 102 | return new Date().getTime() + ''; 103 | }; 104 | } 105 | 106 | if (!clb.suffix) 107 | clb.suffix = 'cn=changelog'; 108 | clb.__defineGetter__('nextChangeNumber', function() { 109 | var n = clb.changeNumberCallback() + ''; 110 | return { 111 | dn: 'changenumber=' + n + ', ' + clb.suffix, 112 | changeNumber: n 113 | }; 114 | }); 115 | 116 | clb.__defineGetter__('indexes', function() { 117 | return ['targetdn', 118 | 'changenumber_int', 119 | 'changetime', 120 | 'changetype']; 121 | }); 122 | 123 | } 124 | 125 | var self = this; 126 | 127 | this.log = options.log4js.getLogger('RiakBackend'); 128 | this.indexes = ['_parent']; 129 | this.uniqueIndexes = []; 130 | if (options.indexes) { 131 | Object.keys(options.indexes).forEach(function(i) { 132 | self.indexes.push(i); 133 | if (options.indexes[i]) { 134 | if (!options.uniqueIndexBucket) 135 | throw new Error('unique index set(' + i + 136 | ') but no unique index bucket set in config'); 137 | self.uniqueIndexes.push(i); 138 | } 139 | }); 140 | } 141 | 142 | this.__defineGetter__('name', function() { 143 | return 'RiakBackend'; 144 | }); 145 | 146 | this.__defineGetter__('client', function() { 147 | if (!self._riak) { 148 | options.client.log4js = options.log4js; 149 | self._riak = new Riak(options.client); 150 | } 151 | 152 | 153 | return self._riak; 154 | }); 155 | 156 | this.__defineGetter__('log4js', function() { 157 | return options.log4js; 158 | }); 159 | 160 | this.__defineGetter__('bucket', function() { 161 | return options.bucket; 162 | }); 163 | 164 | this.__defineGetter__('uniqueIndexBucket', function() { 165 | return options.uniqueIndexBucket || {}; 166 | }); 167 | 168 | this.__defineGetter__('changelogBucket', function() { 169 | return options.changelogBucket || {}; 170 | }); 171 | } 172 | 173 | 174 | /** 175 | * Connects to Riak and performs SetBucket(config.properties). 176 | * 177 | * @param {Function} callback of the form f(err). 178 | */ 179 | RiakBackend.prototype.init = function(callback) { 180 | if (typeof(callback) !== 'function') 181 | throw new TypeError('callback (function) required'); 182 | 183 | var finished = 0; 184 | var waitFor = 0; 185 | var client = this.client; 186 | var self = this; 187 | 188 | function _finish(err) { 189 | if (++finished === 3) 190 | return callback(err); 191 | } 192 | 193 | function _init(bucket) { 194 | if (!bucket || !bucket.name || !bucket.props) 195 | _finish(null); 196 | 197 | return client.setBucket(bucket.name, bucket.props, function(err) { 198 | return _finish(err); 199 | }); 200 | } 201 | 202 | _init(this.bucket); 203 | _init(this.uniqueIndexBucket); 204 | _init(this.changelogBucket); 205 | }; 206 | 207 | 208 | RiakBackend.prototype.add = function(handlers) { 209 | return this._operation(add, handlers); 210 | }; 211 | 212 | RiakBackend.prototype.bind = function(handlers) { 213 | return this._operation(bind, handlers); 214 | }; 215 | 216 | 217 | RiakBackend.prototype.compare = function(handlers) { 218 | return this._operation(compare, handlers); 219 | }; 220 | 221 | 222 | RiakBackend.prototype.del = function(handlers) { 223 | return this._operation(del, handlers); 224 | }; 225 | 226 | 227 | RiakBackend.prototype.modify = function(handlers) { 228 | return this._operation(modify, handlers); 229 | }; 230 | 231 | 232 | RiakBackend.prototype.search = function(handlers) { 233 | return this._operation(search, handlers); 234 | }; 235 | 236 | 237 | RiakBackend.prototype.changelogSearch = function(handlers) { 238 | var self = this; 239 | return this._operation(search, handlers, function() { 240 | return function setup(req, res, next) { 241 | req.riak = { 242 | bucket: self.changelogBucket.name, 243 | client: self.client, 244 | indexes: self.changelogBucket.indexes, 245 | key: req.dn.toString(), 246 | log: self.log4js.getLogger('Riak' + req.type), 247 | uniqueIndexBucket: uuid(), 248 | uniqueIndexes: [] 249 | }; 250 | 251 | return next(); 252 | }; 253 | }); 254 | }; 255 | 256 | RiakBackend.prototype._operation = function(op, handlers, setup) { 257 | if (!handlers) 258 | handlers = []; 259 | 260 | if (!Array.isArray(handlers)) 261 | handlers = [handlers]; 262 | handlers.unshift(setup ? setup() : this._setup()); 263 | return op.chain(handlers); 264 | }; 265 | 266 | 267 | RiakBackend.prototype._setup = function() { 268 | var self = this; 269 | return function setup(req, res, next) { 270 | req.riak = { 271 | bucket: self.bucket.name, 272 | client: self.client, 273 | indexes: self.indexes, 274 | key: req.dn.toString(), 275 | log: self.log4js.getLogger('Riak' + req.type), 276 | uniqueIndexBucket: self.uniqueIndexBucket.name, 277 | changelogBucket: self.changelogBucket ? self.changelogBucket.name : false, 278 | uniqueIndexes: self.uniqueIndexes, 279 | changelog: self.changelogBucket 280 | }; 281 | 282 | return next(); 283 | }; 284 | }; 285 | 286 | 287 | RiakBackend.prototype.toString = function() { 288 | var self = this; 289 | return this.name + ': ' + JSON.stringify({ 290 | url: self.client.url, 291 | bucket: self.bucket, 292 | uniqueIndexBucket: self.uniqueIndexBucket, 293 | changelogBucket: self.changelogBucket, 294 | indexes: self.indexes 295 | }); 296 | }; 297 | 298 | 299 | ///--- Exported API 300 | 301 | module.exports = { 302 | 303 | createBackend: function(options) { 304 | return new RiakBackend(options); 305 | }, 306 | 307 | RiakBackend: RiakBackend, 308 | 309 | createRiakClient: function(options) { 310 | return new Riak(options); 311 | }, 312 | 313 | Riak: Riak 314 | }; 315 | -------------------------------------------------------------------------------- /lib/modify.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var assert = require('assert'); 4 | var util = require('util'); 5 | 6 | var ldap = require('ldapjs'); 7 | 8 | var common = require('./common'); 9 | 10 | 11 | 12 | ///--- Handlers 13 | 14 | function modify(req, res, next) { 15 | assert.ok(req.riak.entry); 16 | var log = req.riak.log; 17 | 18 | var attributes = req.riak.entry.attributes; 19 | var delta = {}; 20 | 21 | // Modify the loaded entry 22 | if (log.isDebugEnabled()) { 23 | var msg = ''; 24 | req.changes.forEach(function(c) { 25 | msg += JSON.stringify(c.json); 26 | }); 27 | log.debug('%s processing modifications %s', req.logId, msg); 28 | } 29 | req.changes.forEach(function(change) { 30 | var mod = change.modification; 31 | 32 | switch (change.operation) { 33 | 34 | case 'add': 35 | if (!attributes[mod.type]) 36 | attributes[mod.type] = []; 37 | mod.vals.forEach(function(v) { 38 | if (attributes[mod.type].indexOf(v) === -1) { 39 | attributes[mod.type].push(v); 40 | if (!delta[mod.type]) 41 | delta[mod.type] = []; 42 | delta[mod.type].push(v); 43 | } 44 | }); 45 | break; 46 | 47 | case 'delete': 48 | if (!attributes[mod.type]) 49 | return; // Just silently allow this. 50 | 51 | if (!mod.vals || !mod.vals.length) { 52 | delete attributes[mod.type]; 53 | } else { 54 | mod.vals.forEach(function(v) { 55 | var index = attributes[mod.type].indexOf(v); 56 | if (index !== -1) 57 | attributes[mod.type].splice(index, 1); 58 | }); 59 | 60 | if (attributes[mod.type].length === 0) 61 | delete attributes[mod.type]; 62 | } 63 | break; 64 | 65 | case 'replace': 66 | if (!attributes[mod.type]) 67 | attributes[mod.type] = []; 68 | if (!delta[mod.type]) 69 | delta[mod.type] = []; 70 | 71 | if (!mod.vals || !mod.vals.length) { 72 | delete attributes[mod.type]; 73 | } else { 74 | // If there are unique indexes present, don't do an 75 | // update if the values are actually the same (delta only) 76 | var diff = true; 77 | if (attributes[mod.type].length === mod.vals.length) { 78 | attributes[mod.type].sort(); 79 | mod.vals.sort(); 80 | for (var i = 0; i < mod.vals.length; i++) { 81 | diff &= (attributes[mod.type][i] !== mod.vals[i]); 82 | if (!diff) 83 | break; 84 | } 85 | } 86 | attributes[mod.type] = mod.vals.slice(); 87 | if (diff) 88 | delta[mod.type] = mod.vals.slice(); 89 | } 90 | break; 91 | } 92 | }); 93 | 94 | log.debug('%s using delta %j to check unique indexes next', req.logId, delta); 95 | req.riak.entry.attributes = delta; 96 | req.riak.stashedAttributes = attributes; 97 | return next(); 98 | } 99 | 100 | 101 | function stash(req, res, next) { 102 | assert.ok(req.riak.uniqueIndexKeys); 103 | 104 | var log = req.riak.log; 105 | 106 | log.debug('%s stashing indexes: %j', req.logId, req.riak.uniqueIndexKeys); 107 | req.riak.stashedIndexKeys = req.riak.uniqueIndexKeys; 108 | req.riak.uniqueIndexKeys = []; 109 | return next(); 110 | } 111 | 112 | 113 | function pop(req, res, next) { 114 | assert.ok(req.riak.stashedIndexKeys); 115 | assert.ok(req.riak.stashedAttributes); 116 | var log = req.riak.log; 117 | 118 | req.riak.uniqueIndexKeys = req.riak.stashedIndexKeys; 119 | req.riak.stashedIndexKeys = null; 120 | req.riak.entry.attributes = req.riak.stashedAttributes; 121 | req.riak.stashedAttributes = null; 122 | 123 | log.debug('%s restored indexes: %j and entry %j', 124 | req.logId, req.riak.uniqueIndexKeys, req.riak.entry); 125 | return next(); 126 | } 127 | 128 | 129 | function changelog(req, res, next) { 130 | if (!req.riak.changelogBucket) 131 | return next(); 132 | 133 | var bucket = req.riak.changelogBucket; 134 | var client = req.riak.client; 135 | var log = req.riak.log; 136 | 137 | log.debug('%s changelogging %s', req.logId, req.dn.toString()); 138 | 139 | var key = req.riak.changelog.nextChangeNumber; 140 | var now = new Date(); 141 | var entry = { 142 | dn: key.dn, 143 | attributes: { 144 | targetdn: req.dn.toString(), 145 | changetime: common.ISODateString(), 146 | changenumber: key.changeNumber, 147 | changetype: 'modify' 148 | } 149 | }; 150 | var changes = []; 151 | req.changes.forEach(function(c) { 152 | if (c.modification.type.toLowerCase() === 'userpassword') 153 | c.modification.vals = ['XXXXXX']; 154 | changes.push(c.json); 155 | }); 156 | 157 | entry.attributes.changes = JSON.stringify(changes); 158 | entry.attributes.objectclass = 'changeLogEntry'; 159 | 160 | var opts = { 161 | indexes: req.riak.changelog.indexes 162 | }; 163 | 164 | // tack changelog entry to the response object 165 | res.changelog = entry; 166 | 167 | return client.put(bucket, key.dn, entry, opts, function(err, obj) { 168 | if (err) 169 | return next(operationsError(err)); 170 | 171 | log.debug('%s changelogging %s', req.logId, req.dn.toString()); 172 | return next(); 173 | }); 174 | } 175 | 176 | ///--- Exported API 177 | 178 | module.exports = { 179 | 180 | chain: function(handlers) { 181 | assert.ok(handlers); 182 | 183 | [ 184 | common.load, 185 | common.buildIndexKeys, 186 | stash, 187 | modify, 188 | common.buildIndexKeys, 189 | common.indexesExist, 190 | pop, 191 | common.unindex, 192 | common.operationalAttributes, 193 | common.buildIndexKeys, 194 | changelog, 195 | common.save, 196 | common.index, 197 | common.done, 198 | common.updatePersistentSearchClients 199 | ].forEach(function(h) { 200 | handlers.push(h); 201 | }); 202 | 203 | return handlers; 204 | } 205 | 206 | }; 207 | -------------------------------------------------------------------------------- /lib/persistent_search.js: -------------------------------------------------------------------------------- 1 | var ldap = require('ldapjs'); 2 | 3 | 4 | 5 | ///--- Globals 6 | 7 | var parseDN = ldap.parseDN; 8 | 9 | var EntryChangeNotificationControl = ldap.EntryChangeNotificationControl; 10 | 11 | ///--- API 12 | 13 | 14 | // Cache used to store connected persistent search clients 15 | function PersistentSearch() { 16 | this.clientList = []; 17 | } 18 | module.exports = PersistentSearch; 19 | 20 | PersistentSearch.prototype.addClient = function(req, res, callback) { 21 | if (typeof(req) !== 'object') 22 | throw new TypeError('req must be an object'); 23 | if (typeof(res) !== 'object') 24 | throw new TypeError('res must be an object'); 25 | if (callback && typeof(callback) !== 'function') 26 | throw new TypeError('callback must be a function'); 27 | 28 | var log = req.log; 29 | 30 | var client = {}; 31 | client.req = req; 32 | client.res = res; 33 | 34 | log.debug('%s storing client', req.logId); 35 | 36 | this.clientList.push(client); 37 | 38 | log.debug('%s stored client', req.logId); 39 | log.debug('%s total number of clients %s', req.logId, this.clientList.length); 40 | if (callback) 41 | callback(client); 42 | }; 43 | 44 | 45 | PersistentSearch.prototype.removeClient = function(req, res, callback) { 46 | if (typeof(req) !== 'object') 47 | throw new TypeError('req must be an object'); 48 | if (typeof(res) !== 'object') 49 | throw new TypeError('res must be an object'); 50 | if (callback && typeof(callback) !== 'function') 51 | throw new TypeError('callback must be a function'); 52 | 53 | var log = req.log; 54 | log.debug('%s removing client', req.logId); 55 | var client = {}; 56 | client.req = req; 57 | client.res = res; 58 | 59 | // remove the client if it exists 60 | this.clientList.forEach(function(element, index, array) { 61 | if (element.req === client.req) { 62 | log.debug('%s removing client from list', req.logId); 63 | array.splice(index, 1); 64 | } 65 | }); 66 | 67 | log.debug('%s number of persistent search clients %s', 68 | req.logId, this.clientList.length); 69 | if (callback) 70 | callback(client); 71 | }; 72 | 73 | 74 | getOperationType = function(requestType) { 75 | switch (requestType) { 76 | case 'AddRequest': 77 | case 'add': 78 | return 1; 79 | case 'DeleteRequest': 80 | case 'delete': 81 | return 2; 82 | case 'ModifyRequest': 83 | case 'modify': 84 | return 4; 85 | case 'ModifyDNRequest': 86 | case 'modrdn': 87 | return 8; 88 | default: 89 | throw new TypeError('requestType %s, is an invalid request type', 90 | request); 91 | } 92 | }; 93 | 94 | 95 | PersistentSearch.getEntryChangeNotificationControl = 96 | function(req, obj, callback) { 97 | // if we want to return a ECNC 98 | if (req.persistentSearch.value.returnECs) { 99 | var attrs = obj.attributes; 100 | var value = {}; 101 | value.changeType = getOperationType(attrs.changetype); 102 | // if it's a modDN request, fill in the previous DN 103 | if (value.changeType === 8 && attrs.previousDN) { 104 | value.previousDN = attrs.previousDN; 105 | } 106 | 107 | value.changeNumber = attrs.changenumber; 108 | return new EntryChangeNotificationControl({ value: value }); 109 | } else { 110 | return false; 111 | } 112 | }; 113 | 114 | 115 | PersistentSearch.checkChangeType = function(req, requestType) { 116 | return (req.persistentSearch.value.changeTypes & 117 | getOperationType(requestType)); 118 | }; 119 | -------------------------------------------------------------------------------- /lib/riak.js: -------------------------------------------------------------------------------- 1 | 2 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 3 | 4 | var assert = require('assert'); 5 | var http = require('http'); 6 | var https = require('https'); 7 | var url = require('url'); 8 | var util = require('util'); 9 | 10 | var qs = require('querystring'); 11 | var retry = require('retry'); 12 | var uuid = require('node-uuid'); 13 | var sprintf = require('sprintf').sprintf; 14 | 15 | var cache = require('./cache'); 16 | 17 | 18 | 19 | ///--- Internal Helpers 20 | 21 | function httpDate(date) { 22 | function pad(val) { 23 | if (parseInt(val, 10) < 10) { 24 | val = '0' + val; 25 | } 26 | return val; 27 | } 28 | 29 | if (!date) 30 | date = new Date(); 31 | 32 | var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 33 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 34 | var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 35 | return days[date.getUTCDay()] + ', ' + 36 | pad(date.getUTCDate()) + ' ' + 37 | months[date.getUTCMonth()] + ' ' + 38 | date.getUTCFullYear() + ' ' + 39 | pad(date.getUTCHours()) + ':' + 40 | pad(date.getUTCMinutes()) + ':' + 41 | pad(date.getUTCSeconds()) + 42 | ' GMT'; 43 | } 44 | 45 | 46 | function RiakError(res, message) { 47 | var name = http.STATUS_CODES[res.statusCode] || 'Unknown'; 48 | 49 | Error.call(this, message || name); 50 | if (Error.captureStackTrace) 51 | Error.captureStackTrace(this, RiakError); 52 | 53 | this.name = name.replace(/\s/, '') + 'Error'; 54 | 55 | this.__defineGetter__('code', function() { 56 | return res.statusCode; 57 | }); 58 | this.__defineGetter__('headers', function() { 59 | return res.headers; 60 | }); 61 | } 62 | util.inherits(RiakError, Error); 63 | 64 | 65 | 66 | ///--- API 67 | 68 | /** 69 | * Creates a new (single) Riak client via HTTP. 70 | * 71 | * Defaults (options): 72 | * - url: http://localhost:8098 73 | * - log4js: require('log4js') 74 | * - retry: { 75 | * retries: 3, 76 | * factor: 2, 77 | * minTimeout: 1s, 78 | * maxTimeout: 60s 79 | * }, 80 | * - cache: { 81 | * size: 1000, 82 | * age: 300 83 | * } 84 | * - clientId: uuid() 85 | * - headers: {} 86 | * 87 | * @param {Object} options see above. 88 | */ 89 | function Riak(options) { 90 | if (!options) 91 | options = {}; 92 | if (typeof(options) !== 'object') 93 | throw new TypeError('options (object) required'); 94 | 95 | if (!options.headers) 96 | options.headers = {}; 97 | if (!options.url) 98 | options.url = ['http://localhost:8098']; 99 | if (!options.clientId) 100 | options.clientId = uuid(); 101 | if (!options.log4js) 102 | options.log4js = require('log4js'); 103 | if (!options.retry) 104 | options.retry = { 105 | retries: 3, 106 | factor: 2, 107 | minTimeout: 1 * 1000, 108 | maxTimeout: 60 * 1000 109 | }; 110 | 111 | if (options.cache) 112 | this.cache = cache.createCache(options.cache); 113 | 114 | if (!Array.isArray(options.url)) 115 | options.url = [options.url]; 116 | 117 | var self = this; 118 | var urls = []; 119 | 120 | options.url.forEach(function(u) { 121 | urls.push(url.parse(u)); 122 | }); 123 | 124 | var index = -1; 125 | this.__defineGetter__('url', function() { 126 | if (++index === urls.length) 127 | index = 0; 128 | 129 | return urls[index]; 130 | }); 131 | 132 | this.__defineGetter__('id', function() { 133 | return options.clientId; 134 | }); 135 | this.__defineGetter__('log', function() { 136 | if (!self._log) 137 | self._log = options.log4js.getLogger('Riak'); 138 | 139 | return self._log; 140 | }); 141 | this.__defineGetter__('retry', function() { 142 | return retry.operation(options.retry); 143 | }); 144 | } 145 | module.exports = Riak; 146 | 147 | 148 | /** 149 | * Performs a Riak ListBuckets operation. 150 | * 151 | * @param {Function} callback of the form f(err, buckets). 152 | */ 153 | Riak.prototype.listBuckets = function(callback) { 154 | if (typeof(callback) !== 'function') 155 | throw new TypeError('callback (function) required'); 156 | 157 | var opts = { 158 | path: '/riak?buckets=true' 159 | }; 160 | this._request(opts, function(err, obj, res) { 161 | if (err) 162 | return callback(err); 163 | 164 | return callback(null, obj.buckets || [], res.headers); 165 | }); 166 | }; 167 | 168 | 169 | /** 170 | * Performs a Riak ListKeys operation. 171 | * 172 | * @param {String} bucket bucket name. 173 | * @param {Function} callback of the form f(err, keys, headers). 174 | */ 175 | Riak.prototype.listKeys = function(bucket, callback) { 176 | if (!bucket || typeof(bucket) !== 'string') 177 | throw new TypeError('bucket (string) required'); 178 | if (typeof(callback) !== 'function') 179 | throw new TypeError('callback (function) required'); 180 | 181 | var self = this; 182 | 183 | var opts = { 184 | path: sprintf('/riak/%s?keys=true&props=false', qs.escape(bucket)) 185 | }; 186 | 187 | this._request(opts, function(err, obj, res) { 188 | if (err) 189 | return callback(err); 190 | 191 | return callback(null, (obj.keys || []), res.headers); 192 | }); 193 | }; 194 | 195 | 196 | /** 197 | * Performs a Riak GetBucket operation. 198 | * 199 | * Does not list keys; use ListKeys for that. 200 | * 201 | * @param {String} bucket bucket name. 202 | * @param {Function} callback of the form f(err, properties). 203 | */ 204 | Riak.prototype.getBucket = function(bucket, callback) { 205 | if (!bucket || typeof(bucket) !== 'string') 206 | throw new TypeError('bucket (string) required'); 207 | if (typeof(callback) !== 'function') 208 | throw new TypeError('callback (function) required'); 209 | 210 | var opts = { 211 | path: sprintf('/riak/%s', qs.escape(bucket)) 212 | }; 213 | this._request(opts, function(err, obj, res) { 214 | if (err) 215 | return callback(err); 216 | 217 | return callback(null, obj.props || {}, res.headers); 218 | }); 219 | }; 220 | 221 | 222 | /** 223 | * Performs a Riak SetBucket operation. 224 | * 225 | * Options takes params exactly as specified in the basho wiki, without the 226 | * 'props' key. 227 | * 228 | * @param {String} bucket bucket name. 229 | * @param {Object} options properties to write. 230 | * @param {Function} callback of the form f(err, properties). 231 | */ 232 | Riak.prototype.setBucket = function(bucket, options, callback) { 233 | if (!bucket || typeof(bucket) !== 'string') 234 | throw new TypeError('bucket (string) required'); 235 | if (!options || typeof(options) !== 'object') 236 | throw new TypeError('options (object) required'); 237 | if (typeof(callback) !== 'function') 238 | throw new TypeError('callback (function) required'); 239 | 240 | var opts = { 241 | path: sprintf('/riak/%s', qs.escape(bucket)), 242 | method: 'PUT' 243 | }; 244 | this._request(opts, function(err, obj, res) { 245 | if (err) 246 | return callback(err); 247 | 248 | return callback((res.statusCode !== 204 ? new RiakError(res) : null), 249 | res.headers); 250 | }, function() { 251 | return JSON.stringify({ props: options }); 252 | }); 253 | }; 254 | 255 | 256 | /** 257 | * Performs a Riak FetchObject. 258 | * 259 | * @param {String} bucket bucket name. 260 | * @param {String} key key name. 261 | * @param {Object} options optional properties (r, vtag, and 'headers'). 262 | * @param {Function} callback of the form f(err, obj, properties). 263 | * @param {Boolean} head optionally set this to true to perform a HEAD. 264 | */ 265 | Riak.prototype.fetchObject = function(bucket, key, options, callback, head) { 266 | if (!bucket || typeof(bucket) !== 'string') 267 | throw new TypeError('bucket (string) required'); 268 | if (!key || typeof(key) !== 'string') 269 | throw new TypeError('key (string) required'); 270 | if (typeof(options) === 'function') { 271 | callback = options; 272 | options = {}; 273 | } 274 | if (typeof(options) !== 'object') 275 | throw new TypeError('options must be an object'); 276 | if (typeof(callback) !== 'function') 277 | throw new TypeError('callback (function) required'); 278 | 279 | var self = this; 280 | var opts = { 281 | path: sprintf('/riak/%s/%s', qs.escape(bucket), qs.escape(key)), 282 | headers: options.headers, 283 | method: head ? 'HEAD' : 'GET' 284 | }; 285 | 286 | if (!head) { 287 | var cached = this._cacheGet(opts.path); 288 | if (cached) 289 | return callback(null, cached); 290 | } 291 | 292 | return this._request(opts, function(err, obj, res) { 293 | if (err) 294 | return callback(err); 295 | 296 | if (res.statusCode === 300) 297 | return callback(new RiakError(res, 'Multiple Choices')); 298 | if (res.statusCode === 404) 299 | return callback(new RiakError(res, opts.path + ' not found')); 300 | if (res.statusCode !== 200) 301 | return callback(new RiakError(res)); 302 | 303 | if (!head) 304 | self._cachePut(opts.path, obj); 305 | 306 | return callback(null, obj || {}, res.headers); 307 | }); 308 | }; 309 | 310 | 311 | /** 312 | * Performs a Riak StoreObject. 313 | * 314 | * @param {String} bucket bucket name. 315 | * @param {String} key key name. 316 | * @param {Object} object JSON object to store. 317 | * @param {Object} options properties (r, vtag, 'indexes' and 'headers'). 318 | * @param {Function} callback of the form f(err, key, properties). 319 | */ 320 | Riak.prototype.storeObject = function(bucket, key, object, options, callback) { 321 | if (!bucket || typeof(bucket) !== 'string') 322 | throw new TypeError('bucket (string) required'); 323 | switch (typeof(key)) { 324 | case 'string': 325 | break; 326 | case 'object': 327 | if (typeof(object) === 'function') { 328 | callback = object; 329 | object = key; 330 | options = {}; 331 | key = ''; 332 | } else if (typeof(options) === 'function') { 333 | callback = options; 334 | options = object; 335 | object = key; 336 | key = ''; 337 | } else { 338 | throw new TypeError('key must be a string'); 339 | } 340 | break; 341 | default: 342 | throw new TypeError('key must be a string'); 343 | } 344 | if (typeof(object) !== 'object') 345 | throw new TypeError('object (object) required'); 346 | if (typeof(options) === 'function') { 347 | callback = options; 348 | options = {}; 349 | } 350 | if (typeof(options) !== 'object') 351 | throw new TypeError('options must be an object'); 352 | if (typeof(callback) !== 'function') 353 | throw new TypeError('callback (function) required'); 354 | 355 | var self = this; 356 | var path; 357 | if (key) 358 | path = sprintf('/riak/%s/%s', qs.escape(bucket), qs.escape(key)); 359 | 360 | if (!path) 361 | path = sprintf('/riak/%s', qs.escape(bucket)); 362 | 363 | var _query = { returnbody: false }; 364 | if (options.w) _query.w = options.w; 365 | if (options.dw) _query.dw = options.dw; 366 | path += '?' + qs.stringify(_query); 367 | 368 | var opts = { 369 | path: path, 370 | headers: options.headers || {}, 371 | method: key ? 'PUT' : 'POST' 372 | }; 373 | 374 | // Add indexes 375 | if (options.indexes) { 376 | var _indexes = this.indexObject(options.indexes, object); 377 | Object.keys(_indexes).forEach(function(i) { 378 | if (!opts.headers[i]) 379 | opts.headers[i] = []; 380 | 381 | opts.headers[i].push(_indexes[i]); 382 | }); 383 | } 384 | 385 | // Nuke this key prematurely 386 | self._cachePut(opts.path, null); 387 | return this._request(opts, function(err, obj, res) { 388 | if (err) 389 | return callback(err); 390 | 391 | if (res.statusCode === 201) 392 | return callback(null, res.headers.location.split('/').pop(), res.headers); 393 | 394 | if (res.statusCode === 300) 395 | return callback(new RiakError(res, 'Multiple Choices')); 396 | 397 | if (res.statusCode !== 200 && res.statusCode !== 204) 398 | return callback(new RiakError(res)); 399 | 400 | self._cachePut(opts.path, object); 401 | 402 | return callback(null, key, res.headers); 403 | }, function() { 404 | return JSON.stringify(object); 405 | }); 406 | }; 407 | 408 | 409 | /** 410 | * Generates an object of HTTP headers that direct Riak to index when you 411 | * save a key. 412 | * 413 | * @param {Array} indexes list of fields to index (strings). 414 | * @param {Object} object the target object you want Riak to index. 415 | */ 416 | Riak.prototype.indexObject = function(indexes, object) { 417 | if (!indexes || (typeof(indexes) !== 'string' && !Array.isArray(indexes))) 418 | throw new TypeError('indexes ([string]) required'); 419 | if (!object || typeof(object) !== 'object') 420 | throw new TypeError('object (object) required'); 421 | 422 | if (!Array.isArray(indexes)) 423 | indexes = [indexes]; 424 | 425 | var headers = {}; 426 | 427 | function _header(index, value) { 428 | index = index.toLowerCase(); 429 | value = value.toLowerCase(); 430 | 431 | if (!/\w_(bin|int)$/.test(index)) 432 | index = index + '_bin'; 433 | 434 | if (!headers['x-riak-index-' + index]) 435 | headers['x-riak-index-' + index] = []; 436 | 437 | headers['x-riak-index-' + index].push(qs.escape(value).toLowerCase()); 438 | return headers; 439 | } 440 | 441 | function _isIndex(key) { 442 | for (var i = 0; i < indexes.length; i++) 443 | if (indexes[i].replace(/_(bin|int)$/, '') === key) 444 | return indexes[i]; 445 | 446 | return false; 447 | } 448 | 449 | function _index(key, value) { 450 | var i; 451 | switch (typeof(value)) { 452 | case 'string': 453 | case 'boolean': 454 | case 'number': 455 | i = _isIndex(key); 456 | if (i) 457 | _header(i, value + ''); 458 | break; 459 | case 'object': 460 | if (value === null) 461 | return; 462 | if (Array.isArray(value)) { 463 | value.forEach(function(v) { 464 | return _index(key, v); 465 | }); 466 | } else { 467 | Object.keys(value).forEach(function(k) { 468 | return _index(k, value[k]); 469 | }); 470 | } 471 | break; 472 | default: 473 | break; 474 | } 475 | } 476 | 477 | Object.keys(object).forEach(function(k) { 478 | return _index(k, object[k]); 479 | }); 480 | return headers; 481 | }; 482 | 483 | 484 | /** 485 | * Performs a Riak DeleteObject. 486 | * 487 | * @param {String} bucket bucket name. 488 | * @param {String} key key name. 489 | * @param {Object} options properties (rw, and 'headers'). 490 | * @param {Function} callback of the form f(err, properties). 491 | */ 492 | Riak.prototype.deleteObject = function(bucket, key, options, callback) { 493 | if (!bucket || typeof(bucket) !== 'string') 494 | throw new TypeError('bucket (string) required'); 495 | if (!key || typeof(key) !== 'string') 496 | throw new TypeError('key (string) required'); 497 | if (typeof(options) === 'function') { 498 | callback = options; 499 | options = {}; 500 | } 501 | if (typeof(options) !== 'object') 502 | throw new TypeError('options must be an object'); 503 | if (typeof(callback) !== 'function') 504 | throw new TypeError('callback (function) required'); 505 | 506 | var self = this; 507 | var opts = { 508 | path: sprintf('/riak/%s/%s', qs.escape(bucket), qs.escape(key)), 509 | headers: options.headers, 510 | method: 'DELETE' 511 | }; 512 | 513 | self._cachePut(opts.path, null); 514 | return this._request(opts, function(err, obj, res) { 515 | if (err) 516 | return callback(err); 517 | 518 | if (res.statusCode === 404) 519 | return callback(new RiakError(res, opts.path + ' not found')); 520 | if (res.statusCode !== 204) 521 | return callback(new RiakError(res)); 522 | 523 | return callback(null, res.headers); 524 | }); 525 | }; 526 | 527 | 528 | /** 529 | * Performs a Riak FetchObject (by index). 530 | * 531 | * To perform a range query pass in an array to $value. Index types default to 532 | * `_bin`. If that's not what you want, just explicitly set it, like: 533 | * 534 | * client.fetchObjectByIndex(bucket, 'time_int', 1234, 'gte', callback); 535 | * 536 | * Note this method "auto resolves" objects. 537 | * 538 | * @param {String} bucket bucket name. 539 | * @param {String} index index name. 540 | * @param {String} value the value to look up. 541 | * @param {Boolean} keysOnly optional param to have this not return objects. 542 | * @param {Function} callback of the form f(err, objects, properties). 543 | */ 544 | Riak.prototype.fetchObjectsByIndex = function(bucket, 545 | index, 546 | value, 547 | keysOnly, 548 | callback) { 549 | 550 | if (!bucket || typeof(bucket) !== 'string') 551 | throw new TypeError('bucket (string) required'); 552 | if (!index || typeof(index) !== 'string') 553 | throw new TypeError('index (string) required'); 554 | if (!value || (typeof(value) !== 'string' && !Array.isArray(value))) 555 | throw new TypeError('value (string|array[string]) required'); 556 | if (typeof(keysOnly) === 'function') { 557 | callback = keysOnly; 558 | keysOnly = false; 559 | } 560 | if (typeof(keysOnly) !== 'boolean') 561 | throw new TypeError('keysOnly (boolean) required'); 562 | if (typeof(callback) !== 'function') 563 | throw new TypeError('callback (function) required'); 564 | 565 | var self = this; 566 | var finished = 0; 567 | var keys; 568 | var objects = []; 569 | 570 | function _esc(v) { 571 | v = v.toLowerCase(); 572 | return qs.escape(v); 573 | } 574 | 575 | if (!Array.isArray(value)) 576 | value = [value]; 577 | 578 | if (!/_(bin|int)$/.test(index)) 579 | index = index + '_bin'; 580 | 581 | index = index.toLowerCase(); 582 | var opts = { 583 | path: sprintf('/buckets/%s/index/%s', qs.escape(bucket), _esc(_esc(index))) 584 | }; 585 | value.forEach(function(v) { 586 | opts.path += '/' + _esc(v); 587 | }); 588 | 589 | return this._request(opts, function(err, obj, res) { 590 | if (err) 591 | return callback(err); 592 | 593 | if (res.statusCode !== 200) 594 | return callback(new RiakError(res)); 595 | 596 | keys = obj.keys || []; 597 | if (keysOnly || !keys.length) 598 | return callback(null, keys); 599 | 600 | return keys.forEach(function keysIterator(k) { 601 | self.fetchObject(bucket, k, function(err, obj, headers) { 602 | if (err && finished < keys.length) { 603 | finished = keys.length + 1; 604 | return callback(err); 605 | } 606 | 607 | objects.push(obj); 608 | if (++finished === keys.length) { 609 | return callback(null, objects); 610 | } 611 | }); 612 | }); 613 | }); 614 | }; 615 | 616 | 617 | /** 618 | * Takes exactly what Basho's wiki says map reduce takes. 619 | * 620 | * @param {Object} inputs map reduce inputs. 621 | * @param {Object} query reduce phases, etc. 622 | * @param {Function} callback of f(err, stuff. headers). 623 | */ 624 | Riak.prototype.mapred = function(inputs, query, callback) { 625 | if (typeof(inputs) !== 'object') 626 | throw new TypeError('inputs (object) required'); 627 | if (typeof(query) !== 'object') 628 | throw new TypeError('query (object) required'); 629 | if (typeof(callback) !== 'function') 630 | throw new TypeError('callback (function) required'); 631 | 632 | var opts = { 633 | path: '/mapred', 634 | method: 'POST' 635 | }; 636 | this._request(opts, function(err, obj, res) { 637 | if (err) 638 | return callback(err); 639 | 640 | return callback((res.statusCode !== 200 ? 641 | new RiakError(res) : null), 642 | obj, 643 | res.headers); 644 | }, function() { 645 | return JSON.stringify({ 646 | inputs: inputs, 647 | query: query 648 | }); 649 | }); 650 | }; 651 | 652 | 653 | 654 | ///--- Friendly wrappers 655 | 656 | Riak.prototype.put = function(bucket, key, object, options, callback) { 657 | if (typeof(key) !== 'string') 658 | return this.setBucket(bucket, options, callback); 659 | 660 | return this.storeObject(bucket, key, object, options, callback); 661 | }; 662 | 663 | 664 | Riak.prototype.get = function(bucket, key, options, callback) { 665 | if (typeof(key) !== 'string') 666 | return this.getBucket(bucket, options, callback); 667 | 668 | return this.fetchObject(bucket, key, options, callback); 669 | }; 670 | 671 | Riak.prototype.head = function(bucket, key, options, callback) { 672 | return this.fetchObject(bucket, key, options, callback, true); 673 | }; 674 | 675 | Riak.prototype.list = function(bucket, callback) { 676 | if (typeof(bucket) !== 'string') 677 | return this.listBuckets(callback); 678 | 679 | return this.listKeys(bucket, callback); 680 | }; 681 | 682 | 683 | Riak.prototype.find = Riak.prototype.fetchObjectsByIndex; 684 | Riak.prototype.post = Riak.prototype.storeObject; 685 | Riak.prototype.del = Riak.prototype.deleteObject; 686 | 687 | 688 | ///--- Private methods 689 | 690 | Riak.prototype._cachePut = function(key, value) { 691 | var log = this.log; 692 | if (this.cache) { 693 | if (log.isTraceEnabled()) 694 | log.trace('cachePut: %s -> %j', key, value); 695 | 696 | function clone(obj) { 697 | if (!obj) 698 | return obj; 699 | 700 | var target; 701 | if (Array.isArray(obj)) { 702 | target = []; 703 | obj.forEach(function(i) { 704 | target.push(clone(i)); 705 | }); 706 | } else { 707 | switch (typeof(obj)) { 708 | case 'object': 709 | target = {}; 710 | Object.keys(obj).forEach(function(k) { 711 | target[k] = clone(obj[k]); 712 | }); 713 | break; 714 | case 'string': 715 | target = obj + ''; 716 | break; 717 | default: 718 | target = obj; 719 | break; 720 | } 721 | } 722 | return target; 723 | } 724 | 725 | this.cache.put(key, clone(value)); 726 | } 727 | }; 728 | 729 | 730 | Riak.prototype._cacheGet = function(key) { 731 | var log = this.log; 732 | var value = null; 733 | 734 | if (this.cache) { 735 | value = this.cache.get(key); 736 | if (log.isTraceEnabled()) 737 | log.trace('cacheGet: %s -> %j', key, value); 738 | } 739 | 740 | return value; 741 | }; 742 | 743 | 744 | Riak.prototype._request = function(options, callback, write) { 745 | assert.ok(options); 746 | assert.ok(callback); 747 | 748 | var self = this; 749 | var content = null; 750 | 751 | var opts = { 752 | method: options.method || 'GET', 753 | path: '', 754 | headers: options.headers || {} 755 | }; 756 | opts.headers.accept = 'application/json'; 757 | opts.headers['X-Riak-ClientId'] = self.id; 758 | opts.headers.date = httpDate(); 759 | 760 | if (write) { 761 | opts.headers['content-type'] = 'application/json'; 762 | content = write(); 763 | } 764 | 765 | var op = self.retry; 766 | op.attempt(function(attempt) { 767 | // Set these so we pick up a new URL each time (potentially) 768 | var u = self.url; 769 | opts.host = u.hostname || '127.0.0.1'; 770 | if (u.pathname && u.pathname !== '/') 771 | opts.path = u.pathname; 772 | opts.path = (options.path ? options.path : ''); 773 | opts.port = u.port || 8098; 774 | 775 | if (self.log.isTraceEnabled()) { 776 | var logHeaders = 'host: ' + u.protocol + '//' + 777 | opts.host + ':' + opts.port; 778 | Object.keys(opts.headers).forEach(function(h) { 779 | logHeaders += sprintf('\n%s: %s', h, opts.headers[h]); 780 | }); 781 | self.log.trace('%s %s HTTP/1.1 #attempt=%d\n%s\n%s\n', 782 | opts.method, opts.path, attempt, logHeaders, 783 | (content ? content : '')); 784 | } 785 | 786 | function _error(err) { 787 | if (op.retry(err)) 788 | return; 789 | 790 | return callback(op.mainError()); 791 | } 792 | 793 | function _callback(res) { 794 | if (self.log.isTraceEnabled()) { 795 | var logHeaders = ''; 796 | Object.keys(res.headers).forEach(function(h) { 797 | logHeaders += sprintf('\n%s: %s', h, res.headers[h]); 798 | }); 799 | self.log.trace('HTTP/1.1 %d%s', res.statusCode, logHeaders); 800 | } 801 | 802 | res.on('error', function(err) { 803 | return _error(res, err.message); 804 | }); 805 | 806 | res.setEncoding('utf8'); 807 | res.body = ''; 808 | if (!options.stream) { 809 | res.on('data', function(chunk) { 810 | res.body += chunk; 811 | }); 812 | res.on('end', function() { 813 | var _obj = null; 814 | 815 | if (res.statusCode >= 500) 816 | return _error(new RiakError(res, res.body)); 817 | 818 | self.log.trace('response received %s', res.body); 819 | if (res.body && res.headers['content-type'] === 'application/json') { 820 | try { 821 | _obj = JSON.parse(res.body) || {}; 822 | } catch (e) { 823 | return callback(e); 824 | } 825 | } 826 | 827 | return callback(null, _obj, res); 828 | }); 829 | } else { 830 | return callback(null, res); 831 | } 832 | } 833 | 834 | var req = /^https.+/.test(u.protocol) ? 835 | https.request(opts, _callback) : 836 | http.request(opts, _callback); 837 | 838 | req.on('error', function(err) { 839 | return _error(err); 840 | }); 841 | 842 | if (write) 843 | req.write(content); 844 | 845 | req.end(); 846 | }); 847 | }; 848 | -------------------------------------------------------------------------------- /lib/search.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var assert = require('assert'); 4 | var qs = require('querystring'); 5 | 6 | var ldap = require('ldapjs'); 7 | 8 | var common = require('./common'); 9 | 10 | 11 | 12 | ///--- Globals 13 | 14 | var operationsError = common.operationsError; 15 | var parseDN = ldap.parseDN; 16 | 17 | var UTF8_START = String.fromCharCode(0x00); 18 | var UTF8_END = String.fromCharCode(0xffff); 19 | 20 | 21 | ///--- Handlers 22 | 23 | function _send(req, res, obj, callback) { 24 | var log = req.riak.log; 25 | 26 | if (typeof(req.searchCallback) !== 'function') 27 | return common.sendSearchRequest(req, res, obj, callback); 28 | 29 | log.debug('searchCallback registered, calling with %j', obj); 30 | return req.searchCallback(req, obj, function(err, _obj) { 31 | if (err) 32 | log.warn('%s searchCallback failed for %s. Sending original entry: %s', 33 | req.logId, req.dn.toString(), err.stack); 34 | 35 | return common.sendSearchRequest(req, res, _obj || obj, callback); 36 | }); 37 | } 38 | 39 | 40 | ///--- API 41 | 42 | function subtreeSearch(req, res, next) { 43 | var bucket = req.riak.bucket; 44 | var client = req.riak.client; 45 | var filter = req.filter; 46 | var key = req.riak.key; 47 | 48 | function getIndexVals(filter) { 49 | assert.ok(filter); 50 | 51 | if (filter.attribute && req.riak.indexes.indexOf(filter.attribute) === -1) 52 | return false; 53 | 54 | var vals = false; 55 | switch (filter.type) { 56 | case 'present': 57 | case 'substring': 58 | vals = [UTF8_START, UTF8_END]; 59 | break; 60 | 61 | case 'approx': 62 | case 'equal': 63 | vals = [qs.escape(filter.value)]; 64 | break; 65 | 66 | case 'ge': 67 | vals = [qs.escape(filter.value), UTF8_END]; 68 | break; 69 | 70 | case 'le': 71 | vals = [UTF8_START, qs.escape(filter.value)]; 72 | break; 73 | 74 | case 'and': 75 | for (var i = 0; i < filter.filters.length; i++) { 76 | var _res = getIndexVals(filter.filters[i]); 77 | if (_res && _res.attribute && _res.vals) 78 | return _res; 79 | } 80 | 81 | break; 82 | 83 | default: // or and not we can't deal with here 84 | break; 85 | } 86 | 87 | return vals ? { attribute: filter.attribute, vals: vals } : false; 88 | } 89 | 90 | var job = getIndexVals(req.filter); 91 | if (job) { 92 | return client.find(bucket, job.attribute, job.vals, function(err, objects) { 93 | if (err) 94 | return next(operationsError(err)); 95 | 96 | if (!objects || !objects.length) 97 | return next(); 98 | 99 | var finished = 0; 100 | function callback() { 101 | if (++finished === objects.length) 102 | return next(); 103 | } 104 | 105 | objects.forEach(function(o) { 106 | _send(req, res, o, callback); 107 | }); 108 | }); 109 | } 110 | 111 | return client.listKeys(bucket, function(err, keys) { 112 | if (err) 113 | return next(operationsError(err)); 114 | 115 | if (!keys.length) 116 | return next(); 117 | 118 | var query = []; 119 | var done = false; 120 | var finished = 0; 121 | keys.forEach(function(k) { 122 | var dn = parseDN(k); 123 | 124 | if (!req.baseObject.parentOf(dn) && !req.baseObject.equals(dn)) 125 | return; 126 | 127 | return query.push(k); 128 | }); 129 | 130 | if (!query.length) 131 | return next(); 132 | 133 | return query.forEach(function(k) { 134 | return client.get(bucket, k, function(err, obj) { 135 | if (done) 136 | return; 137 | 138 | if (err) { 139 | done = true; 140 | return next(operationsError(err)); 141 | } 142 | 143 | return _send(req, res, obj, function() { 144 | if (++finished === query.length) { 145 | if (!done) { 146 | done = true; 147 | return next(); 148 | } 149 | } 150 | }); 151 | }); 152 | }); 153 | }); 154 | } 155 | 156 | 157 | function baseSearch(req, res, next) { 158 | var bucket = req.riak.bucket; 159 | var client = req.riak.client; 160 | var key = req.riak.key; 161 | 162 | return client.get(bucket, key, function(err, obj) { 163 | if (err) { 164 | if (err.code !== 404) 165 | return next(operationsError(err)); 166 | return next(new ldap.NoSuchObjectError(key)); 167 | } 168 | 169 | _send(req, res, obj, function() { 170 | return next(); 171 | }); 172 | }); 173 | } 174 | 175 | 176 | function oneLevelSearch(req, res, next) { 177 | var log = req.riak.log; 178 | var bucket = req.riak.bucket; 179 | var client = req.riak.client; 180 | var key = req.riak.key; 181 | 182 | return client.find(bucket, '_parent', qs.escape(key), function(err, objects) { 183 | if (err) 184 | return next(operationsError(err)); 185 | 186 | if (!objects || !objects.length) 187 | return next(); 188 | 189 | var finished = 0; 190 | function callback() { 191 | if (++finished === objects.length) 192 | return next(); 193 | } 194 | 195 | objects.forEach(function(o) { 196 | _send(req, res, o, callback); 197 | }); 198 | }); 199 | } 200 | 201 | 202 | function search(req, res, next) { 203 | var log = req.riak.log; 204 | 205 | if (log.isDebugEnabled()) 206 | log.debug('%s searching %j', req.logId, req.json); 207 | 208 | // intercept the search request and figure out if it's persistent 209 | req.controls.forEach(function(c) { 210 | if (c.type === '1.3.6.1.4.1.38678.1') // hidden attributes control 211 | req.hidden = true; 212 | if (c.type === '2.16.840.1.113730.3.4.3') { // persistent search control 213 | req.persistentSearch = c; 214 | } 215 | }); 216 | 217 | if (req.persistentSearch && req.persistentSearch.value.changesOnly) { 218 | // short circuit search if it's changes only 219 | return next(); 220 | } 221 | 222 | try { 223 | switch (req.scope) { 224 | case 'base': 225 | return baseSearch(req, res, next); 226 | case 'one': 227 | return oneLevelSearch(req, res, next); 228 | case 'sub': 229 | return subtreeSearch(req, res, next); 230 | } 231 | } catch (e) { 232 | log.warn('%s invalid search scope: %s', req.logId, e.stack); 233 | return next(new ldap.ProtocolError(e.message)); 234 | } 235 | } 236 | 237 | 238 | 239 | ///--- API 240 | 241 | module.exports = { 242 | send: _send, 243 | chain: function(handlers) { 244 | assert.ok(handlers); 245 | 246 | [ 247 | search, 248 | common.done 249 | 250 | ].forEach(function(h) { 251 | handlers.push(h); 252 | }); 253 | 254 | return handlers; 255 | } 256 | 257 | }; 258 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Mark Cavage ", 3 | "name": "ldapjs-riak", 4 | "homepage": "http://ldapjs.org", 5 | "description": "A Riak backend for ldapjs (server).", 6 | "version": "0.2.5", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/mcavage/node-ldapjs-riak.git" 10 | }, 11 | "main": "lib/index.js", 12 | "scripts": { 13 | "pretest": "which gjslint; if [[ \"$?\" = 0 ]] ; then gjslint --nojsdoc -r lib -r tst; else echo \"Missing gjslint. Skipping lint\"; fi", 14 | "test": "./node_modules/.bin/tap ./tst" 15 | }, 16 | "engines": { 17 | "node": ">=0.4.10" 18 | }, 19 | "dependencies": { 20 | "ldapjs": "0.4.2", 21 | "lru-cache": "1.0.5", 22 | "node-uuid": "1.3.3", 23 | "retry": "0.5.0", 24 | "sprintf": "0.1.1" 25 | }, 26 | "devDependencies": { 27 | "tap": "0.1.4", 28 | "log4js": "0.4.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tst/add.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var ldap = require('ldapjs'); 4 | var log4js = require('log4js'); 5 | var test = require('tap').test; 6 | var uuid = require('node-uuid'); 7 | 8 | 9 | 10 | ///--- Globals 11 | 12 | var SUFFIX = 'cn=unit, o=test'; 13 | var SOCKET = '/tmp/.' + uuid(); 14 | 15 | var backend; 16 | var client; 17 | var server; 18 | 19 | 20 | 21 | ///--- Tests 22 | 23 | test('setup', function(t) { 24 | var riakjs = require('../lib/index'); 25 | t.ok(riakjs); 26 | t.ok(riakjs.createBackend); 27 | t.equal(typeof(riakjs.createBackend), 'function'); 28 | backend = riakjs.createBackend({ 29 | bucket: { 30 | name: uuid() 31 | }, 32 | uniqueIndexBucket: { 33 | name: uuid() 34 | }, 35 | changelogBucket: { 36 | name: uuid() 37 | }, 38 | indexes: { 39 | l: false, 40 | cn: true 41 | }, 42 | client: { 43 | url: 'http://localhost:8098', 44 | cache: { 45 | size: 100, 46 | age: 10 47 | } 48 | }, 49 | log4js: log4js 50 | }); 51 | t.ok(backend); 52 | t.ok(backend.add); 53 | t.equal(typeof(backend.add), 'function'); 54 | server = ldap.createServer(); 55 | t.ok(server); 56 | 57 | server.add(SUFFIX, backend, backend.add()); 58 | 59 | server.listen(SOCKET, function() { 60 | client = ldap.createClient({ 61 | socketPath: SOCKET 62 | }); 63 | t.ok(client); 64 | t.end(); 65 | }); 66 | }); 67 | 68 | 69 | test('handler chain', function(t) { 70 | var handlers = backend.add(); 71 | t.ok(handlers); 72 | t.ok(Array.isArray(handlers)); 73 | handlers.forEach(function(h) { 74 | t.equal(typeof(h), 'function'); 75 | }); 76 | t.end(); 77 | }); 78 | 79 | 80 | test('handler chain append', function(t) { 81 | var handlers = backend.add([ 82 | function foo(req, res, next) { 83 | return next(); 84 | } 85 | ]); 86 | t.ok(handlers); 87 | t.ok(Array.isArray(handlers)); 88 | handlers.forEach(function(h) { 89 | t.equal(typeof(h), 'function'); 90 | }); 91 | t.equal(handlers[1].name, 'foo'); 92 | t.end(); 93 | }); 94 | 95 | 96 | test('add suffix', function(t) { 97 | var entry = { 98 | cn: 'unit', 99 | objectClass: 'organization', 100 | o: 'test' 101 | }; 102 | client.add(SUFFIX, entry, function(err, res) { 103 | t.ifError(err); 104 | t.ok(res); 105 | t.equal(res.status, 0); 106 | t.end(); 107 | }); 108 | }); 109 | 110 | 111 | test('add child missing parent', function(t) { 112 | var entry = { 113 | cn: 'unit', 114 | objectClass: 'organization', 115 | o: 'test' 116 | }; 117 | client.add('cn=fail, ou=fail' + SUFFIX, entry, function(err, res) { 118 | t.ok(err); 119 | t.ok(err instanceof ldap.NoSuchObjectError); 120 | t.notOk(res); 121 | t.end(); 122 | }); 123 | }); 124 | 125 | 126 | test('add child ok', function(t) { 127 | var entry = { 128 | cn: 'child', 129 | objectClass: 'person', 130 | sn: 'test', 131 | l: 'seattle' 132 | }; 133 | client.add('cn=child,' + SUFFIX, entry, function(err, res) { 134 | t.ifError(err); 135 | t.ok(res); 136 | t.equal(res.status, 0); 137 | t.end(); 138 | }); 139 | }); 140 | 141 | 142 | test('add child exists', function(t) { 143 | var entry = { 144 | objectClass: uuid() 145 | }; 146 | client.add('cn=child,' + SUFFIX, entry, function(err, res) { 147 | t.ok(err); 148 | t.ok(err instanceof ldap.EntryAlreadyExistsError); 149 | t.notOk(res); 150 | t.end(); 151 | }); 152 | }); 153 | 154 | 155 | test('add child unique confilct', function(t) { 156 | var entry = { 157 | cn: 'child', 158 | objectClass: 'person', 159 | sn: 'test', 160 | l: 'seattle' 161 | }; 162 | client.add('cn=child2,' + SUFFIX, entry, function(err, res) { 163 | t.ok(err); 164 | t.ok(err instanceof ldap.ConstraintViolationError); 165 | t.notOk(res); 166 | t.end(); 167 | }); 168 | }); 169 | 170 | 171 | test('teardown', function(t) { 172 | function close() { 173 | client.unbind(function() { 174 | server.on('close', function() { 175 | t.end(); 176 | }); 177 | server.close(); 178 | }); 179 | } 180 | 181 | function cleanup(bucket) { 182 | riak.list(bucket, function(err, keys) { 183 | if (keys && keys.length) { 184 | var _finished = 0; 185 | return keys.forEach(function(k) { 186 | riak.del(bucket, k, function(err) { 187 | if (++_finished >= keys.length) { 188 | if (++finished === 3) 189 | return close(); 190 | } 191 | }); 192 | }); 193 | } 194 | 195 | if (++finished === 3) 196 | return close(); 197 | }); 198 | } 199 | 200 | var riak = backend.client; 201 | var finished = 0; 202 | cleanup(backend.bucket.name); 203 | cleanup(backend.changelogBucket.name); 204 | cleanup(backend.uniqueIndexBucket.name); 205 | }); 206 | -------------------------------------------------------------------------------- /tst/bind.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var ldap = require('ldapjs'); 4 | var log4js = require('log4js'); 5 | var test = require('tap').test; 6 | var uuid = require('node-uuid'); 7 | 8 | 9 | 10 | ///--- Globals 11 | 12 | var SUFFIX = 'o=' + uuid(); 13 | var SOCKET = '/tmp/.' + uuid(); 14 | 15 | var backend; 16 | var client; 17 | var server; 18 | 19 | 20 | ///--- Tests 21 | 22 | test('setup', function(t) { 23 | var riakjs = require('../lib/index'); 24 | t.ok(riakjs); 25 | t.ok(riakjs.createBackend); 26 | t.equal(typeof(riakjs.createBackend), 'function'); 27 | backend = riakjs.createBackend({ 28 | bucket: { 29 | name: uuid() 30 | }, 31 | uniqueIndexBucket: { 32 | name: uuid() 33 | }, 34 | indexes: { 35 | l: false, 36 | uid: true 37 | }, 38 | client: { 39 | url: 'http://localhost:8098', 40 | cache: { 41 | size: 100, 42 | age: 10 43 | } 44 | }, 45 | log4js: log4js 46 | }); 47 | 48 | t.ok(backend); 49 | t.ok(backend.bind); 50 | t.equal(typeof(backend.bind), 'function'); 51 | server = ldap.createServer(); 52 | t.ok(server); 53 | 54 | server.add(SUFFIX, backend, backend.add()); 55 | server.bind(SUFFIX, backend, backend.bind()); 56 | 57 | server.listen(SOCKET, function() { 58 | client = ldap.createClient({ 59 | socketPath: SOCKET 60 | }); 61 | t.ok(client); 62 | t.end(); 63 | }); 64 | }); 65 | 66 | 67 | test('handler chain', function(t) { 68 | var handlers = backend.bind(); 69 | t.ok(handlers); 70 | t.ok(Array.isArray(handlers)); 71 | handlers.forEach(function(h) { 72 | t.equal(typeof(h), 'function'); 73 | }); 74 | t.end(); 75 | }); 76 | 77 | 78 | test('handler chain append', function(t) { 79 | var handlers = backend.bind([ 80 | function foo(req, res, next) { 81 | return next(); 82 | } 83 | ]); 84 | t.ok(handlers); 85 | t.ok(Array.isArray(handlers)); 86 | handlers.forEach(function(h) { 87 | t.equal(typeof(h), 'function'); 88 | }); 89 | t.equal(handlers[1].name, 'foo'); 90 | t.end(); 91 | }); 92 | 93 | 94 | test('add fixtures', function(t) { 95 | var suffix = { 96 | objectClass: 'top', 97 | objectClass: 'organization', 98 | o: SUFFIX.split('=')[1], 99 | userPassword: 'secret' 100 | }; 101 | client.add(SUFFIX, suffix, function(err, res) { 102 | t.ifError(err); 103 | t.ok(res); 104 | t.equal(res.status, 0); 105 | t.end(); 106 | }); 107 | }); 108 | 109 | 110 | test('bind success', function(t) { 111 | client.bind(SUFFIX, 'secret', function(err) { 112 | t.ifError(err); 113 | t.end(); 114 | }); 115 | }); 116 | 117 | 118 | test('bind invalid password', function(t) { 119 | client.bind(SUFFIX, 'secre', function(err) { 120 | t.ok(err); 121 | t.ok(err instanceof ldap.InvalidCredentialsError); 122 | t.end(); 123 | }); 124 | }); 125 | 126 | 127 | test('bind non-existent entry', function(t) { 128 | client.bind('cn=child,' + SUFFIX, 'foo', function(err) { 129 | t.ok(err); 130 | t.ok(err instanceof ldap.NoSuchObjectError); 131 | t.end(); 132 | }); 133 | }); 134 | 135 | 136 | test('teardown', function(t) { 137 | var riak = backend.client; 138 | var bucket = backend.bucket; 139 | 140 | function close() { 141 | client.unbind(function() { 142 | server.on('close', function() { 143 | t.end(); 144 | }); 145 | server.close(); 146 | }); 147 | } 148 | 149 | function removeUniqueIndexes() { 150 | var bucket = backend.uniqueIndexBucket.name; 151 | riak.list(bucket, function(err, keys) { 152 | if (keys && keys.length) { 153 | var finished = 0; 154 | keys.forEach(function(k) { 155 | riak.del(bucket, k, function(err) { 156 | if (++finished >= keys.length) { 157 | return close(); 158 | } 159 | }); 160 | }); 161 | } else { 162 | return close(); 163 | } 164 | }); 165 | } 166 | 167 | var bucket = backend.bucket.name; 168 | return riak.list(bucket, function(err, keys) { 169 | if (keys && keys.length) { 170 | var finished = 0; 171 | keys.forEach(function(k) { 172 | riak.del(bucket, k, function(err) { 173 | if (++finished >= keys.length) { 174 | return removeUniqueIndexes(); 175 | } 176 | }); 177 | }); 178 | } else { 179 | return removeUniqueIndexes(); 180 | } 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /tst/changelog.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var ldap = require('ldapjs'); 4 | var log4js = require('log4js'); 5 | var test = require('tap').test; 6 | var uuid = require('node-uuid'); 7 | 8 | 9 | 10 | ///--- Globals 11 | 12 | var SUFFIX = 'o=' + uuid(); 13 | var SOCKET = '/tmp/.' + uuid(); 14 | var TOTAL_ENTRIES = 3; 15 | 16 | var backend; 17 | var client; 18 | var server; 19 | 20 | 21 | ///--- Tests 22 | 23 | test('setup', function(t) { 24 | var riakjs = require('../lib/index'); 25 | t.ok(riakjs); 26 | t.ok(riakjs.createBackend); 27 | t.equal(typeof(riakjs.createBackend), 'function'); 28 | backend = riakjs.createBackend({ 29 | bucket: { 30 | name: uuid() 31 | }, 32 | uniqueIndexBucket: { 33 | name: uuid() 34 | }, 35 | changelogBucket: { 36 | name: uuid() 37 | }, 38 | indexes: { 39 | l: false, 40 | uid: true 41 | }, 42 | client: { 43 | url: 'http://localhost:8098' 44 | }, 45 | log4js: log4js 46 | }); 47 | t.ok(backend); 48 | t.ok(backend.search); 49 | t.equal(typeof(backend.search), 'function'); 50 | server = ldap.createServer(); 51 | t.ok(server); 52 | 53 | server.add(SUFFIX, backend, backend.add()); 54 | server.search('cn=changelog', backend, backend.changelogSearch()); 55 | 56 | server.listen(SOCKET, function() { 57 | client = ldap.createClient({ 58 | socketPath: SOCKET 59 | }); 60 | t.ok(client); 61 | t.end(); 62 | }); 63 | }); 64 | 65 | 66 | test('handler chain', function(t) { 67 | var handlers = backend.changelogSearch(); 68 | t.ok(handlers); 69 | t.ok(Array.isArray(handlers)); 70 | handlers.forEach(function(h) { 71 | t.equal(typeof(h), 'function'); 72 | }); 73 | t.end(); 74 | }); 75 | 76 | 77 | test('handler chain append', function(t) { 78 | var handlers = backend.changelogSearch([ 79 | function foo(req, res, next) { 80 | return next(); 81 | } 82 | ]); 83 | t.ok(handlers); 84 | t.ok(Array.isArray(handlers)); 85 | handlers.forEach(function(h) { 86 | t.equal(typeof(h), 'function'); 87 | }); 88 | t.equal(handlers[1].name, 'foo'); 89 | t.end(); 90 | }); 91 | 92 | 93 | test('add fixtures', function(t) { 94 | var suffix = { 95 | objectClass: 'top', 96 | objectClass: 'organization', 97 | o: SUFFIX.split('=')[1] 98 | }; 99 | client.add(SUFFIX, suffix, function(err, res) { 100 | t.ifError(err); 101 | t.ok(res); 102 | t.equal(res.status, 0); 103 | 104 | var finished = 0; 105 | for (var i = 0; i < TOTAL_ENTRIES; i++) { 106 | var entry = { 107 | cn: 'child' + i, 108 | objectClass: 'person', 109 | uid: uuid(), 110 | sn: 'test', 111 | l: i % 3 ? 'vancouver' : 'seattle' 112 | }; 113 | client.add('cn=child' + i + ',' + SUFFIX, entry, function(err, res) { 114 | t.ifError(err); 115 | t.ok(res); 116 | t.equal(res.status, 0); 117 | 118 | if (++finished === TOTAL_ENTRIES) 119 | t.end(); 120 | }); 121 | } 122 | }); 123 | }); 124 | 125 | 126 | test('search sub objectclass=*', function(t) { 127 | client.search('cn=changelog', { scope: 'sub' }, function(err, res) { 128 | t.ifError(err); 129 | t.ok(res); 130 | 131 | var retrieved = 0; 132 | res.on('searchEntry', function(entry) { 133 | t.ok(entry); 134 | t.ok(entry instanceof ldap.SearchEntry); 135 | t.ok(entry.dn.toString()); 136 | t.ok(entry.attributes); 137 | t.ok(entry.attributes.length); 138 | t.ok(entry.object); 139 | retrieved++; 140 | }); 141 | res.on('error', function(err) { 142 | t.fail(err); 143 | }); 144 | res.on('end', function(res) { 145 | t.ok(res); 146 | t.ok(res instanceof ldap.SearchResponse); 147 | t.equal(res.status, 0); 148 | t.ok(retrieved); 149 | t.end(); 150 | }); 151 | }); 152 | }); 153 | 154 | test('teardown', function(t) { 155 | function close() { 156 | client.unbind(function() { 157 | server.on('close', function() { 158 | t.end(); 159 | }); 160 | server.close(); 161 | }); 162 | } 163 | 164 | function cleanup(bucket) { 165 | riak.list(bucket, function(err, keys) { 166 | if (keys && keys.length) { 167 | var _finished = 0; 168 | return keys.forEach(function(k) { 169 | riak.del(bucket, k, function(err) { 170 | if (++_finished >= keys.length) { 171 | if (++finished === 3) 172 | return close(); 173 | } 174 | }); 175 | }); 176 | } 177 | 178 | if (++finished === 3) 179 | return close(); 180 | }); 181 | } 182 | 183 | var riak = backend.client; 184 | var finished = 0; 185 | cleanup(backend.bucket.name); 186 | cleanup(backend.changelogBucket.name); 187 | cleanup(backend.uniqueIndexBucket.name); 188 | }); 189 | -------------------------------------------------------------------------------- /tst/compare.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var ldap = require('ldapjs'); 4 | var log4js = require('log4js'); 5 | var test = require('tap').test; 6 | var uuid = require('node-uuid'); 7 | 8 | 9 | 10 | ///--- Globals 11 | 12 | var SUFFIX = 'o=' + uuid(); 13 | var SOCKET = '/tmp/.' + uuid(); 14 | 15 | var backend; 16 | var client; 17 | var server; 18 | 19 | 20 | ///--- Tests 21 | 22 | test('setup', function(t) { 23 | var riakjs = require('../lib/index'); 24 | t.ok(riakjs); 25 | t.ok(riakjs.createBackend); 26 | t.equal(typeof(riakjs.createBackend), 'function'); 27 | backend = riakjs.createBackend({ 28 | bucket: { 29 | name: uuid() 30 | }, 31 | uniqueIndexBucket: { 32 | name: uuid() 33 | }, 34 | indexes: { 35 | l: false, 36 | uid: true 37 | }, 38 | client: { 39 | url: 'http://localhost:8098', 40 | cache: { 41 | size: 100, 42 | age: 10 43 | } 44 | }, 45 | log4js: log4js 46 | }); 47 | t.ok(backend); 48 | t.ok(backend.add); 49 | t.equal(typeof(backend.add), 'function'); 50 | server = ldap.createServer({ 51 | log4js: log4js 52 | }); 53 | t.ok(server); 54 | 55 | server.add(SUFFIX, backend, backend.add()); 56 | server.compare(SUFFIX, backend, backend.compare()); 57 | 58 | server.listen(SOCKET, function() { 59 | client = ldap.createClient({ 60 | socketPath: SOCKET 61 | }); 62 | t.ok(client); 63 | t.end(); 64 | }); 65 | }); 66 | 67 | 68 | test('handler chain', function(t) { 69 | var handlers = backend.compare(); 70 | t.ok(handlers); 71 | t.ok(Array.isArray(handlers)); 72 | handlers.forEach(function(h) { 73 | t.equal(typeof(h), 'function'); 74 | }); 75 | t.end(); 76 | }); 77 | 78 | 79 | test('handler chain append', function(t) { 80 | var handlers = backend.compare([ 81 | function foo(req, res, next) { 82 | return next(); 83 | } 84 | ]); 85 | t.ok(handlers); 86 | t.ok(Array.isArray(handlers)); 87 | handlers.forEach(function(h) { 88 | t.equal(typeof(h), 'function'); 89 | }); 90 | t.equal(handlers[1].name, 'foo'); 91 | t.end(); 92 | }); 93 | 94 | 95 | test('add fixtures', function(t) { 96 | var suffix = { 97 | objectClass: 'top', 98 | objectClass: 'organization', 99 | o: SUFFIX.split('=')[1], 100 | cn: 'foo' 101 | }; 102 | client.add(SUFFIX, suffix, function(err, res) { 103 | t.ifError(err); 104 | t.ok(res); 105 | t.equal(res.status, 0); 106 | t.end(); 107 | }); 108 | }); 109 | 110 | 111 | test('compare true', function(t) { 112 | client.compare(SUFFIX, 'cn', 'foo', function(err, matched) { 113 | t.ifError(err); 114 | t.ok(matched); 115 | t.end(); 116 | }); 117 | }); 118 | 119 | 120 | test('compare false', function(t) { 121 | client.compare(SUFFIX, 'cn', 'bar', function(err, equal) { 122 | t.ifError(err); 123 | t.equal(equal, false); 124 | t.end(); 125 | }); 126 | }); 127 | 128 | 129 | test('compare non-existent attribute', function(t) { 130 | client.compare(SUFFIX, uuid(), 'foo', function(err) { 131 | t.ok(err); 132 | t.ok(err instanceof ldap.NoSuchAttributeError); 133 | t.end(); 134 | }); 135 | }); 136 | 137 | 138 | test('compare non-existent entry', function(t) { 139 | client.compare('cn=child,' + SUFFIX, 'foo', 'bar', function(err) { 140 | t.ok(err); 141 | t.ok(err instanceof ldap.NoSuchObjectError); 142 | t.end(); 143 | }); 144 | }); 145 | 146 | 147 | test('teardown', function(t) { 148 | var riak = backend.client; 149 | var bucket = backend.bucket; 150 | 151 | function close() { 152 | client.unbind(function() { 153 | server.on('close', function() { 154 | t.end(); 155 | }); 156 | server.close(); 157 | }); 158 | } 159 | 160 | function removeUniqueIndexes() { 161 | var bucket = backend.uniqueIndexBucket.name; 162 | riak.list(bucket, function(err, keys) { 163 | if (keys && keys.length) { 164 | var finished = 0; 165 | keys.forEach(function(k) { 166 | riak.del(bucket, k, function(err) { 167 | if (++finished >= keys.length) { 168 | return close(); 169 | } 170 | }); 171 | }); 172 | } else { 173 | return close(); 174 | } 175 | }); 176 | } 177 | 178 | var bucket = backend.bucket.name; 179 | return riak.list(bucket, function(err, keys) { 180 | if (keys && keys.length) { 181 | var finished = 0; 182 | keys.forEach(function(k) { 183 | riak.del(bucket, k, function(err) { 184 | if (++finished >= keys.length) { 185 | return removeUniqueIndexes(); 186 | } 187 | }); 188 | }); 189 | } else { 190 | return removeUniqueIndexes(); 191 | } 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /tst/del.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var ldap = require('ldapjs'); 4 | var log4js = require('log4js'); 5 | var test = require('tap').test; 6 | var uuid = require('node-uuid'); 7 | 8 | 9 | 10 | ///--- Globals 11 | 12 | var SUFFIX = 'o=' + uuid(); 13 | var SOCKET = '/tmp/.' + uuid(); 14 | var TOTAL_ENTRIES = 2; 15 | 16 | var backend; 17 | var client; 18 | var server; 19 | 20 | 21 | ///--- Tests 22 | 23 | test('setup', function(t) { 24 | var riakjs = require('../lib/index'); 25 | t.ok(riakjs); 26 | t.ok(riakjs.createBackend); 27 | t.equal(typeof(riakjs.createBackend), 'function'); 28 | backend = riakjs.createBackend({ 29 | bucket: { 30 | name: uuid() 31 | }, 32 | uniqueIndexBucket: { 33 | name: uuid() 34 | }, 35 | changelogBucket: { 36 | name: uuid() 37 | }, 38 | indexes: { 39 | l: false, 40 | uid: true 41 | }, 42 | client: { 43 | url: 'http://localhost:8098', 44 | cache: { 45 | size: 100, 46 | age: 10 47 | } 48 | }, 49 | log4js: log4js 50 | }); 51 | t.ok(backend); 52 | t.ok(backend.add); 53 | t.equal(typeof(backend.add), 'function'); 54 | server = ldap.createServer({ 55 | log4js: log4js 56 | }); 57 | t.ok(server); 58 | 59 | server.add(SUFFIX, backend, backend.add()); 60 | server.del(SUFFIX, backend, backend.del()); 61 | 62 | server.listen(SOCKET, function() { 63 | client = ldap.createClient({ 64 | socketPath: SOCKET 65 | }); 66 | t.ok(client); 67 | t.end(); 68 | }); 69 | }); 70 | 71 | 72 | test('handler chain', function(t) { 73 | var handlers = backend.del(); 74 | t.ok(handlers); 75 | t.ok(Array.isArray(handlers)); 76 | handlers.forEach(function(h) { 77 | t.equal(typeof(h), 'function'); 78 | }); 79 | t.end(); 80 | }); 81 | 82 | 83 | test('handler chain append', function(t) { 84 | var handlers = backend.del([ 85 | function foo(req, res, next) { 86 | return next(); 87 | } 88 | ]); 89 | t.ok(handlers); 90 | t.ok(Array.isArray(handlers)); 91 | handlers.forEach(function(h) { 92 | t.equal(typeof(h), 'function'); 93 | }); 94 | t.equal(handlers[1].name, 'foo'); 95 | t.end(); 96 | }); 97 | 98 | 99 | test('add fixtures', function(t) { 100 | var suffix = { 101 | objectClass: 'top', 102 | objectClass: 'organization', 103 | o: SUFFIX.split('=')[1] 104 | }; 105 | client.add(SUFFIX, suffix, function(err, res) { 106 | t.ifError(err); 107 | t.ok(res); 108 | t.equal(res.status, 0); 109 | 110 | var finished = 0; 111 | for (var i = 0; i < TOTAL_ENTRIES; i++) { 112 | var entry = { 113 | cn: 'child' + i, 114 | objectClass: 'person', 115 | uid: uuid(), 116 | sn: 'test', 117 | l: i % 3 ? 'vancouver' : 'seattle' 118 | }; 119 | client.add('cn=child' + i + ',' + SUFFIX, entry, function(err, res) { 120 | t.ifError(err); 121 | t.ok(res); 122 | t.equal(res.status, 0); 123 | 124 | if (++finished === TOTAL_ENTRIES) 125 | t.end(); 126 | }); 127 | } 128 | }); 129 | }); 130 | 131 | 132 | test('delete ok', function(t) { 133 | client.del('cn=child1,' + SUFFIX, function(err) { 134 | t.ifError(err); 135 | t.end(); 136 | }); 137 | }); 138 | 139 | 140 | test('delete non-existent entry', function(t) { 141 | client.del('cn=child1,' + SUFFIX, function(err) { 142 | t.ok(err); 143 | t.ok(err instanceof ldap.NoSuchObjectError); 144 | t.end(); 145 | }); 146 | }); 147 | 148 | 149 | test('delete non-leaf entry', function(t) { 150 | client.del(SUFFIX, function(err) { 151 | t.ok(err); 152 | t.ok(err instanceof ldap.NotAllowedOnNonLeafError); 153 | t.end(); 154 | }); 155 | }); 156 | 157 | 158 | 159 | test('teardown', function(t) { 160 | function close() { 161 | client.unbind(function() { 162 | server.on('close', function() { 163 | t.end(); 164 | }); 165 | server.close(); 166 | }); 167 | } 168 | 169 | function cleanup(bucket) { 170 | riak.list(bucket, function(err, keys) { 171 | if (keys && keys.length) { 172 | var _finished = 0; 173 | return keys.forEach(function(k) { 174 | riak.del(bucket, k, function(err) { 175 | if (++_finished >= keys.length) { 176 | if (++finished === 3) 177 | return close(); 178 | } 179 | }); 180 | }); 181 | } 182 | 183 | if (++finished === 3) 184 | return close(); 185 | }); 186 | } 187 | 188 | var riak = backend.client; 189 | var finished = 0; 190 | cleanup(backend.bucket.name); 191 | cleanup(backend.changelogBucket.name); 192 | cleanup(backend.uniqueIndexBucket.name); 193 | }); 194 | -------------------------------------------------------------------------------- /tst/modify.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var ldap = require('ldapjs'); 4 | var log4js = require('log4js'); 5 | var test = require('tap').test; 6 | var uuid = require('node-uuid'); 7 | 8 | 9 | 10 | ///--- Globals 11 | 12 | var Attribute = ldap.Attribute; 13 | var Change = ldap.Change; 14 | 15 | var SUFFIX = 'o=' + uuid(); 16 | var SOCKET = '/tmp/.' + uuid(); 17 | 18 | var backend; 19 | var client; 20 | var server; 21 | 22 | 23 | 24 | ///--- Tests 25 | 26 | test('setup', function(t) { 27 | var riakjs = require('../lib/index'); 28 | t.ok(riakjs); 29 | t.ok(riakjs.createBackend); 30 | t.equal(typeof(riakjs.createBackend), 'function'); 31 | backend = riakjs.createBackend({ 32 | bucket: { 33 | name: uuid() 34 | }, 35 | uniqueIndexBucket: { 36 | name: uuid() 37 | }, 38 | changelogBucket: { 39 | name: uuid() 40 | }, 41 | indexes: { 42 | l: false, 43 | uid: true 44 | }, 45 | client: { 46 | url: 'http://localhost:8098', 47 | cache: { 48 | size: 100, 49 | age: 10 50 | } 51 | }, 52 | log4js: log4js 53 | }); 54 | t.ok(backend); 55 | t.ok(backend.add); 56 | t.equal(typeof(backend.add), 'function'); 57 | server = ldap.createServer({ 58 | log4js: log4js 59 | }); 60 | t.ok(server); 61 | 62 | server.add(SUFFIX, backend, backend.add()); 63 | server.modify(SUFFIX, backend, backend.modify()); 64 | 65 | server.listen(SOCKET, function() { 66 | client = ldap.createClient({ 67 | socketPath: SOCKET 68 | }); 69 | t.ok(client); 70 | t.end(); 71 | }); 72 | }); 73 | 74 | 75 | test('handler chain', function(t) { 76 | var handlers = backend.modify(); 77 | t.ok(handlers); 78 | t.ok(Array.isArray(handlers)); 79 | handlers.forEach(function(h) { 80 | t.equal(typeof(h), 'function'); 81 | }); 82 | t.end(); 83 | }); 84 | 85 | 86 | test('handler chain append', function(t) { 87 | var handlers = backend.modify([ 88 | function foo(req, res, next) { 89 | return next(); 90 | } 91 | ]); 92 | t.ok(handlers); 93 | t.ok(Array.isArray(handlers)); 94 | handlers.forEach(function(h) { 95 | t.equal(typeof(h), 'function'); 96 | }); 97 | t.equal(handlers[1].name, 'foo'); 98 | t.end(); 99 | }); 100 | 101 | 102 | test('add fixtures', function(t) { 103 | var suffix = { 104 | objectClass: 'top', 105 | objectClass: 'organization', 106 | o: SUFFIX.split('=')[1] 107 | }; 108 | client.add(SUFFIX, suffix, function(err, res) { 109 | t.ifError(err); 110 | t.ok(res); 111 | t.equal(res.status, 0); 112 | t.end(); 113 | }); 114 | }); 115 | 116 | 117 | test('modify add ok', function(t) { 118 | var change = new Change({ 119 | type: 'add', 120 | modification: { 121 | 'pets': ['honey badger', 'bear'] 122 | } 123 | }); 124 | client.modify(SUFFIX, change, function(err, res) { 125 | t.ifError(err); 126 | t.end(); 127 | }); 128 | }); 129 | 130 | 131 | test('modify replace ok', function(t) { 132 | var change = new Change({ 133 | type: 'replace', 134 | modification: new Attribute({ 135 | type: 'pets', 136 | vals: ['moose'] 137 | }) 138 | }); 139 | client.modify(SUFFIX, change, function(err, res) { 140 | t.ifError(err); 141 | t.end(); 142 | }); 143 | }); 144 | 145 | 146 | test('modify delete ok', function(t) { 147 | var change = new Change({ 148 | type: 'delete', 149 | modification: new Attribute({ 150 | type: 'pets' 151 | }) 152 | }); 153 | client.modify(SUFFIX, change, function(err, res) { 154 | t.ifError(err); 155 | t.end(); 156 | }); 157 | }); 158 | 159 | 160 | test('modify non-existent entry', function(t) { 161 | var change = new Change({ 162 | type: 'delete', 163 | modification: new Attribute({ 164 | type: 'pets' 165 | }) 166 | }); 167 | client.modify('cn=child1,' + SUFFIX, change, function(err) { 168 | t.ok(err); 169 | t.ok(err instanceof ldap.NoSuchObjectError); 170 | t.end(); 171 | }); 172 | }); 173 | 174 | 175 | test('teardown', function(t) { 176 | function close() { 177 | client.unbind(function() { 178 | server.on('close', function() { 179 | t.end(); 180 | }); 181 | server.close(); 182 | }); 183 | } 184 | 185 | function cleanup(bucket) { 186 | riak.list(bucket, function(err, keys) { 187 | if (keys && keys.length) { 188 | var _finished = 0; 189 | return keys.forEach(function(k) { 190 | riak.del(bucket, k, function(err) { 191 | if (++_finished >= keys.length) { 192 | if (++finished === 3) 193 | return close(); 194 | } 195 | }); 196 | }); 197 | } 198 | 199 | if (++finished === 3) 200 | return close(); 201 | }); 202 | } 203 | 204 | var riak = backend.client; 205 | var finished = 0; 206 | cleanup(backend.bucket.name); 207 | cleanup(backend.changelogBucket.name); 208 | cleanup(backend.uniqueIndexBucket.name); 209 | }); 210 | -------------------------------------------------------------------------------- /tst/persistent_search.test.js: -------------------------------------------------------------------------------- 1 | var ldap = require('ldapjs'); 2 | var log4js = require('log4js'); 3 | var test = require('tap').test; 4 | var uuid = require('node-uuid'); 5 | 6 | 7 | 8 | ///--- Globals 9 | 10 | var Attribute = ldap.Attribute; 11 | var Change = ldap.Change; 12 | 13 | var SUFFIX = 'o=' + uuid(); 14 | var SOCKET = '/tmp/.' + uuid(); 15 | var TOTAL_ENTRIES = 2; 16 | 17 | var backend; 18 | var addclient; 19 | var client; 20 | var server; 21 | 22 | var ctrl = new ldap.PersistentSearchControl({ 23 | type: '2.16.840.1.113730.3.4.3', 24 | value: { 25 | changeTypes: 15, 26 | changesOnly: false, 27 | returnECs: true 28 | } 29 | }); 30 | 31 | var addOnly = new ldap.PersistentSearchControl({ 32 | type: '2.16.840.1.113730.3.4.3', 33 | value: { 34 | changeTypes: 1, 35 | changesOnly: true, 36 | returnECs: true 37 | } 38 | }); 39 | 40 | var deleteOnly = new ldap.PersistentSearchControl({ 41 | type: '2.16.840.1.113730.3.4.3', 42 | value: { 43 | changeTypes: 2, 44 | changesOnly: true, 45 | returnECs: true 46 | } 47 | }); 48 | 49 | var modOnly = new ldap.PersistentSearchControl({ 50 | type: '2.16.840.1.113730.3.4.3', 51 | value: { 52 | changeTypes: 4, 53 | changesOnly: true, 54 | returnECs: true 55 | } 56 | }); 57 | 58 | var changesOnly = new ldap.PersistentSearchControl({ 59 | type: '2.16.840.1.113730.3.4.3', 60 | value: { 61 | changeTypes: 15, 62 | changesOnly: true, 63 | returnECs: true 64 | } 65 | }); 66 | 67 | 68 | 69 | ///--- Tests 70 | 71 | test('setup', function(t) { 72 | // log4js.setGlobalLogLevel('Warn'); 73 | var riakjs = require('../lib/index'); 74 | t.ok(riakjs); 75 | t.ok(riakjs.createBackend); 76 | t.equal(typeof(riakjs.createBackend), 'function'); 77 | backend = riakjs.createBackend({ 78 | bucket: { 79 | name: uuid() 80 | }, 81 | uniqueIndexBucket: { 82 | name: uuid() 83 | }, 84 | changelogBucket: { 85 | name: uuid() 86 | }, 87 | indexes: { 88 | l: false, 89 | uid: true 90 | }, 91 | client: { 92 | url: 'http://localhost:8098' 93 | }, 94 | log4js: log4js 95 | }); 96 | 97 | // backend.log.setLevel('Warn'); 98 | t.ok(backend); 99 | t.ok(backend.search); 100 | t.equal(typeof(backend.search), 'function'); 101 | server = ldap.createServer(); 102 | t.ok(server); 103 | 104 | server.add(SUFFIX, backend, backend.add()); 105 | server.modify(SUFFIX, backend, backend.modify()); 106 | server.search(SUFFIX, backend, backend.search()); 107 | server.search('cn=changelog', backend, backend.changelogSearch()); 108 | server.del(SUFFIX, backend, backend.del()); 109 | 110 | server.listen(SOCKET, function() { 111 | client = ldap.createClient({ 112 | socketPath: SOCKET 113 | }); 114 | t.ok(client); 115 | addclient = ldap.createClient({ 116 | socketPath: SOCKET 117 | }); 118 | t.ok(addclient); 119 | t.end(); 120 | }); 121 | }); 122 | 123 | 124 | test('add fixtures', function(t) { 125 | var suffix = { 126 | objectClass: ['top', 'organization'], 127 | o: SUFFIX.split('=')[1] 128 | }; 129 | addclient.add(SUFFIX, suffix, function(err, res) { 130 | t.ifError(err); 131 | t.ok(res); 132 | t.equal(res.status, 0); 133 | 134 | var finished = 0; 135 | for (var i = 0; i < TOTAL_ENTRIES; i++) { 136 | var entry = { 137 | cn: 'child' + i, 138 | objectClass: 'person', 139 | uid: uuid(), 140 | sn: 'test', 141 | l: i % 3 ? 'vancouver' : 'seattle' 142 | }; 143 | addclient.add('cn=child' + i + ',' + SUFFIX, entry, function(err, res) { 144 | t.ifError(err); 145 | if (err) { 146 | t.fail('error adding fixtures', err); 147 | } 148 | t.ok(res); 149 | t.equal(res.status, 0); 150 | console.log('add', finished); 151 | if (++finished === TOTAL_ENTRIES) { 152 | console.log('ending add fixtures'); 153 | setTimeout(function() { t.end(); }, 1000); 154 | } 155 | }); 156 | } 157 | }); 158 | }); 159 | 160 | 161 | test('persistent search', function(t) { 162 | console.log('entering search test'); 163 | // sub search on a child cn 164 | client.search('cn=child1,' + SUFFIX, {scope: 'sub'}, ctrl, 165 | function(err, res) { 166 | t.ifError(err); 167 | t.ok(res); 168 | var retrieved = 0; 169 | res.on('searchEntry', function(entry) { 170 | retrieved++; 171 | if (retrieved > 2) { 172 | t.fail('only two entries for child 1'); 173 | } 174 | t.ok(entry); 175 | t.ok(entry instanceof ldap.SearchEntry); 176 | t.ok(entry.dn.toString()); 177 | t.equal(entry.dn.toString(), 'cn=child1, ' + SUFFIX); 178 | t.ok(entry.attributes); 179 | t.ok(entry.attributes.length); 180 | t.ok(entry.object); 181 | 182 | if (retrieved === 2) { 183 | t.ok(entry.controls[0]); 184 | t.ok(entry.controls[0].value.changeNumber); 185 | t.equal(entry.controls[0].value.changeType, 4); 186 | } 187 | }); 188 | 189 | res.on('error', function(err) { 190 | t.fail('child1', err); 191 | }); 192 | res.on('end', function(res) { 193 | t.fail('server should not sever connection'); 194 | }); 195 | }); 196 | 197 | // search on changelog 198 | client.search('cn=changelog', { scope: 'sub'}, ctrl, function(err, res) { 199 | t.ifError(err); 200 | t.ok(res); 201 | var retrieved = 0; 202 | res.on('searchEntry', function(entry) { 203 | retrieved++; 204 | t.ok(entry); 205 | t.ok(entry instanceof ldap.SearchEntry); 206 | t.ok(entry.dn.toString()); 207 | t.ok(entry.attributes); 208 | t.ok(entry.attributes.length); 209 | t.ok(entry.object); 210 | }); 211 | res.on('error', function(err) { 212 | t.fail(err); 213 | }); 214 | res.on('end', function(res) { 215 | t.fail('server should not sever connection'); 216 | }); 217 | }); 218 | 219 | // search on suffix, return only dels 220 | client.search(SUFFIX, {scope: 'sub'}, deleteOnly, function(err, res) { 221 | t.ifError(err); 222 | t.ok(res); 223 | var retrieved = 0; 224 | res.on('searchEntry', function(entry) { 225 | retrieved++; 226 | t.ok(entry); 227 | t.ok(entry instanceof ldap.SearchEntry); 228 | t.ok(entry.dn.toString()); 229 | t.ok(entry.attributes); 230 | t.ok(entry.attributes.length); 231 | t.ok(entry.object); 232 | if (retrieved === 1) { 233 | t.equal(entry.dn.toString(), 'cn=yunong, ' + SUFFIX); 234 | t.ok(entry.controls[0]); 235 | t.ok(entry.controls[0].value.changeNumber); 236 | t.equal(entry.controls[0].value.changeType, 2); 237 | } 238 | 239 | if (retrieved > 1) { 240 | t.fail('should only have 1 responses'); 241 | } 242 | }); 243 | res.on('error', function(err) { 244 | t.fail(err); 245 | }); 246 | res.on('end', function(res) { 247 | t.fail('server should not sever connection'); 248 | }); 249 | }); 250 | 251 | // search on suffix, return only adds 252 | client.search(SUFFIX, {scope: 'sub'}, addOnly, function(err, res) { 253 | t.ifError(err); 254 | t.ok(res); 255 | var retrieved = 0; 256 | res.on('searchEntry', function(entry) { 257 | retrieved++; 258 | t.ok(entry); 259 | t.ok(entry instanceof ldap.SearchEntry); 260 | t.ok(entry.dn.toString()); 261 | t.ok(entry.attributes); 262 | t.ok(entry.attributes.length); 263 | t.ok(entry.object); 264 | if (retrieved === 1) { 265 | t.equal(entry.dn.toString(), 'cn=yunong, ' + SUFFIX); 266 | t.ok(entry.controls[0]); 267 | t.ok(entry.controls[0].value.changeNumber); 268 | t.equal(entry.controls[0].value.changeType, 1); 269 | } 270 | 271 | if (retrieved > 1) { 272 | t.fail('should only have 1 responses'); 273 | } 274 | }); 275 | res.on('error', function(err) { 276 | t.fail(err); 277 | }); 278 | res.on('end', function(res) { 279 | t.fail('server should not sever connection'); 280 | }); 281 | }); 282 | 283 | // search on suffix, return only mods 284 | client.search(SUFFIX, {scope: 'sub'}, modOnly, function(err, res) { 285 | t.ifError(err); 286 | t.ok(res); 287 | var retrieved = 0; 288 | res.on('searchEntry', function(entry) { 289 | retrieved++; 290 | t.ok(entry); 291 | t.ok(entry instanceof ldap.SearchEntry); 292 | t.ok(entry.dn.toString()); 293 | t.ok(entry.attributes); 294 | t.ok(entry.attributes.length); 295 | t.ok(entry.object); 296 | if (retrieved === 1) { 297 | t.equal(entry.dn.toString(), 'cn=child1, ' + SUFFIX); 298 | t.ok(entry.controls[0]); 299 | t.ok(entry.controls[0].value.changeNumber); 300 | t.equal(entry.controls[0].value.changeType, 4); 301 | } 302 | 303 | if (retrieved > 1) { 304 | t.fail('should only have 1 responses'); 305 | } 306 | }); 307 | res.on('error', function(err) { 308 | t.fail(err); 309 | }); 310 | res.on('end', function(res) { 311 | t.fail('server should not sever connection'); 312 | }); 313 | }); 314 | 315 | // base search on suffix 316 | client.search(SUFFIX, {scope: 'base'}, ctrl, function(err, res) { 317 | t.ifError(err); 318 | t.ok(res); 319 | var retrieved = 0; 320 | res.on('searchEntry', function(entry) { 321 | retrieved++; 322 | if (retrieved > 1) { 323 | t.fail('should only have 1 entry on base'); 324 | } 325 | t.ok(entry); 326 | t.ok(entry instanceof ldap.SearchEntry); 327 | t.ok(entry.dn.toString()); 328 | t.ok(entry.attributes); 329 | t.ok(entry.attributes.length); 330 | t.ok(entry.object); 331 | }); 332 | res.on('error', function(err) { 333 | t.fail(err); 334 | }); 335 | res.on('end', function(res) { 336 | t.fail('server should not sever connection'); 337 | }); 338 | }); 339 | 340 | 341 | // search on cn=child0, changes only while adding/deleting/modding 342 | client.search('cn=child0,' + SUFFIX, {scope: 'sub'}, changesOnly, 343 | function(err, res) { 344 | t.ifError(err); 345 | t.ok(res); 346 | res.on('searchEntry', function(entry) { 347 | t.fail('changesonly control should not fire'); 348 | }); 349 | res.on('error', function(err) { 350 | t.fail(err); 351 | }); 352 | res.on('end', function(res) { 353 | t.fail('server should not sever connection'); 354 | }); 355 | }); 356 | 357 | 358 | // search on suffix, changes only while adding/deleting/modding 359 | client.search(SUFFIX, {scope: 'sub'}, changesOnly, function(err, res) { 360 | t.ifError(err); 361 | t.ok(res); 362 | var retrieved = 0; 363 | res.on('searchEntry', function(entry) { 364 | retrieved++; 365 | console.log('changesonly all', retrieved); 366 | t.ok(entry); 367 | t.ok(entry instanceof ldap.SearchEntry); 368 | t.ok(entry.dn.toString()); 369 | t.ok(entry.attributes); 370 | t.ok(entry.attributes.length); 371 | t.ok(entry.object); 372 | if (retrieved === 1) { 373 | t.equal(entry.dn.toString(), 'cn=yunong, ' + SUFFIX); 374 | t.ok(entry.controls[0]); 375 | t.ok(entry.controls[0].value.changeNumber); 376 | t.equal(entry.controls[0].value.changeType, 1); 377 | } 378 | 379 | if (retrieved === 2) { 380 | t.equal(entry.dn.toString(), 'cn=child1, ' + SUFFIX); 381 | t.ok(entry.controls[0]); 382 | t.equal(entry.controls[0].value.changeType, 4); 383 | } 384 | 385 | if (retrieved === 3) { 386 | t.equal(entry.dn.toString(), 'cn=yunong, ' + SUFFIX); 387 | t.ok(entry.controls[0]); 388 | t.ok(entry.controls[0].value.changeNumber); 389 | t.equal(entry.controls[0].value.changeType, 2); 390 | } 391 | 392 | if (retrieved > 3) { 393 | console.log('more than 4'); 394 | t.fail('should only have 3 responses'); 395 | t.end(); 396 | } 397 | }); 398 | res.on('error', function(err) { 399 | t.fail(err); 400 | }); 401 | res.on('end', function(res) { 402 | t.fail('server should not sever connection'); 403 | }); 404 | }); 405 | 406 | // search on suffix while adding/deleting/modding 407 | client.search(SUFFIX, {scope: 'sub'}, ctrl, function(err, res) { 408 | t.ifError(err); 409 | t.ok(res); 410 | var retrieved = 0; 411 | res.on('searchEntry', function(entry) { 412 | retrieved++; 413 | t.ok(entry); 414 | t.ok(entry instanceof ldap.SearchEntry); 415 | t.ok(entry.dn.toString()); 416 | t.ok(entry.attributes); 417 | t.ok(entry.attributes.length); 418 | t.ok(entry.object); 419 | if (retrieved === TOTAL_ENTRIES + 2) { 420 | t.equal(entry.dn.toString(), 'cn=yunong, ' + SUFFIX); 421 | t.ok(entry.controls[0]); 422 | t.ok(entry.controls[0].value.changeNumber); 423 | t.equal(entry.controls[0].value.changeType, 1); 424 | } 425 | 426 | if (retrieved === TOTAL_ENTRIES + 3) { 427 | t.equal(entry.dn.toString(), 'cn=child1, ' + SUFFIX); 428 | t.ok(entry.controls[0]); 429 | t.equal(entry.controls[0].value.changeType, 4); 430 | } 431 | 432 | if (retrieved === TOTAL_ENTRIES + 4) { 433 | t.equal(entry.dn.toString(), 'cn=yunong, ' + SUFFIX); 434 | t.ok(entry.controls[0]); 435 | t.ok(entry.controls[0].value.changeNumber); 436 | t.equal(entry.controls[0].value.changeType, 2); 437 | t.end(); 438 | } 439 | }); 440 | res.on('error', function(err) { 441 | t.fail(err); 442 | if (err) { 443 | console.log('error', err); 444 | } 445 | }); 446 | res.on('end', function(res) { 447 | t.fail('server should not sever connection'); 448 | }); 449 | }); 450 | 451 | var entry = { 452 | cn: 'yunong', 453 | objectClass: 'person', 454 | uid: uuid(), 455 | sn: 'test', 456 | l: 'seattle' 457 | }; 458 | 459 | var change = new Change({ 460 | type: 'add', 461 | modification: { 462 | 'pets': ['honey badger', 'bear'] 463 | } 464 | }); 465 | 466 | addclient.add('cn=yunong,' + SUFFIX, entry, function(err, res) { 467 | t.ifError(err); 468 | if (err) 469 | t.fail(err); 470 | 471 | addclient.modify('cn=child1,' + SUFFIX, change, function(err, res) { 472 | t.ifError(err); 473 | if (err) 474 | t.fail(err); 475 | 476 | addclient.del('cn=yunong,' + SUFFIX, function(err) { 477 | t.ifError(err); 478 | if (err) 479 | t.fail(err); 480 | }); 481 | }); 482 | }); 483 | }); 484 | 485 | 486 | test('teardown', function(t) { 487 | function close() { 488 | client.unbind(function() { 489 | addclient.unbind(function() { 490 | server.on('close', function() { 491 | console.log('closing server'); 492 | t.end(); 493 | }); 494 | server.close(); 495 | }); 496 | }); 497 | } 498 | 499 | function cleanup(bucket) { 500 | riak.list(bucket, function(err, keys) { 501 | if (keys && keys.length) { 502 | var _finished = 0; 503 | return keys.forEach(function(k) { 504 | riak.del(bucket, k, function(err) { 505 | if (++_finished >= keys.length) { 506 | if (++finished === 3) 507 | return close(); 508 | } 509 | }); 510 | }); 511 | } 512 | 513 | if (++finished === 3) 514 | return close(); 515 | }); 516 | } 517 | 518 | var riak = backend.client; 519 | var finished = 0; 520 | cleanup(backend.bucket.name); 521 | cleanup(backend.changelogBucket.name); 522 | cleanup(backend.uniqueIndexBucket.name); 523 | }); 524 | -------------------------------------------------------------------------------- /tst/riak.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var qs = require('querystring'); 4 | var log4js = require('log4js'); 5 | var test = require('tap').test; 6 | var uuid = require('node-uuid'); 7 | 8 | var Riak = require('../lib/riak'); 9 | 10 | 11 | 12 | ///--- Globals 13 | 14 | var client; 15 | var bucket = uuid(); 16 | var key = uuid(); 17 | 18 | 19 | 20 | ///--- Test 21 | 22 | test('setup', function(t) { 23 | log4js.setGlobalLogLevel('TRACE'); 24 | client = new Riak({ 25 | log4js: log4js, 26 | cache: { 27 | size: 100, 28 | age: 30 29 | } 30 | }); 31 | t.ok(client); 32 | t.end(); 33 | }); 34 | 35 | 36 | test('constructor no options', function(t) { 37 | var _client = new Riak(); 38 | t.ok(_client); 39 | t.end(); 40 | }); 41 | 42 | 43 | test('ListBuckets', function(t) { 44 | client.listBuckets(function(err, buckets) { 45 | t.ifError(err); 46 | t.ok(buckets); 47 | t.end(); 48 | }); 49 | }); 50 | 51 | 52 | test('SetBucket', function(t) { 53 | client.setBucket(uuid(), { n_val: 1 }, function(err) { 54 | t.ifError(err); 55 | t.end(); 56 | }); 57 | }); 58 | 59 | 60 | test('ListKeys', function(t) { 61 | client.list(uuid(), function(err, buckets) { 62 | t.ifError(err); 63 | t.ok(buckets); 64 | t.end(); 65 | }); 66 | }); 67 | 68 | 69 | test('GetObject (404)', function(t) { 70 | client.get(bucket, uuid(), function(err, object) { 71 | t.ok(err); 72 | t.equal(err.name, 'NotFoundError'); 73 | t.ok(!object); 74 | t.end(); 75 | }); 76 | }); 77 | 78 | 79 | test('StoreObject (with key)', function(t) { 80 | client.put(bucket, key, {foo: 'bar'}, function(err, key, headers) { 81 | t.ifError(err); 82 | t.ok(key); 83 | t.end(); 84 | }); 85 | }); 86 | 87 | 88 | test('FetchObject', function(t) { 89 | client.get(bucket, key, function(err, obj, headers) { 90 | t.ifError(err); 91 | t.ok(obj); 92 | t.equal(obj.foo, 'bar'); 93 | t.end(); 94 | }); 95 | }); 96 | 97 | 98 | test('DeleteObject', function(t) { 99 | client.del(bucket, key, function(err, headers) { 100 | t.ifError(err); 101 | t.ok(headers); 102 | t.end(); 103 | }); 104 | }); 105 | 106 | 107 | test('GetObject (deleted)', function(t) { 108 | client.get(bucket, key, function(err, object) { 109 | t.ok(err); 110 | t.equal(err.name, 'NotFoundError'); 111 | t.ok(!object); 112 | t.end(); 113 | }); 114 | }); 115 | 116 | 117 | test('StoreObject (no key, indexes)', function(t) { 118 | var obj = { 119 | foo: 'bar', 120 | email: 'foo@bar.com' 121 | }; 122 | var opts = { 123 | indexes: ['email'] 124 | }; 125 | client.put(bucket, key, obj, opts, function(err, key, headers) { 126 | t.ifError(err); 127 | t.ok(key); 128 | var obj2 = { 129 | foo: 'car', 130 | email: 'foo@car.com' 131 | }; 132 | client.post(bucket, obj2, opts, function(err, key, headers) { 133 | t.ifError(err); 134 | t.ok(key); 135 | t.end(); 136 | }); 137 | }); 138 | }); 139 | 140 | 141 | test('Find by index', function(t) { 142 | var val = qs.escape('foo@car.com'); 143 | client.find(bucket, 'email', val, function(err, objects) { 144 | t.ifError(err); 145 | t.ok(objects); 146 | t.ok(objects.length); 147 | t.ok(objects[0]); 148 | t.equal(objects[0].email, 'foo@car.com'); 149 | t.end(); 150 | }); 151 | }); 152 | 153 | 154 | test('map reduce', function(t) { 155 | client.mapred( 156 | { 157 | bucket: bucket, 158 | key_filters: [['matches', '.*']] 159 | }, 160 | [ 161 | { 162 | reduce: { 163 | language: 'erlang', 164 | module: 'riak_kv_mapreduce', 165 | 'function': 'reduce_identity', 166 | 'keep': true 167 | } 168 | } 169 | ], 170 | function(err, stuff) { 171 | t.ifError(err); 172 | t.ok(stuff); 173 | t.end(); 174 | }); 175 | }); 176 | 177 | 178 | 179 | test('tear down (via listKeys -> Delete)', function(t) { 180 | client.listKeys(bucket, function(err, keys, headers) { 181 | t.ifError(err); 182 | t.ok(keys); 183 | 184 | var finished = 0; 185 | return keys.forEach(function(k) { 186 | return client.del(bucket, k, function(err) { 187 | t.ifError(err); 188 | if (++finished === keys.length) 189 | t.end(); 190 | }); 191 | }); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /tst/search.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | var ldap = require('ldapjs'); 4 | var log4js = require('log4js'); 5 | var test = require('tap').test; 6 | var uuid = require('node-uuid'); 7 | 8 | 9 | 10 | ///--- Globals 11 | 12 | var SUFFIX = 'o=' + uuid(); 13 | var SOCKET = '/tmp/.' + uuid(); 14 | var TOTAL_ENTRIES = 100; 15 | 16 | var backend; 17 | var client; 18 | var server; 19 | 20 | 21 | ///--- Tests 22 | 23 | test('setup', function(t) { 24 | var riakjs = require('../lib/index'); 25 | t.ok(riakjs); 26 | t.ok(riakjs.createBackend); 27 | t.equal(typeof(riakjs.createBackend), 'function'); 28 | backend = riakjs.createBackend({ 29 | bucket: { 30 | name: uuid() 31 | }, 32 | uniqueIndexBucket: { 33 | name: uuid() 34 | }, 35 | changelogBucket: { 36 | name: uuid() 37 | }, 38 | indexes: { 39 | l: false, 40 | uid: true 41 | }, 42 | client: { 43 | url: 'http://localhost:8098', 44 | cache: { 45 | size: 100, 46 | age: 20 47 | } 48 | }, 49 | log4js: log4js 50 | }); 51 | t.ok(backend); 52 | t.ok(backend.search); 53 | t.equal(typeof(backend.search), 'function'); 54 | server = ldap.createServer(); 55 | t.ok(server); 56 | 57 | server.add(SUFFIX, backend, backend.add()); 58 | server.search(SUFFIX, backend, backend.search()); 59 | 60 | server.listen(SOCKET, function() { 61 | client = ldap.createClient({ 62 | socketPath: SOCKET 63 | }); 64 | t.ok(client); 65 | t.end(); 66 | }); 67 | }); 68 | 69 | 70 | test('handler chain', function(t) { 71 | var handlers = backend.search(); 72 | t.ok(handlers); 73 | t.ok(Array.isArray(handlers)); 74 | handlers.forEach(function(h) { 75 | t.equal(typeof(h), 'function'); 76 | }); 77 | t.end(); 78 | }); 79 | 80 | 81 | test('handler chain append', function(t) { 82 | var handlers = backend.search([ 83 | function foo(req, res, next) { 84 | return next(); 85 | } 86 | ]); 87 | t.ok(handlers); 88 | t.ok(Array.isArray(handlers)); 89 | handlers.forEach(function(h) { 90 | t.equal(typeof(h), 'function'); 91 | }); 92 | t.equal(handlers[1].name, 'foo'); 93 | t.end(); 94 | }); 95 | 96 | 97 | test('add fixtures', function(t) { 98 | var suffix = { 99 | objectClass: 'top', 100 | objectClass: 'organization', 101 | o: SUFFIX.split('=')[1] 102 | }; 103 | client.add(SUFFIX, suffix, function(err, res) { 104 | t.ifError(err); 105 | t.ok(res); 106 | t.equal(res.status, 0); 107 | 108 | var finished = 0; 109 | for (var i = 0; i < TOTAL_ENTRIES; i++) { 110 | var entry = { 111 | cn: 'child' + i, 112 | objectClass: 'person', 113 | uid: uuid(), 114 | sn: 'test', 115 | l: i % 3 ? 'vancouver' : 'seattle' 116 | }; 117 | client.add('cn=child' + i + ',' + SUFFIX, entry, function(err, res) { 118 | t.ifError(err); 119 | t.ok(res); 120 | t.equal(res.status, 0); 121 | 122 | if (++finished === TOTAL_ENTRIES) 123 | t.end(); 124 | }); 125 | } 126 | }); 127 | }); 128 | 129 | 130 | test('search base objectclass=*', function(t) { 131 | client.search('cn=child1,' + SUFFIX, function(err, res) { 132 | t.ifError(err); 133 | t.ok(res); 134 | 135 | var retrieved = 0; 136 | res.on('searchEntry', function(entry) { 137 | t.ok(entry); 138 | t.ok(entry instanceof ldap.SearchEntry); 139 | t.equal(entry.dn.toString(), 'cn=child1, ' + SUFFIX); 140 | t.ok(entry.attributes); 141 | t.ok(entry.attributes.length); 142 | t.ok(entry.object); 143 | var hasChangenumber = false; 144 | entry.attributes.forEach(function(element) { 145 | if (element.type == 'changenumber' && element.vals) 146 | hasChangenumber = true; 147 | }); 148 | t.equal(hasChangenumber, true); 149 | retrieved++; 150 | }); 151 | res.on('error', function(err) { 152 | t.fail(err); 153 | }); 154 | res.on('end', function(res) { 155 | t.ok(res); 156 | t.ok(res instanceof ldap.SearchResponse); 157 | t.equal(res.status, 0); 158 | t.equal(retrieved, 1); 159 | t.end(); 160 | }); 161 | }); 162 | }); 163 | 164 | 165 | test('search base eq filter ok', function(t) { 166 | client.search('cn=child1,' + SUFFIX, '(cn=child1)', function(err, res) { 167 | t.ifError(err); 168 | t.ok(res); 169 | 170 | var retrieved = 0; 171 | res.on('searchEntry', function(entry) { 172 | t.ok(entry); 173 | t.ok(entry instanceof ldap.SearchEntry); 174 | t.equal(entry.dn.toString(), 'cn=child1, ' + SUFFIX); 175 | t.ok(entry.attributes); 176 | t.ok(entry.attributes.length); 177 | t.ok(entry.object); 178 | var hasChangenumber = false; 179 | entry.attributes.forEach(function(element) { 180 | if (element.type == 'changenumber' && element.vals) 181 | hasChangenumber = true; 182 | }); 183 | t.equal(hasChangenumber, true); 184 | retrieved++; 185 | }); 186 | res.on('error', function(err) { 187 | t.fail(err); 188 | }); 189 | res.on('end', function(res) { 190 | t.ok(res); 191 | t.ok(res instanceof ldap.SearchResponse); 192 | t.equal(res.status, 0); 193 | t.equal(retrieved, 1); 194 | t.end(); 195 | }); 196 | }); 197 | }); 198 | 199 | 200 | test('search base eq filter no match', function(t) { 201 | client.search('cn=child1,' + SUFFIX, '(cn=child2)', function(err, res) { 202 | t.ifError(err); 203 | t.ok(res); 204 | 205 | res.on('searchEntry', function(entry) { 206 | t.fail('Got an entry, but shouldn\'t have'); 207 | }); 208 | res.on('error', function(err) { 209 | t.fail(err); 210 | }); 211 | res.on('end', function(res) { 212 | t.ok(res); 213 | t.ok(res instanceof ldap.SearchResponse); 214 | t.equal(res.status, 0); 215 | t.end(); 216 | }); 217 | }); 218 | }); 219 | 220 | 221 | test('search sub filter ok', function(t) { 222 | var opts = { 223 | filter: '(cn=child*)', 224 | scope: 'sub' 225 | }; 226 | client.search(SUFFIX, opts, function(err, res) { 227 | t.ifError(err); 228 | t.ok(res); 229 | 230 | var retrieved = 0; 231 | res.on('searchEntry', function(entry) { 232 | t.ok(entry); 233 | t.ok(entry instanceof ldap.SearchEntry); 234 | t.ok(entry.dn.toString()); 235 | t.ok(entry.attributes); 236 | t.ok(entry.attributes.length); 237 | t.ok(entry.object); 238 | var hasChangenumber = false; 239 | entry.attributes.forEach(function(element) { 240 | if (element.type == 'changenumber' && element.vals) 241 | hasChangenumber = true; 242 | }); 243 | t.equal(hasChangenumber, true); 244 | retrieved++; 245 | }); 246 | res.on('error', function(err) { 247 | t.fail(err); 248 | }); 249 | res.on('end', function(res) { 250 | t.ok(res); 251 | t.ok(res instanceof ldap.SearchResponse); 252 | t.equal(res.status, 0); 253 | t.equal(retrieved, TOTAL_ENTRIES); // suffix doesn't match 254 | t.end(); 255 | }); 256 | }); 257 | }); 258 | 259 | 260 | test('search sub wrong base', function(t) { 261 | var opts = { 262 | filter: '(cn=*)', 263 | scope: 'sub' 264 | }; 265 | client.search('cn=foo,' + SUFFIX, opts, function(err, res) { 266 | t.ifError(err); 267 | t.ok(res); 268 | 269 | res.on('searchEntry', function(entry) { 270 | t.fail('Got an entry, but shouldn\'t have'); 271 | }); 272 | res.on('error', function(err) { 273 | t.fail(err); 274 | }); 275 | res.on('end', function(res) { 276 | t.ok(res); 277 | t.ok(res instanceof ldap.SearchResponse); 278 | t.equal(res.status, 0); 279 | t.end(); 280 | }); 281 | }); 282 | }); 283 | 284 | 285 | test('search sub filter no match', function(t) { 286 | var opts = { 287 | filter: '(foo=bar)', 288 | scope: 'sub' 289 | }; 290 | client.search(SUFFIX, opts, function(err, res) { 291 | t.ifError(err); 292 | t.ok(res); 293 | 294 | res.on('searchEntry', function(entry) { 295 | t.fail('Got an entry, but shouldn\'t have'); 296 | }); 297 | res.on('error', function(err) { 298 | t.fail(err); 299 | }); 300 | res.on('end', function(res) { 301 | t.ok(res); 302 | t.ok(res instanceof ldap.SearchResponse); 303 | t.equal(res.status, 0); 304 | t.end(); 305 | }); 306 | }); 307 | }); 308 | 309 | 310 | test('search sub ge filter ok', function(t) { 311 | var opts = { 312 | scope: 'sub', 313 | filter: '(cn>=child9)' 314 | }; 315 | client.search(SUFFIX, opts, function(err, res) { 316 | t.ifError(err); 317 | t.ok(res); 318 | 319 | var retrieved = 0; 320 | res.on('searchEntry', function(entry) { 321 | t.ok(entry); 322 | t.ok(entry instanceof ldap.SearchEntry); 323 | t.ok(entry.dn.toString()); 324 | t.ok(entry.attributes); 325 | t.ok(entry.attributes.length); 326 | t.ok(entry.object); 327 | var hasChangenumber = false; 328 | entry.attributes.forEach(function(element) { 329 | if (element.type == 'changenumber' && element.vals) 330 | hasChangenumber = true; 331 | }); 332 | t.equal(hasChangenumber, true); 333 | retrieved++; 334 | }); 335 | res.on('error', function(err) { 336 | t.fail(err); 337 | }); 338 | res.on('end', function(res) { 339 | t.ok(res); 340 | t.ok(res instanceof ldap.SearchResponse); 341 | t.equal(res.status, 0); 342 | t.equal(retrieved, 11); 343 | t.end(); 344 | }); 345 | }); 346 | }); 347 | 348 | 349 | test('search sub le filter ok', function(t) { 350 | var opts = { 351 | scope: 'sub', 352 | filter: '(cn<=child19)' 353 | }; 354 | client.search(SUFFIX, opts, function(err, res) { 355 | t.ifError(err); 356 | t.ok(res); 357 | 358 | var retrieved = 0; 359 | res.on('searchEntry', function(entry) { 360 | t.ok(entry); 361 | t.ok(entry instanceof ldap.SearchEntry); 362 | console.log(entry.dn.toString()); 363 | t.ok(entry.dn.toString()); 364 | t.ok(entry.attributes); 365 | t.ok(entry.attributes.length); 366 | t.ok(entry.object); 367 | var hasChangenumber = false; 368 | entry.attributes.forEach(function(element) { 369 | if (element.type == 'changenumber' && element.vals) 370 | hasChangenumber = true; 371 | }); 372 | t.equal(hasChangenumber, true); 373 | retrieved++; 374 | }); 375 | res.on('error', function(err) { 376 | t.fail(err); 377 | }); 378 | res.on('end', function(res) { 379 | t.ok(res); 380 | t.ok(res instanceof ldap.SearchResponse); 381 | t.equal(res.status, 0); 382 | t.equal(retrieved, 12); 383 | t.end(); 384 | }); 385 | }); 386 | }); 387 | 388 | 389 | test('search sub and filter ok', function(t) { 390 | var opts = { 391 | scope: 'sub', 392 | filter: '(&(cn>=child19)(sn=test))' 393 | }; 394 | client.search(SUFFIX, opts, function(err, res) { 395 | t.ifError(err); 396 | t.ok(res); 397 | 398 | var retrieved = 0; 399 | res.on('searchEntry', function(entry) { 400 | t.ok(entry); 401 | t.ok(entry instanceof ldap.SearchEntry); 402 | t.ok(entry.dn.toString()); 403 | t.ok(entry.attributes); 404 | t.ok(entry.attributes.length); 405 | t.ok(entry.object); 406 | var hasChangenumber = false; 407 | entry.attributes.forEach(function(element) { 408 | if (element.type == 'changenumber' && element.vals) 409 | hasChangenumber = true; 410 | }); 411 | t.equal(hasChangenumber, true); 412 | retrieved++; 413 | }); 414 | res.on('error', function(err) { 415 | t.fail(err); 416 | }); 417 | res.on('end', function(res) { 418 | t.ok(res); 419 | t.ok(res instanceof ldap.SearchResponse); 420 | t.equal(res.status, 0); 421 | t.equal(retrieved, TOTAL_ENTRIES - 11); 422 | t.end(); 423 | }); 424 | }); 425 | }); 426 | 427 | 428 | test('search sub or filter ok', function(t) { 429 | var opts = { 430 | scope: 'sub', 431 | filter: '(|(cn>=child19)(sn=t*s*))' 432 | }; 433 | client.search(SUFFIX, opts, function(err, res) { 434 | t.ifError(err); 435 | t.ok(res); 436 | 437 | var retrieved = 0; 438 | res.on('searchEntry', function(entry) { 439 | t.ok(entry); 440 | t.ok(entry instanceof ldap.SearchEntry); 441 | t.ok(entry.dn.toString()); 442 | t.ok(entry.attributes); 443 | t.ok(entry.attributes.length); 444 | t.ok(entry.object); 445 | var hasChangenumber = false; 446 | entry.attributes.forEach(function(element) { 447 | if (element.type == 'changenumber' && element.vals) 448 | hasChangenumber = true; 449 | }); 450 | t.equal(hasChangenumber, true); 451 | retrieved++; 452 | }); 453 | res.on('error', function(err) { 454 | t.fail(err); 455 | }); 456 | res.on('end', function(res) { 457 | t.ok(res); 458 | t.ok(res instanceof ldap.SearchResponse); 459 | t.equal(res.status, 0); 460 | t.equal(retrieved, TOTAL_ENTRIES); 461 | t.end(); 462 | }); 463 | }); 464 | }); 465 | 466 | 467 | test('teardown', function(t) { 468 | function close() { 469 | client.unbind(function() { 470 | server.on('close', function() { 471 | t.end(); 472 | }); 473 | server.close(); 474 | }); 475 | } 476 | 477 | function cleanup(bucket) { 478 | riak.list(bucket, function(err, keys) { 479 | if (keys && keys.length) { 480 | var _finished = 0; 481 | return keys.forEach(function(k) { 482 | riak.del(bucket, k, function(err) { 483 | if (++_finished >= keys.length) { 484 | if (++finished === 2) 485 | return close(); 486 | } 487 | }); 488 | }); 489 | } 490 | 491 | if (++finished === 3) 492 | return close(); 493 | }); 494 | } 495 | 496 | var riak = backend.client; 497 | var finished = 0; 498 | cleanup(backend.bucket.name); 499 | cleanup(backend.uniqueIndexBucket.name); 500 | }); 501 | --------------------------------------------------------------------------------