├── .gitignore ├── LICENSE ├── README.md ├── covercouch.js ├── cvr ├── acl.js ├── config.js ├── ddoc.js ├── lib.js ├── listreduce.js ├── logger.js ├── rater.js ├── restmap.js ├── router.js └── worker.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node-modules/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ermouth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Cover Couch 0.1β 3 | 4 | CoverCouch implements per-document r/w/d ACL for CouchDB. CoverCouch acts as proxy – original CouchDB REST API kept untouched, but all requests to Couch – r/w/d, \_changes feed, \_view, \_update, \_list or other fn call, replication – *everything* is filtered. 5 | 6 | Document ACL is defined using `creator`,`owners` and `acl` properties of a doc. Their values, combined by `_design/acl/_view/acl` view function, reflect final ACL for a doc. 7 | 8 | Also CoverCouch implements per-method fine-grained ACL – some paths like `_update/someFnName` can be restricted for several roles or users. CoverCouch can even restrict on query basis – for example we can allow `attachments=true` only for several roles. 9 | 10 | All these rules, ACL view function and other ACL-related stuff are stored in `_design/acl` design doc. This ddoc defines access rules for particular CouchDB bucket. 11 | 12 | Buckets that have no ACL ddoc, behave as native CouchDB. 13 | 14 | Other CoverCouch features: 15 | 16 | * multi-worker, workers are independent, 17 | * has rate-locker, rejects excessive activity early, 18 | * very fast – atomic ACL resolve is sync and takes <10µs, 19 | * non-polling replies without attaches are gzipped in most cases, 20 | * docs can inherit ACL from parent docs 21 | * syncs with other CouchDBs and PouchDBs. 22 | 23 | Special note: reduce and \_list work fine, since they are emulated and ingest only filtered \_view feeds. 24 | 25 | ## Quick start 26 | 27 | CoverCouch 0.1 is standalone app, it’s not a module right now. To install and run CoverCouch: 28 | 29 | * CouchDB 1.6–1.7 and node.js 0.10.35+ required, never tested with Couch 2.x 30 | * `$ git clone git://github.com/ermouth/covercouch.git folderName` 31 | * `$ cd folderName` 32 | * `$ npm install` 33 | * Edit general settings in `/cvr/config.js` 34 | * Run `$ node covercouch` 35 | 36 | For buckets listed in `couch.preload` section of `/cvr/config.js`, design docs `_design/acl` are created automatically (if no present). Default ddoc template is located in `/cvr/ddoc.js`. 37 | 38 | Now you have CouchDB wrapped with r/w/d ACL. 39 | 40 | ## Per-document ACL 41 | 42 | __Below text describes ACL behavior with default `_design/acl` ddoc. You can write your own implementation of it.__ 43 | 44 | Per-document ACL is defined using `creator`,`owners` and `acl` properties of a doc. Also its `parent` property may point to ‘parent’ doc – in this case ACL is inherited from parent, if any. 45 | 46 | All these properties are optional. If the first three are skipped, doc assumed to be free for r/w/d by any bucket user. 47 | 48 | ### `doc.creator` string 49 | 50 | Format is `"userName"` or `"u-userName"`. User, that can perform any action with the doc, if op requested was not restricted on path basis. 51 | 52 | Creator, once set, can not be changed by non-admins. Non-admin can not set `creator` for new doc other than himself. 53 | 54 | ### `doc.owners` array 55 | 56 | List of users and roles, who have very same permissions as creator, but they can not: 57 | 58 | * delete the doc, 59 | * modify `creator` and `owners` properties. 60 | 61 | This property must look like `["u-userName1", "r-role1", "r-role2", "u-userName2", ...]`. 62 | 63 | ### `doc.acl` array 64 | 65 | List of users and roles, that can read doc or attaches and call `_update` functions for the doc, that are not restricted on path basis. Format is same to `owners`. 66 | 67 | ### `doc.parent` string 68 | 69 | Pointer from ‘child’ doc to its ‘parent’, `_id` of ‘parent’ doc. Parent ACL is superimposed with doc ACL, the most permissive rules win. 70 | 71 | Useful for comment-like docs – they may inherit ACL from parent post. Changes in parent ACL modify resulting access rules of children without changing child docs themselves. 72 | 73 | ### Example docs 74 | ```` 75 | { 76 | "_id": "123abc", "_rev": "1-abcd", 77 | "type": "message", 78 | "creator": "u-mom", 79 | "owners": ["u-dad"], 80 | "acl": ["r-Johnsons", "u-kitchener"], 81 | "body": "What about summer fence? Ain’t it too early?" 82 | } 83 | -- 84 | { 85 | "_id": "234def", "_rev": "1-7390", 86 | "type": "comment", 87 | "creator": "u-jim", 88 | "parent": "123abc", 89 | "body": "Ok, unboxed it." 90 | } 91 | ```` 92 | ### Important edge case 93 | 94 | __Please note, that `_update/function/docid` requests are validated using READ document permissions, not WRITE.__ 95 | 96 | Updates assumed safe – in general they change only several properties of the doc and control values received. Access to \_update functions themselves can be limited using per-bucket restrictions. 97 | 98 | This combination allow readers, for example, mark doc as read or add some other data to doc using appropriate \_update. Compared with general ‘write document’, that can totally destruct the doc, \_update functions modify docs in controllable way. 99 | 100 | Choice between r-w-u-d and r-w-d was made when I analyzed how real sets of these permissions might look like. In nearly every case read permissions were equal to update permissions – so special set of update permissions was removed. 101 | 102 | 103 | ## Per-bucket ACL and restrictions 104 | 105 | Design doc `_design/acl` may have properties `restrict` and/or `dbacl`: 106 | 107 | * Object `restrict` allow to fine-tune permissions for particular CouchDB REST functions. 108 | * Object `dbacl` is superimposed with any doc-defined ACL during access rights resolution. 109 | 110 | Example: 111 | 112 | ```` 113 | { 114 | "_id": "_design/acl", "_rev": "1-2345", 115 | "views":{"acl":{"map":"function(doc){...}"}}, 116 | "acl": [], 117 | "restrict":{ 118 | "*": ["r-marketing", "r-sales", "u-boss", "u-cfo"], 119 | "get":{ 120 | "*attachments=true": ["u-cfo"] 121 | }, 122 | "post":{ 123 | "*attachments=true": ["u-cfo"] 124 | "_update/approveBudget": ["u-cfo"] 125 | }, 126 | "put":{ 127 | "*": [] 128 | } 129 | }, 130 | "dbacl":{ 131 | "_r": ["u-cfo", "u-boss"], 132 | "_w": ["u-boss"] 133 | } 134 | } 135 | ```` 136 | Array `restrict.*` have special meaning – it restricts users and roles, that have access to the bucket. Main difference between CouchDB security object and `restrict.*` is that buckets, inaccessible for user, are eliminated from `/_all_dbs` reply. 137 | 138 | Objects `restrict.get`, `restrict.post` and so on limit access to particular CouchDB API functions. Their keys are path fragments. Two wildcards are possible for keys: 139 | 140 | * `*` is one or more characters; 141 | * `+` is one or more characters, other than `/`. 142 | 143 | Above example ddoc’s `restrict` means that: 144 | 145 | * only marketing and sales depts, boss and CFO see this bucket; 146 | * no one (except admins, surely) can put doc or attach into bucket directly; 147 | * only CFO can call `approveBudget` update function (from unspecified ddoc); 148 | * only CFO can fetch data with attaches included. 149 | 150 | Example `dbacl` property means, that CFO and boss can read any doc from bucket regardless of rules in per-doc ACL. Boss also can write into any doc. 151 | 152 | Properties `acl`, `creator` and `owners`, defined for design doc, only restrict access to ddoc itself, it’s body and attaches, not to functions it expose. Above example ddoc is marked invisible for all users except admins with `"acl":[]`. 153 | 154 | ## How request is processed 155 | 156 | Generally, request is processed by several middlewares. Each processor evaluates some restrictions and pass request through, or modifies and then pass, or rejects it. 157 | 158 | General sequence for bucket-related request: 159 | 160 | 1. Rate locker rejects request if thread is out of capacity or remote client makes too many requests. 161 | 2. Session manager checks user creds or session and reject invalid. 162 | 3. DB locker rejects request if user have no permissions to deal with requested bucket. 163 | 4. Method locker rejects request if user have no rights to exec requested method and/or query. 164 | 5. If create/write requested, input data is filtered. Docs, that user have no permissions to write into, are eliminated from request. 165 | 6. Request is passed to CouchDB 166 | 7. CouchDB applies own security rules and `validate_doc_update` from `_design/acl`, that denies invalid ACL-related properties changes. 167 | 8. CouchDB response is filtered, docs that user is not allowed to read, are eliminated. 168 | 9. Response is sent or piped to user. 169 | 170 | Processors and mappings between CouchDB API routes and flow chains are contained in `/cvr/router.js` and `/cvr/restmap.js` files. 171 | 172 | 173 | ## Some technical details 174 | 175 | ### RAM 176 | 177 | CoverCouch is memory-intensive. Entire bucket ACL is memcached on first access or start. Moreover, each worker has its’s own ACL cache, they are not shared. 178 | 179 | This approach allows to resolve ACL synchronously in microseconds – but it costs ~300–500 bytes of RAM for each doc, and you should multiply result by number of workers. 180 | 181 | So if you have 1M doc DB that need per-doc ACL (very rear case in CouchDB world), you need 500Mb+ of RAM for each worker. 182 | 183 | Also when CoverCouch pipes, it need about 3 times more RAM, then two subsequent rows transmitted. Be careful if you inline 100Mb attach in JSON – you may need to wire ~400Mb to process pipe slice. 184 | 185 | 186 | ### Fetch/resend vs pipe 187 | 188 | __Fetch/resend__ strategy is used for ‘not very long’ requests that can produce set of rows. CoverCouch fetches entire CouchDB response, filters it and resends gzipped reply to client. 189 | 190 | ‘Not very long’ means that no inlined attachments expected and request has some range limiting keys (`startkey`–`endkey`,`keys` or `key`). 191 | 192 | Fetch/resend strategy allows to send response faster (sometimes much faster) due to compression and unnecessary response fragmentation removal. 193 | 194 | __Pipe__ strategy is used for potentially ‘long’ requests: feeds, or requests with attaches inlined, or with no query limits. 195 | 196 | Single-doc and attachment GETs are also piped. 197 | 198 | 199 | ### Auto restart 200 | 201 | Each worker restarts daily at an hour, defined in `workers.reloadAt` conf key. Restart takes back frozen and leaked memory and terminates hung feeds. Sibling threads never restart simultaneously – min gap is defined in `workers.reloadOverlap`. 202 | 203 | 204 | ## Limitations 205 | 206 | ### List functions 207 | 208 | Since _list functions are emulated inside CoverCouch, they do not support `provides()` inside. Gonna fix it in 0.2. 209 | 210 | ### Authorization methods 211 | 212 | Only cookie and basic auth supported. Request with `user:pwd@domain.name` are treated as they have no auth in URL. 213 | 214 | ### Length of `_id` 215 | 216 | Length of `_id` property is limited to 200 chars by default to speed up regexp, that digs out doc `_id`s from pipe without parsing JSON. 217 | 218 | Doc _id length limit is defined in `couch.maxIdLength` conf property. This limit does not in any way restricts creation of docs with longer `_id`s. The limitation means ‘we assume DB has no docs with ids longer, than 200 chars’. 219 | 220 | ### Weird behavior of `limit` query param 221 | 222 | Since CouchDB response is filtered, we can not expect, that `limit` param works properly in all cases. CouchDB can, for example, send 10 docs – and they all may be eliminated from response by ACL. 223 | 224 | To avoid this behavior do not use CoverCouch as an intentional filter, ACL engine was not intended to be a filter. 225 | 226 | For example, do not use ACL-filtered `_all_docs` to retrieve all user docs. Much better way is to make special view for it and them tap it with key range. Also this approach is much faster. 227 | 228 | Same for `limit`. Use special views and key ranges, not `limit`, to fetch predictable set of docs. 229 | 230 | ### Futon 231 | 232 | Futon is visible only for admins. Also please note, that Logout link in Futon does not work since it use `_:_@your.couch.url/_session` auth syntax. 233 | 234 | 235 | ### No COPY method 236 | 237 | COPY request processors are not yet implemented. 238 | 239 | ## Known issues and plans 240 | 241 | Tests suits and demos are underway. Same for interactive ddoc JSON editor (see current version at [http://cloudwall.me/etc/json-editor.html](http://cloudwall.me/etc/json-editor.html)). 242 | 243 | Also going to implement precache-free ACL mode – async and more slow, but less memory demanding. 244 | 245 | Please, feel free to open issues or contribute. 246 | 247 | --- 248 | 249 | © 2015 ermouth. CoverCouch is MIT-licensed. 250 | -------------------------------------------------------------------------------- /covercouch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CoverCouch 0.1.5 main 3 | * Read ACL for CouchDB 4 | * 5 | * Created by ermouth on 18.01.15. 6 | */ 7 | 8 | 9 | require('sugar'); 10 | 11 | var worker = require('./cvr/worker'), 12 | runtime = { 13 | cluster: require('cluster'), 14 | root: __dirname 15 | }, 16 | conf = require('./cvr/config')(runtime), 17 | log = require('./cvr/logger')(runtime); 18 | 19 | if (runtime.cluster.isMaster) { 20 | 21 | var cpus = conf.workers.count || require('os').cpus().length, 22 | fs = require('fs'), 23 | workers = []; 24 | 25 | // On worker die 26 | runtime.cluster.on('exit', function (worker) { 27 | for (var i = 0; i < workers.length; i++) if (worker.id == workers[i].id) workers[i] = null; 28 | workers = workers.compact(true); 29 | workers.push(runtime.cluster.fork()); 30 | }); 31 | 32 | fs.watch(runtime.root + '/cvr/config.js', function (event, filename) { 33 | _restart('Config changed'); 34 | }); 35 | 36 | // Fork workers 37 | for (var i = 0; i < cpus; i++) workers.push(runtime.cluster.fork()); 38 | 39 | // Restarter 40 | var _restart = function (msg) { 41 | log('Restart workers: ' + msg); 42 | var i = workers.length; 43 | while (i--) _stop.fill(workers[i]).delay(i * conf.workers.reloadOverlap); 44 | }, 45 | _stop = function (w) { 46 | if (w) { 47 | w.send({event: 'shutdown', time: Date.now()}); 48 | _kill.fill(w).delay(conf.workers.killDelay); 49 | } 50 | }, 51 | _kill = function (w) { 52 | if (w && w.suicide === undefined) w.kill(); 53 | }, 54 | _restarter = function () { 55 | if (Date.create().getHours() == (conf.workers.reloadAt || 0)) _restart('Daily restart'); 56 | }, 57 | restarter = setInterval(_restarter, 36e5); 58 | 59 | } else if (runtime.cluster.isWorker) worker(runtime); 60 | -------------------------------------------------------------------------------- /cvr/acl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CoverCouch 0.1.1 ACL-related functions 3 | * Created by ermouth on 19.01.15. 4 | */ 5 | module.exports = function (cvr) { 6 | 7 | var Q = cvr.Q, 8 | pending={}, 9 | isA = Object.isArray, 10 | isB = Object.isBoolean, 11 | isS = Object.isString, 12 | isO = Object.isObject, 13 | isN = Object.isNumber, 14 | isR = Object.isRegExp, 15 | isF = Object.isFunction; 16 | 17 | function _getAcl(u, dbv, id0) { 18 | // assume acl view is loaded 19 | var id=id0 || "", 20 | acl = { 21 | _r:true, 22 | _w:true, 23 | _d:true 24 | }, 25 | dacl; 26 | 27 | if (u.admin) return acl; 28 | 29 | if (/^_design\//.test(id)) acl._w = acl._d = false; 30 | 31 | if (!dbv.noacl && !u.superuser) { 32 | // check doc... 33 | var dacl = dbv.acl[id]; 34 | if (dacl) { 35 | acl._r = acl._w = acl._d = false; 36 | u._acl.forEach(_setAcl); 37 | 38 | // ...and parent 39 | if (dacl.p && dbv.acl[dacl.p]) { 40 | dacl = dbv.acl[dacl.p]; 41 | u._acl.forEach(_setAcl); 42 | } 43 | } 44 | } 45 | 46 | // Apply per-DB rules 47 | var ddoc = dbv.ddoc['_design/acl']; 48 | if (ddoc && isO(ddoc.dbacl)) { 49 | if (!id) acl._r = acl._w = acl._d = false; 50 | dacl = ddoc.dbacl; 51 | u._acl.forEach(_setAcl); 52 | } 53 | 54 | return acl; 55 | 56 | //-------------- 57 | 58 | function _setAcl(e) { 59 | if (!acl._r && dacl._r && dacl._r[e]) acl._r = true; 60 | if (!acl._w && dacl._w && dacl._w[e]) acl._w = true; 61 | if (!acl._d && dacl._d && dacl._d[e]) acl._d = true; 62 | } 63 | } 64 | 65 | cvr.ACL = { 66 | _pending:pending, // ACL update requests pending, keys are "dbname docid dbseq" 67 | 68 | load: function(db, id, seq0){ 69 | // return promise that is resolved with acl doc 70 | // when ACL become up-to-date 71 | var dbv = cvr.db[db], 72 | dbc = dbv.nano, 73 | seq = +seq0, 74 | key = db+" "+id, 75 | pi, i, iseq = 0, aclreq; 76 | 77 | if (dbv.acl[id] && +dbv.acl[id].s >= seq) { 78 | // ACL is up-to-date 79 | iseq = +dbv.acl[id].s; 80 | pi=null; 81 | if (pending[key] && Object.size(pending[key])) { 82 | // detach promises from repo 83 | for (i in pending[key]) { 84 | if (+i <= iseq) { 85 | pi = pending[key]; 86 | pi.resolve(dbv.acl[id]); 87 | delete pending[key][i]; 88 | } 89 | } 90 | if (pi) { 91 | return pi.promise; 92 | } 93 | } 94 | pi = Q.defer(); 95 | pi.resolve(dbv.acl[id]); 96 | return pi.promise; 97 | } 98 | else { 99 | // ACL is not up-to-date 100 | if (pending[key]) { 101 | // Check if we already have ACL request 102 | // to CouchDB pending 103 | aclreq = null; iseq = 0; 104 | for (i in pending[key]) { 105 | if (+i>iseq) { 106 | aclreq = pending[key][i]; 107 | iseq = +i; 108 | } 109 | } 110 | if (aclreq && iseq>=seq) { 111 | // return ACL req promise 112 | //console.log("Cache hit – pending ACL "+key); 113 | return aclreq.promise; 114 | } 115 | } 116 | // wire new ACL request 117 | if (!pending[key]) pending[key] = {}; 118 | pending[key][seq] = pi = Q.defer(); 119 | Q.denodeify(dbc.view)("acl","acl",{keys:[id]}) 120 | .then(function(data){ 121 | var r = dbv.acl[id]; 122 | if (data[0].rows && data[0].rows.length) { 123 | r = dbv.acl[id] = data[0].rows[0].value; 124 | } 125 | else if (r) { 126 | dbv.acl[id].s = seq; 127 | } 128 | // Resolve all promises with ACL seq <= reqd one 129 | if (r) { 130 | iseq = r.s; 131 | for (i in pending[key]) { 132 | if (+i <= iseq) { 133 | pending[key][i].resolve(r); 134 | delete pending[key][i]; 135 | } 136 | } 137 | } else pi.resolve({_r:{},_d:{},_w:{},p:"",s:seq}); 138 | 139 | }, 140 | function(d){ 141 | pi.reject(); 142 | }); 143 | return pi.promise; 144 | } 145 | }, 146 | db: function (session, db) { 147 | var u = cvr.user[session.user], 148 | dbv = cvr.db[db]; 149 | if (!dbv) return false; 150 | if (dbv.isforall && dbv.noacl && !dbv.restricted) return 2; 151 | else { 152 | if (!dbv.isforall) { 153 | // we have _design/acl.restrict.* general access rule 154 | var acl = cvr.db[db].ddoc['_design/acl'].restrict["*"], 155 | allow = false; 156 | u._acl.forEach(function(e){if (acl[e]) allow=true;}); 157 | return allow?1:0; 158 | } 159 | else return 1; 160 | } 161 | }, 162 | doc: function(session, db, id){ 163 | // Sync validator by doc._id, 164 | // assume acl view is loaded 165 | // and up-to-date 166 | var u = cvr.user[session.user], 167 | dbv = cvr.db[db]; 168 | return _getAcl(u, dbv, id); 169 | }, 170 | rows: function(session, db, rows, action, preserveDenied){ 171 | // rows-like array mass validator 172 | var u = cvr.user[session.user], 173 | dbv = cvr.db[db], 174 | op = action||"_r", 175 | r = []; 176 | rows.forEach(function(e){ 177 | if (_getAcl(u, dbv, e.id)[op]) r.push(e); 178 | else if (preserveDenied) r.push({id: e.id, error:"not_found"}) 179 | }); 180 | return r; 181 | }, 182 | object: function(session, db, list, action){ 183 | var i, u = cvr.user[session.user], 184 | dbv = cvr.db[db], 185 | op = action||"_r", 186 | r = {}; 187 | for (i in list) { 188 | if (_getAcl(u, dbv, i)[op]) r[i]=list[i]; 189 | } 190 | return r; 191 | }, 192 | unwind: function(a){ 193 | // converts ["user1","r-users","u-user2"] 194 | // to unified {"u-user1": 1, "r-users": 1, "u-user2": 1} 195 | if (!isA(a) || !a.length) return {}; 196 | var r = {}; 197 | a.forEach(function(e){ 198 | if (isS(e) && e) { 199 | if (/^[ru]-.+/.test(e)) r[e]=1; 200 | else r['u-'+e] = 1; 201 | } 202 | }); 203 | return r; 204 | } 205 | 206 | }; 207 | 208 | } -------------------------------------------------------------------------------- /cvr/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CoverCouch 0.1.5 configuration 3 | * Created by ermouth on 18.01.15. 4 | */ 5 | 6 | module.exports = function (runtime) { 7 | return { 8 | 9 | server: { 10 | mount: "/", // Mount path, no trailing slash 11 | port: 8000, // Port 12 | maxpost: 50 * 1024 * 1024, // Max size of POST request 13 | 14 | rater: { // Request rate locker 15 | all: { // Total requests limit 16 | interval: 1, // Seconds, collect interval 17 | limit: 100 // Max requests per interval 18 | }, 19 | ip: { // Per-ip requests limit 20 | interval: 10, 21 | limit: 100 22 | } 23 | } 24 | }, 25 | 26 | couch: { 27 | url: "http://127.0.0.1:5984", // Couch URL 28 | nano: "http://login:pass@127.0.0.1:5984", // Couch URL with admin login:pass 29 | users: "_users", // Users bucket 30 | maxIdLength: 200, // Max _id length 31 | renewSessionInterval:300, // Seconds between subsequent _session request 32 | preload: [ // Buckets to preload and to insert acl ddoc if none 33 | // "sales","dev" 34 | ] 35 | }, 36 | 37 | workers: { 38 | "count": 1, // Total threads 39 | "reloadAt": 4, // Hour all threads are restarted 40 | "reloadOverlap": 30e3, // Gap between restarts of simultaneous threads 41 | "killDelay": 2e3 // Delay between shutdown msg to worker and kill, ms 42 | }, 43 | 44 | // CORS headers 45 | headers: { 46 | "Access-Control-Allow-Credentials": true, 47 | "Access-Control-Expose-Headers": "Content-Type, Server", 48 | "Access-Control-Allow-Headers": "Content-Type, Server, Authorization", 49 | "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD", 50 | "Access-Control-Max-Age": "86400", 51 | "X-Powered-By": "CoverCouch 0.1.0" 52 | }, 53 | 54 | // CORS domains, like "http://xxx.xxx": true 55 | origins: {} 56 | } 57 | } -------------------------------------------------------------------------------- /cvr/ddoc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CoverCouch 0.1.1 default _design/acl 3 | * Created by ermouth on 21.01.15. 4 | */ 5 | 6 | 7 | module.exports = function () { 8 | 9 | // To make JSON, pasteable in CouchDB as new doc, 10 | // use JSON editor http://cloudwall.me/etc/json-editor.html – 11 | // just paste raw ddoc definition from below. 12 | 13 | var ddoc = { 14 | _id: "_design/acl", 15 | options: { 16 | local_seq: true, 17 | include_design: true 18 | }, 19 | 20 | type: "ddoc", 21 | stamp: Date.now(), 22 | version: "0.1.4", 23 | 24 | acl: [], 25 | /* 26 | restrict:{ 27 | "*":[] 28 | }, 29 | dbacl:{ 30 | _r:[], 31 | _w:[], 32 | _d:[] 33 | }, 34 | */ 35 | views: { 36 | acl: { 37 | map: function (doc) { 38 | 39 | // Map fn that generates ACL index 40 | 41 | var r = { 42 | s: doc._local_seq, 43 | p: "", 44 | _r: {}, 45 | _w: {}, 46 | _d: {} 47 | }, 48 | tmp = "", i, ctr = 0, 49 | cr = doc.creator, acl = doc.acl, ow = doc.owners, 50 | S = "string", O = "object", F = "function", 51 | rr = /^r-/, ru = /^u-/; 52 | 53 | if (typeof cr == S && cr) { 54 | tmp = cr; 55 | if (!ru.test(tmp)) tmp = 'u-' + tmp; 56 | r._r[tmp] = r._w[tmp] = r._d[tmp] = 1; 57 | ctr += 1; 58 | } 59 | 60 | if (acl != null && typeof acl == O && typeof acl.slice == F) { 61 | for (i = 0; i < acl.length; i++) { 62 | tmp = acl[i]; 63 | if (typeof tmp == S) { 64 | if (rr.test(tmp) || ru.test(tmp)) r._r[tmp] = 1; 65 | else r._r['u-' + tmp] = 1; 66 | } 67 | } 68 | ctr += 1; 69 | } 70 | 71 | if (ow != null && typeof ow == O && typeof ow.slice == F) { 72 | for (i = 0; i < ow.length; i++) { 73 | tmp = ow[i]; 74 | if (typeof tmp == S) { 75 | if (!rr.test(tmp) && !ru.test(tmp)) tmp = 'u-' + tmp; 76 | r._r[tmp] = r._w[tmp] = 1; 77 | } 78 | } 79 | ctr += 1; 80 | } 81 | 82 | if (!ctr) { 83 | tmp = "r-*"; 84 | if (/^_design/.test(doc._id)) r._r[tmp] = 1; 85 | else r._r[tmp] = r._w[tmp] = r._d[tmp] = 1; 86 | } 87 | 88 | if (typeof doc.parent == S) r.p = doc.parent; 89 | 90 | emit(doc._id, r); 91 | } 92 | } 93 | }, 94 | validate_doc_update: function (nd, od, userCtx, secObj) { 95 | var adm = !!( userCtx.roles.indexOf("_admin") >= 0 ), 96 | u = userCtx.name, 97 | uu = 'u-' + u, 98 | O = 'object', 99 | F = 'function', 100 | isA = function (o) { 101 | return (typeof o == O && typeof o.slice == F); 102 | }; 103 | 104 | if (!adm) { 105 | if (!od) { 106 | // Insert 107 | if (nd.creator && nd.creator != u && nd.creator != uu) 108 | throw({forbidden: 'Can’t create doc on behalf of other user.'}); 109 | } else { 110 | // Update or delete 111 | var odc = od.creator, 112 | odw = (isA(od.owners) ? od.owners : []).sort(), 113 | oda = isA(od.acl) ? od.acl.sort() + '' : '', 114 | ndc = nd.creator, 115 | ndw = (isA(nd.owners) ? nd.owners : []).sort(), 116 | nda = isA(nd.acl) ? nd.acl.sort() + '' : '', 117 | notCreator = (odc != u && odc != uu), 118 | notOwner = notCreator && odw.indexOf(u) == -1 && odw.indexOf(uu) == -1; 119 | 120 | if (!nd._deleted) { 121 | if (odc && odc != ndc) throw({ 122 | forbidden: 'Creator can not be changed.' 123 | }); 124 | 125 | if (notCreator && odw + '' != ndw + '') throw({ 126 | forbidden: 'Owners list can not be changed.' 127 | }); 128 | 129 | if (notOwner && oda != nda) throw({ 130 | forbidden: 'Readers list can not be changed.' 131 | }); 132 | } 133 | else { 134 | // Delete 135 | if (notCreator) throw({ 136 | forbidden: 'You can’t delete doc.' 137 | }); 138 | } 139 | 140 | } 141 | } 142 | } 143 | } 144 | 145 | return ddoc; 146 | 147 | } -------------------------------------------------------------------------------- /cvr/lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Std lib for JSON manipulation 3 | * Created by ermouth 2015-02-05 4 | */ 5 | 6 | module.exports = function (cw) { 7 | 8 | var n = function (o) { 9 | return o !== null && o !== undefined; 10 | }, 11 | isA = Object.isArray, isB = Object.isBoolean, isS = Object.isString, isO = Object.isObject, 12 | isN = Object.isNumber, isR = Object.isRegExp, isF = Object.isFunction, 13 | Fu = "function"; 14 | 15 | var l = cw.lib = { 16 | 17 | "getref": _getref, 18 | 19 | "a2o": function (a0, all) { 20 | //converts array of string to object with keys of strings 21 | var ob = {}, s = "", v; 22 | if (!Object.isArray(a0)) return ob; 23 | for (var i = 0; i < a0.length; i++) { 24 | v = a0[i]; 25 | if (all || v !== null && v !== undefined && v !== false && v !== "") { 26 | if (!ob[v]) ob[v] = 0; 27 | ob[v] += 1; 28 | } 29 | } 30 | return ob; 31 | }, 32 | "dry": function (obj, full) { 33 | /* makes shallow copy of obj and 34 | * removes all keys that starts with _ except _id and _rev, 35 | * if full==true, removes _id and _rev */ 36 | var dry = {}, i; 37 | for (i in obj) if (obj.hasOwnProperty(i) 38 | && (i.substr(0, 1) !== "_" 39 | || (!full && (i === "_id" || i === "_rev")) || i === "_attachments") 40 | ) dry[i] = obj[i]; 41 | return dry; 42 | }, 43 | "fuse": function fuse(o1, o2) { 44 | // overlaps o2 over o1, arrays are completely replaced with clone of, not merged 45 | if (arguments.length == 0) return {}; 46 | if (arguments.length == 1) return arguments[0]; 47 | for (var i = 1; i < arguments.length; i++) Object.merge(arguments[0], arguments[i], false, function (key, a, b) { 48 | if (b === undefined || b === null) return a; 49 | if (isA(b)) return Object.clone(b, true); 50 | else if (!isO(b)) return b; 51 | else return Object.merge(a, b, false); 52 | }); 53 | return arguments[0]; 54 | }, 55 | "overlap": function (o1, o2) { 56 | //overlaps o2 over o1, arrays are completely replaced, not merged 57 | return Object.merge(o1, o2, false, function (key, a, b) { 58 | if (!Object.isObject(b)) return b; 59 | else return Object.merge(a, b, false); 60 | }); 61 | }, 62 | "sdbmCode": function (s0) { 63 | //very fast hash used in Berkeley DB 64 | for (var s = JSON.stringify(s0), hash = 0, i = 0; i < s.length; i++) 65 | hash = s.charCodeAt(i) + (hash << 6) + (hash << 16) - hash; 66 | return (1e11 + hash).toString(36); 67 | }, 68 | "json": (function () { 69 | function f(n) { 70 | return n < 10 ? '0' + n : n; 71 | } 72 | 73 | Date.prototype.toJSON = function () { 74 | var t = this; 75 | return t.getUTCFullYear() + '-' + f(t.getUTCMonth() + 1) + '-' + f(t.getUTCDate()) + 76 | 'T' + f(t.getUTCHours()) + ':' + f(t.getUTCMinutes()) + ':' + f(t.getUTCSeconds()) + 'Z'; 77 | }; 78 | RegExp.prototype.toJSON = function () { 79 | return "new RegExp(" + this.toString() + ")"; 80 | }; 81 | var tabs = '\t'.repeat(10), fj = JSON.stringify; 82 | 83 | // - - - - - - - - - - - - - - - - - - - - - - - 84 | function s2(w, ctab0, tab) { 85 | var tl = 0, a, i, k, v, ctab = ctab0 || 0, xt = tabs; 86 | if (tab && isS(tab)) { 87 | tl = String(tab).length; 88 | xt = String(tab).repeat(10); 89 | } 90 | switch ((typeof w).substr(0, 3)) { 91 | case 'str': 92 | return fj(w); 93 | case'num': 94 | return isFinite(w) ? '' + String(w) + '' : 'null'; 95 | case 'boo': 96 | case'nul': 97 | return String(w); 98 | case 'fun': 99 | return fj( 100 | w.toString().replace(/^(function)([^\(]*)(\(.*)/, "$1 $3") 101 | .replace(/(})([^}]*$)/, '$1') 102 | ); 103 | case 'obj': 104 | if (!w) return'null'; 105 | if (typeof w.toJSON === Fu) return s2(w.toJSON(), ctab + (tab ? 1 : 0), tab); 106 | a = []; 107 | if (isA(w)) { 108 | for (i = 0; i < w.length; i += 1) { 109 | a.push(s2(w[i], ctab + (tab ? 1 : 0), tab) || 'null'); 110 | } 111 | return'[' + a.join(',' + (tab ? "\n" + xt.to(ctab * tl + tl) : "")) + ']'; 112 | } 113 | for (k in w) if (isS(k)) { 114 | v = s2(w[k], ctab + (tab ? 1 : 0), tab); 115 | if (v) a.push((tab ? "\n" + xt.to(ctab * tl + tl) : "") + s2(k, ctab + (tab ? 1 : 0), tab) + ': ' + v); 116 | } 117 | return '{' + a.join(',') + (tab ? "\n" + xt.to(ctab * tl) : "") + '}'; 118 | } 119 | } 120 | 121 | return s2.fill(undefined, 0, undefined); 122 | 123 | })(), 124 | "fromjson": function (s) { 125 | var obj = JSON.parse(s); 126 | _unjson(obj); 127 | return obj; 128 | }, 129 | "unjson": function (o) { 130 | _unjson(o); 131 | return o; 132 | }, 133 | "mask": function (src, mask0) { 134 | //returns src obj masked with mask 135 | if (!isO(src)) return null; 136 | var res, mask = mask0; 137 | if (isS(mask)) { 138 | return _getref(src, mask); 139 | } else if (isA(mask)) { 140 | res = []; 141 | for (var i = 0; i < mask.length; i++) { 142 | res[i] = isS(mask[i]) ? _getref(src, mask[i]) || null : null; 143 | } 144 | return res; 145 | } else if (isO(mask)) 146 | return _merge(src, mask); 147 | //- - - - 148 | function _merge(src, mask) { 149 | if (!isO(mask)) return {}; 150 | var dest = {}; 151 | for (var i in mask) { 152 | if (!isO(mask[i]) && src.hasOwnProperty(i)) { 153 | dest[i] = Object.clone(src[i], true); 154 | } 155 | else if (src.hasOwnProperty(i)) { 156 | if (isO(src[i])) dest[i] = _merge(src[i], mask[i]); 157 | else dest[i] = Object.clone(src[i], true); 158 | } 159 | } 160 | return dest; 161 | } 162 | }, 163 | "unmask": function (src, mask) { 164 | // unfolds masked into obj 165 | var res = {}; 166 | if (isO(src) && isO(mask)) return f.mask(src, mask); 167 | else if (isA(src) && isA(mask)) { 168 | for (var i = 0; i < mask.length; i++) { 169 | if (src[i] != null) _blow(res, src[i], mask[i]); 170 | } 171 | return res; 172 | } else if (isS(mask)) return _blow({}, src, mask); 173 | else return null; 174 | 175 | //- - - 176 | function _blow(data, src, ref) { 177 | var ptr, path, preptr, val = Object.clone(src, true), i = 0; 178 | if (!/\./.test(ref)) { 179 | //ref is flat 180 | if (null != src) data[ref] = val; 181 | } else { 182 | path = ref.split(".").each(function (a, i) { 183 | this[i] = String(a).compact(); 184 | }); 185 | ptr = data; 186 | for (; i < path.length; i++) { 187 | if (i === path.length - 1) ptr[path[i]] = val; //we'r in the hole 188 | if (i === 0) ptr = data[path[0]], preptr = data; 189 | else preptr = preptr[path[i - 1]], ptr = ptr[path[i]]; 190 | if (undefined === ptr) ptr = preptr[path[i]] = {}; 191 | } 192 | } 193 | return data; 194 | } 195 | }, 196 | "crc2": (function() { 197 | var sdbm, keys = Object.keys; 198 | 199 | sdbm = function (s){ 200 | for (var hash=0,i=0;i= 0 ? remaining : 0); 39 | }, 40 | onLimit: function (req, res, next, options, rate) { 41 | res.set(420).end(); 42 | } 43 | }) 44 | ]; 45 | 46 | }; -------------------------------------------------------------------------------- /cvr/restmap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CoverCouch 0.1.5 REST map 3 | * Created by ermouth on 22.01.15. 4 | */ 5 | 6 | module.exports = function (cvr) { 7 | 8 | // Processing chains for CouchDB API members in human-readable form. 9 | // Later router.js unwinds {get:{/db:{/_design/id:'db,doc,pipe'}}} 10 | // into express.js routes like 11 | // router.get('/:db/_design/:id', actors.db, actors.doc, actors.pipe). 12 | // Keys order is important! 13 | 14 | var routes = {}, 15 | map = { 16 | get: { 17 | '/': 'pipe', 18 | '/_session': 'session', 19 | '/_active_tasks': 'admin', 20 | '/_all_dbs': 'dblist', 21 | '/_db_updates': 'admin', 22 | '/_log': 'admin', 23 | '/_stats': 'admin', 24 | '/_stats/*': 'admin', 25 | '/_config': 'admin', 26 | '/_config/*': 'admin', 27 | '/_utils': 'admin', 28 | '/_utils/*': 'admin', 29 | '/_uuids': 'pipe', 30 | 31 | '/db': { 32 | '': 'db,pipe', 33 | '/_all_docs': 'db,rows', 34 | '/_changes': 'db,changes', 35 | '/_security': 'admin', 36 | '/_revs_limit': 'admin', 37 | '/id': 'db,doc,pipe', 38 | 39 | '/_design/id': 'db,doc,pipe', 40 | '/_design/id/fname': 'db,doc,pipe', 41 | '/_local/id': 'db,pipe', 42 | '/id/fname': 'db,doc,pipe', 43 | 44 | '/_design/ddoc': { 45 | '/_info': 'admin', 46 | 47 | '/_view/view': 'db,rows', 48 | '/_show/show': 'pipe', 49 | '/_rewrite/p': 'admin', 50 | 51 | '/_list/list/view': 'db,dbinfo,list', 52 | '/_show/show/id': 'db,doc,pipe', 53 | 54 | '/_list/list/ddoc2/view': 'db,dbinfo,list' 55 | } 56 | } 57 | }, 58 | post: { 59 | '/_session': 'body,auth', 60 | '/_replicate': 'admin', 61 | '/_restart': 'admin', 62 | '/db': { 63 | '': 'db,body,doc,pipe', 64 | '/_all_docs': 'db,body,rows', 65 | '/_bulk_docs': 'db,body,bulk', 66 | '/_changes': 'db,changes', 67 | '/_compact': 'admin', 68 | '/_compact/*': 'admin', 69 | '/_view_cleanup': 'admin', 70 | '/_temp_view': 'admin', 71 | '/_purge': 'admin', 72 | '/_missing_revs': 'db,body,revs', 73 | '/_revs_diff': 'db,body,revs', 74 | '/_ensure_full_commit': 'admin', 75 | 76 | '/_design/ddoc': { 77 | '/_view/view': 'db,body,rows', 78 | '/_show/show': 'db,pipe', 79 | '/_update/update': 'db,pipe', 80 | 81 | '/_list/list/view': 'db,body,dbinfo,list', 82 | '/_show/show/id': 'db,body,doc,pipe', 83 | '/_update/update/id': 'db,body,doc,pipe', // Update checks R, not W permissions! 84 | 85 | '/_list/list/ddoc2/view': 'db,body,dbinfo,list' 86 | } 87 | } 88 | }, 89 | put: { 90 | '/db': { 91 | '': 'admin', 92 | '/_security': 'admin', 93 | '/_revs_limit': 'admin', 94 | '/id': 'db,doc,pipe', 95 | '/_design/id': 'db,doc,pipe', 96 | '/_design/id/fname': 'db,doc,pipe', 97 | '/_local/id': 'db,pipe', 98 | '/id/fname': 'db,doc,pipe', 99 | '/_design/ddoc/_update/update/id': 'db,doc,pipe' 100 | } 101 | }, 102 | head: { 103 | '/db': { 104 | '': 'db,pipe', 105 | '/id': 'db,doc,pipe', 106 | '/_design/id': 'db,pipe', 107 | '/id/fname': 'db,doc,pipe', 108 | '/_design/id/fname': 'db,pipe' 109 | } 110 | }, 111 | delete: { 112 | '/_session': 'session', 113 | '/db': { 114 | '': 'admin', 115 | '/id': 'db,doc,pipe', 116 | '/_design/id': 'db,doc,pipe', 117 | '/_design/id/fname': 'db,doc,pipe', 118 | '/_local/id': 'db,pipe', 119 | '/id/fname': 'db,doc,pipe' 120 | } 121 | }} 122 | // -- end of request actors map 123 | 124 | var go = function (r, key, obj) { 125 | if (Object.isObject(obj)) { 126 | for (var i in obj) go(r, key + i, obj[i]); 127 | return r; 128 | } else r.push({ 129 | path: key.split("/").map(function (e) { 130 | if (e && !/^[_\*]/.test(e))return':' + e; 131 | else return e; 132 | }).join("/"), 133 | ops: obj.split(/\*?,\s*/).compact(true) 134 | }); 135 | }; 136 | // Build routes 137 | for (var i in map) { 138 | routes[i] = []; 139 | go(routes[i], '', map[i]) 140 | } 141 | 142 | return routes; 143 | 144 | } -------------------------------------------------------------------------------- /cvr/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CoverCouch 0.1.5 router middleware functions 3 | * Created by ermouth on 18.01.15. 4 | */ 5 | 6 | 7 | module.exports = function (R, cvr) { 8 | 9 | var i, 10 | es = cvr.Estream, 11 | routes = require('./restmap')(cvr), 12 | conf = cvr.config, 13 | trimPipe = conf.couch.maxIdLength * 1 + 100, 14 | couch = conf.couch.url, 15 | Q = cvr.Q, 16 | isA = Object.isArray, 17 | isB = Object.isBoolean, 18 | isS = Object.isString, 19 | isO = Object.isObject, 20 | isN = Object.isNumber, 21 | isR = Object.isRegExp, 22 | isF = Object.isFunction; 23 | 24 | var actors = { 25 | 26 | 27 | // ====== Prechecks, parsers and early guards ====== 28 | 29 | db: function (req, res, next) { 30 | 31 | // Checks if principal has permissions 32 | // to access bucket and particular method. 33 | // Detects if request has rangekeys. 34 | // Caches bucket ACL and ddocs if they are not cached. 35 | 36 | var db = (req.params || {}).db, 37 | u = cvr.user[req.session.user], 38 | m = req.method, 39 | dbv; 40 | if (!db) next(); 41 | else { 42 | if (dbv = cvr.db[db]) { 43 | if (!cvr.db[db].cached) cvr.Couch.cacheDb(db).then(_check); 44 | else _check(); 45 | } 46 | else _fail(req, res, {error: "not_found", reason: "no_db_file"}, 404); 47 | } 48 | return; 49 | 50 | // - - - - - - - - - - - - - - 51 | 52 | function _check() { 53 | 54 | //Can user see this bucket? 55 | 56 | var ok = cvr.ACL.db(req.session, db); 57 | if (ok == 2) actors.pipe(req, res); 58 | else if (ok == 1) _restrict(); 59 | else _fail(req, res, {error: "not_found", reason: "ACL"}, 404); 60 | } 61 | 62 | function _restrict() { 63 | 64 | // Can user exec method and fn requested? 65 | 66 | if (!dbv.restricted || !dbv._restrict[req.method]) _isLong(); 67 | else { 68 | var acl = null, 69 | allow = false, 70 | rules = dbv._restrict[req.method], 71 | url = req.url.from(req.params.db.length + 1); 72 | rules.forEach(function (e) { 73 | if (e[0].test(url)) { 74 | if (!acl) acl = {}; 75 | Object.merge(acl, e[1]); 76 | } 77 | }); 78 | if (!acl) return _isLong(); 79 | u._acl.forEach(function (e) { 80 | if (acl[e]) allow = true; 81 | }); 82 | if (!allow) _fail(req, res, {error: "forbidden", reason: "Method restricted."}, 403); 83 | else _isLong(); 84 | } 85 | } 86 | 87 | function _isLong() { 88 | 89 | // Request is long? 90 | 91 | var q = req.query || {}; 92 | if (m === "GET" || m === "POST") { 93 | req.isLong = !( 94 | (req.body && req.body.keys) 95 | || 96 | (q.startkey !== undefined && q.endkey !== undefined ) 97 | || 98 | q.key !== undefined 99 | ) 100 | || 101 | (q.include_docs !== undefined && q.attachments !== undefined ); 102 | } 103 | next(); 104 | } 105 | }, 106 | 107 | doc: function (req, res, next) { 108 | // acl-controls doc and op over it 109 | var mt = req.method, 110 | m = ({ 111 | GET: '_r', 112 | PUT: '_w', 113 | DELETE: '_d', 114 | HEAD: '_r' 115 | })[mt], 116 | ispost = mt == "POST", 117 | db = req.params.db, 118 | id = req.params.id, 119 | acl; 120 | 121 | if (ispost) { 122 | 123 | // Special case – can be new doc, _update or _show call. 124 | // Note we validate _update request as READ, not WRITE, cause 125 | // doc modifications using _update functions are assumed safe. 126 | // Real-world scenario – user may have read permissions 127 | // and rights to update several doc fields with _update fn. 128 | 129 | if (id) m = '_r'; //_show or _update 130 | else { 131 | m = '_w'; 132 | id = req.body._id; 133 | } 134 | } 135 | 136 | if (!req.params.ddoc && /^[^\?]\/_design\//.test(req.url)) id = '_design/' + id; 137 | 138 | acl = cvr.ACL.doc(req.session, db, id); 139 | if (acl[m]) next(); 140 | else { 141 | _fail(req, res, { 142 | error: m != '_r' ? "forbidden" : "not_found", 143 | reason: "ACL" 144 | }, m != '_r' ? 403 : 404); 145 | } 146 | }, 147 | 148 | body: function (req, res, next) { 149 | // bodyParser 150 | try { 151 | cvr.bodyParser({limit: conf.server.maxpost})(req, res, next); 152 | } catch (e) { 153 | _fail(req, res, {error: "bad_request", reason: 'Invalid format.'}, 400); 154 | } 155 | }, 156 | 157 | dbinfo : function (req, res, next){ 158 | 159 | // Mounts different db info to req, 160 | // needed for _list emulation 161 | 162 | var db = req.params.db, 163 | done = (function(){ next(); }).after(3); 164 | 165 | cvr.nano.db.get(db, function(e, r){ 166 | if (!e) req.dbInfo = r; 167 | done(); 168 | }); 169 | 170 | cvr.nano.request({ db:db, path:'/_security' }, function(e, r){ 171 | if (!e) req.secObj = r; 172 | done(); 173 | }); 174 | 175 | cvr.nano.request({ path:'_uuids' }, function(e, r){ 176 | if (!e) req.uuid = r.uuids[0]; 177 | done(); 178 | }); 179 | }, 180 | 181 | 182 | // ====== Terminal functions ====== 183 | 184 | error: function (err, req, res, next) { 185 | _fail(req, res, { 186 | error: err.status == 400 ? "bad_request" : "error", 187 | reason: err.message || 'Invalid request.' 188 | }, err.status || 400); 189 | }, 190 | 191 | 192 | bulk: function (req, res) { 193 | // saves multiple docs 194 | var o = req.body, 195 | d = o.docs, 196 | r0 = [], r1 = [], 197 | atomic = req.body ? req.body.all_or_nothing + '' == 'true' : false, 198 | errs = false, 199 | p = _gen(req, {body: Object.reject(o, 'docs')}, true); 200 | 201 | if (isA(d) && d.length) { 202 | 203 | d.forEach(function (e) { 204 | if (!isO(e)) { 205 | r1.push({error: 'error', reason: 'Invalid object.'}); 206 | errs = true; 207 | } 208 | else if (!e._id) { 209 | r0.push(e); 210 | r1.push(null); 211 | } 212 | else if (cvr.ACL.doc(req.session, req.params.db, e._id)[e._deleted ? '_d' : '_w']) { 213 | r0.push(e); 214 | r1.push(null); 215 | } 216 | else { 217 | r1.push({id: e._id, error: 'forbidden', reason: 'ACL'}); 218 | errs = true; 219 | } 220 | }); 221 | p.body.docs = r0; 222 | 223 | // Now we ready to request Couch 224 | // to save subset of elts that may be allowed 225 | 226 | if (atomic && errs) { 227 | _fail(req, res, { error: "forbidden", reason: "ACL rejected transaction." }, 403); 228 | } 229 | else if (r0.length) cvr.Request(p).then(function (data) { 230 | var a, ctr = 0; 231 | try { 232 | a = isS(data[1]) ? JSON.parse(data[1]) : data[1]; 233 | } catch (e) { 234 | } 235 | if (isA(a)) { 236 | a.forEach(function (e) { 237 | if (!ctr && r1[0] == null) { 238 | r1[0] = e; 239 | } else { 240 | while (r1[ctr] != null && ctr <= r1.length) { 241 | ctr++; 242 | } 243 | if (ctr < r1.length) r1[ctr] = e; 244 | } 245 | }); 246 | _send(req, res, [data[0], r1.compact(true)]); 247 | a = r0 = void 0; 248 | } 249 | else _send(req, res, data); 250 | }); 251 | else _sendRaw(req, res, r1, 200); 252 | } 253 | else actors.pipe(req, res); 254 | 255 | }, 256 | 257 | 258 | session: function (req, res) { 259 | // Gets session 260 | if (req.method == "DELETE" && req.session.id) { 261 | cvr.session[req.session.id] = null; 262 | } 263 | req.pipe(cvr.request({ 264 | url: couch + req.url, 265 | headers: req.h 266 | })) 267 | .pipe(res); 268 | }, 269 | 270 | 271 | auth: function (req, res) { 272 | // Authorize user 273 | var p; 274 | if ( 275 | !req.body 276 | || !isS(req.body.name) 277 | || !isS(req.body.password) 278 | ) { 279 | _fail(req, res, { error: "unauthorized", reason: "Invalid request." }, 401); 280 | } 281 | else { 282 | var u = cvr.user[req.body.name]; 283 | if (!u || u.name == '_anonymous' || u.inactive) { 284 | _fail(req, res, { error: "unauthorized", reason: "Invalid login or password." }, 401); 285 | } else { 286 | p = _gen(req, {method: "POST"}); 287 | cvr.Request(p).done(function (data) { 288 | // We do not memoize session now 289 | _send(req, res, data); 290 | }); 291 | } 292 | } 293 | }, 294 | 295 | 296 | admin: function (req, res) { 297 | // Rejects request if not admin, 298 | // pipes if admin 299 | var u = cvr.user[req.session.user]; 300 | if (u.admin) actors.pipe(req, res); 301 | else _fail(req, res); 302 | }, 303 | 304 | 305 | pipe: function (req, res) { 306 | // Pipes request through 307 | var p = { 308 | url: couch + req.url, 309 | headers: req.h 310 | }; 311 | //If we have no body parsed, pipe request 312 | if (!req.body) req.pipe(cvr.request(p)).pipe(res); 313 | else { 314 | // Make request and send result 315 | p = _gen(req, {}); 316 | cvr.Request(p).done(function (data) { 317 | _send(req, res, data); 318 | }); 319 | } 320 | }, 321 | 322 | 323 | dblist: function (req, res) { 324 | // Get list of all dbs, 325 | // first checks accesibility of each 326 | // and restrictions in _design/acl for 327 | // user 328 | 329 | cvr.Request(_gen(req), {}, true).done(function (data) { 330 | // We do not memoize session 331 | // until next request with cookie 332 | var a, dbs = []; 333 | try { 334 | a = isS(data[1]) ? JSON.parse(data[1]) : data[1]; 335 | } catch (e) { 336 | } 337 | if (isA(a)) { 338 | a.forEach(function (db) { 339 | if (cvr.ACL.db(req.session, db)) dbs.push(db); 340 | }) 341 | } 342 | _send(req, res, [data[0], dbs]); 343 | }); 344 | }, 345 | 346 | changes: function (req, res) { 347 | // Pipes filtered changes feed 348 | var db = req.params.db, 349 | m = "no", 350 | seq = {}, 351 | ctr = 0, 352 | prev = null, 353 | json = false, 354 | p = _gen(req); 355 | 356 | // detect method 357 | if (/^(normal|longpoll|continuous|eventsource)$/.test(req.query.feed)) m = req.query.feed.to(2); 358 | if (req.query.attachments && req.query.include_docs && m == "no") m = "lo"; 359 | 360 | json = /^[nl]/.test(m); 361 | 362 | req.pipe(cvr.request(p)) 363 | .pipe(es.split()) 364 | .pipe(es.map(function (data, done) { 365 | var id, ok = false, dseq; 366 | ctr += 1; 367 | if (!data.length) { 368 | ok = true; 369 | _fin() 370 | } 371 | else { 372 | if (json) { 373 | if (ctr == 1 || data.to(11) === '"last_seq":') ok = true; 374 | else if (data.to(2) === '],') ok = true; 375 | } 376 | else if (m == "ev" && data.to(3) === 'id:') { 377 | if (seq[data.from(3).trim()]) ok = true; 378 | else ok = false; 379 | } 380 | else if (m == "co" && data.to(11) == '{"last_seq"') ok = true; 381 | // parse id 382 | if (!ok) { 383 | id = (data.to(trimPipe).match(/^(data:)?\{[^\{]*"id":"(.+?)","/) || []).last(); 384 | 385 | if (id) { 386 | dseq = (data.to(50).match(/^(data:)?\{[^\{]*"seq":(\d+),"/) || []).last(); 387 | if (dseq) { 388 | dseq = +dseq; 389 | if (cvr.db[db].acl[id] && cvr.db[db].acl[id].s >= dseq) { 390 | 391 | ok = !!cvr.ACL.doc(req.session, req.params.db, id)._r; 392 | _fin(dseq); 393 | } 394 | else { 395 | // read ACL async 396 | cvr.ACL.load(db, id, dseq) 397 | .then( 398 | function () { 399 | ok = !!cvr.ACL.doc(req.session, req.params.db, id)._r; 400 | _fin(dseq); 401 | }, 402 | function () { 403 | ok = false; 404 | _fin(); 405 | } 406 | ); 407 | } 408 | } else _fin(); 409 | } else _fin(); 410 | } else _fin(); 411 | } 412 | return; 413 | 414 | function _fin(dseq) { 415 | if (ok) { 416 | if (dseq) seq[dseq] = true; 417 | done(null, data + '\n'); 418 | } 419 | else { 420 | if (dseq) seq[dseq] = false; 421 | done(); 422 | } 423 | } 424 | })) 425 | .pipe(es.split()) 426 | .pipe(es.mapSync(function (data) { 427 | // Manipulations to trim off last comma before ]} 428 | // if it appears due to ACL-dropped rows. 429 | var tosend = ''; 430 | if (!json) return data; 431 | if (data.to(2) === '],') { 432 | if (prev.last() === ',') tosend = prev.to(-1); 433 | else tosend = prev; 434 | tosend += '\n' + data; 435 | prev = ''; 436 | } 437 | else { 438 | tosend = prev; 439 | prev = data; 440 | } 441 | 442 | if (null !== tosend) return tosend + '\n'; 443 | else return void 0; 444 | })) 445 | .pipe(res); 446 | }, 447 | 448 | 449 | list: function(req, res){ 450 | // _list implemetation, 3rd edition 451 | var db = req.params.db, 452 | dbv = cvr.db[db], 453 | path = req.params, 454 | jsopts={}, opts, 455 | rows=[], 456 | resobj={}, 457 | viewResult = {offset:0, total_rows:0}, 458 | isReduce = ( 459 | cvr.lib.getref(dbv.viewnames, (path.ddoc2||path.ddoc)+'.'+path.view) == 'reduce' 460 | && req.query.reduce != 'false' 461 | ); 462 | 463 | if (isO(req.query)) jsopts = _unjsonQuery(req.query); 464 | 465 | opts = Object.merge( 466 | Object.reject(jsopts, ['attachments', 'include_docs', 'format','reduce','group','group_level']), 467 | req.body && isA(req.body.keys) ? {keys: req.body.keys} : {}, 468 | true 469 | ); 470 | opts.reduce = false; 471 | 472 | dbv.nano.view(path.ddoc, path.view, opts, _list) 473 | .pipe(es.split()) 474 | .pipe(es.mapSync(function (data) { 475 | var id, d; 476 | if (data.length<100 && /^\{[^\{]*"offset":[^\{]*\[$/.test(data)) { 477 | d = JSON.parse(data+']}'); 478 | viewResult.offset = d.offset; 479 | viewResult.total_rows = d.total_rows; 480 | } 481 | // detect id 482 | id = (data.to(trimPipe).match(/^\{[^\{]*"id":"(.+?)","/) || []).last(); 483 | if (id && cvr.ACL.doc(req.session, db, id)._r) { 484 | // detect key, need to parse JSON 485 | try { 486 | d = JSON.parse(data.last() == ',' ? data.to(-1) : data); 487 | } catch (e) {} 488 | 489 | if (undefined !== d) rows.push(d); 490 | } 491 | return ''; 492 | })); 493 | 494 | //---------------------------- 495 | 496 | function _list(err){ 497 | if (!err) { 498 | if (isReduce) viewResult = cvr.Sandbox.reduce(req,rows); 499 | else viewResult.rows = rows; 500 | 501 | resobj = cvr.Sandbox.list(req, viewResult); 502 | 503 | // convert vobj to valid response 504 | res.status(resobj.code || 200); 505 | res.set(resobj.headers); 506 | res.send(resobj.body); 507 | } 508 | else _fail(req,res,{error:"error", reason:err.reason}, err.statusCode) 509 | } 510 | }, 511 | 512 | 513 | rows: function (req, res) { 514 | // Get and acl-filter rows 515 | var db = req.params.db, 516 | dbv = cvr.db[db], 517 | path = req.params, 518 | jsopts, opts, 519 | rows=[], 520 | p = _gen(req, {}, true); 521 | 522 | if ( 523 | cvr.lib.getref(dbv.viewnames, path.ddoc+'.'+path.view) == 'reduce' 524 | && req.query.reduce != 'false' 525 | ) { 526 | 527 | // We have reduce 528 | 529 | if (isO(req.query)) jsopts = _unjsonQuery(req.query); 530 | 531 | opts = Object.merge( 532 | Object.reject(jsopts, ['attachments', 'include_docs', 'format','reduce','group','group_level']), 533 | req.body && isA(req.body.keys) ? {keys: req.body.keys} : {}, 534 | true 535 | ); 536 | opts.reduce = false; 537 | 538 | dbv.nano.view(path.ddoc, path.view, opts, _reduce) 539 | .pipe(es.split()) 540 | .pipe(es.mapSync(function (data) { 541 | var id, d; 542 | 543 | // detect id 544 | id = (data.to(trimPipe).match(/^\{[^\{]*"id":"(.+?)","/) || []).last(); 545 | if (id && cvr.ACL.doc(req.session, db, id)._r) { 546 | // detect key, need to parse JSON 547 | try { 548 | d = JSON.parse(data.last() == ',' ? data.to(-1) : data); 549 | } catch (e) {} 550 | 551 | if (undefined !== d) rows.push(d); 552 | } 553 | return ''; 554 | })); 555 | 556 | } else { 557 | 558 | // No reduce 559 | 560 | if (req.isLong) { 561 | // Potentially long request, 562 | // use pipe ACL (no compression) 563 | var prev = null; 564 | req.pipe(cvr.request(p)) 565 | .pipe(es.split()) 566 | .pipe(es.mapSync(function (data) { 567 | var end = ( ']}' === data ), 568 | id, ok = false, 569 | tosend = ''; 570 | 571 | if (!data.length || end || data.last() === '[') ok = true; 572 | else { 573 | // try to detect id without parsing json 574 | id = (data.to(trimPipe).match(/^(data:)?\{[^\{]*"id":"(.+?)","/) || []).last(); 575 | if (id) ok = !!cvr.ACL.doc(req.session, req.params.db, id)._r; 576 | else ok = true; 577 | } 578 | 579 | if (ok) { 580 | // Manipulations to trim off last comma before ]} 581 | if (end) { 582 | if (prev.last() === ',') tosend = prev.to(-1); 583 | else tosend = prev; 584 | tosend += '\n' + data; 585 | prev = ''; 586 | } 587 | else { 588 | tosend = prev; 589 | prev = data; 590 | } 591 | } 592 | 593 | if (ok && null !== tosend) return tosend + '\n'; 594 | else return void 0; 595 | })) 596 | .pipe(res); 597 | } 598 | 599 | else { 600 | // If request has no include_docs & attachments, 601 | // and have some range keys, 602 | // use full-fetch and one-time check. 603 | // Employs compression and generally faster then pipe. 604 | cvr.Request(p).done(function (data) { 605 | var d, r; 606 | try { 607 | d = isS(data[1]) ? JSON.parse(data[1]) : data[1]; 608 | } catch (e) { 609 | } 610 | if (d && d.rows) { 611 | r = {total_rows: d.total_rows, offset: (d).offset || 0, rows: []} 612 | r.rows = cvr.ACL.rows( 613 | req.session, 614 | db, 615 | d.rows, 616 | '_r', 617 | req.method == "POST" && p.body.keys 618 | ); 619 | _send(req, res, [data[0], r]); 620 | d = r = null; 621 | } 622 | else _send(req, res, data); 623 | }); 624 | } 625 | } 626 | 627 | //---------------------------- 628 | 629 | function _reduce(err){ 630 | if (!err) { 631 | res.set(req.h); 632 | res.send(cvr.Sandbox.reduce(req,rows)); 633 | } 634 | else _fail(req,res,{error:"error", reason:err.reason}, err.statusCode) 635 | } 636 | }, 637 | 638 | 639 | revs: function (req, res) { 640 | // get and acl-filter revs-diff or missing-revs 641 | var db = req.params.db; 642 | cvr.Request(_gen(req, { body: cvr.ACL.object(req.session, db, req.body, '_r') }, true)) 643 | .done(function (data) { 644 | _send(req, res, data); 645 | }); 646 | }, 647 | 648 | 649 | test: function (req, res) { 650 | res.send({path: req.path, params: req.params, query: req.query, body: req.body, headers:req.headers}); 651 | } 652 | }; 653 | 654 | 655 | // Build router 656 | 657 | for (i in routes) { 658 | routes[i].forEach(function (e) { 659 | var args = [e.path].add(e.ops.map(function (op) { 660 | return actors[op]; 661 | })).add(actors.error); 662 | R[i].apply(R, args); 663 | }) 664 | } 665 | 666 | return R; 667 | 668 | 669 | // ##### SERVICE FNS ######## 670 | 671 | function _jsonQuery(obj) { 672 | var i, r = isA(obj)?[]:{}; 673 | for ( i in obj) { 674 | if (/^(start\-?key|end\-?key|key)$/.test(i)) r[i] = JSON.stringify(obj[i]); 675 | else r[i] = obj[i]; 676 | } 677 | return r; 678 | } 679 | 680 | 681 | //---------------------------- 682 | 683 | function _unjsonQuery(obj0) { 684 | var i, tmp, jsopts = {}, obj = obj0||{}; 685 | for (i in obj) { 686 | tmp = void 0; 687 | if (/^(start\-?key|end\-?key|key)$/.test(i)) { 688 | try { 689 | tmp = JSON.parse(req.query[i]); 690 | } catch (e) {} 691 | if (tmp!==void 0) jsopts[i] = tmp; 692 | } 693 | else jsopts[i] = obj[i]; 694 | } 695 | return jsopts; 696 | } 697 | 698 | 699 | //---------------------------- 700 | 701 | function _gen(req, obj, forceJSON) { 702 | // Generates obj for request.js 703 | var src = isO(obj) ? obj : {}, 704 | p = { 705 | url: couch + req.url, 706 | headers: req.h, 707 | method: req.method 708 | }; 709 | 710 | Object.merge(p, src, true); 711 | 712 | if (isO(req.body) && p.method == "POST") { 713 | p.body = p.body || req.body; 714 | p.json = true; 715 | p.headers['content-type'] = 'application/json'; 716 | } 717 | 718 | if (forceJSON) { 719 | p.headers['content-type'] = 'application/json'; 720 | p.headers.accept = 'application/json'; 721 | } 722 | 723 | return p; 724 | 725 | } 726 | 727 | //---------------------------- 728 | 729 | function _fail(req, res, obj, code) { 730 | _sendRaw(req, res, obj || { 731 | error: "forbidden", 732 | reason: "Access denied." 733 | }, code || 403); 734 | } 735 | 736 | //---------------------------- 737 | 738 | function _sendRaw(req, res, data, code) { 739 | res.set(req.h); 740 | res.status(code || 200).send(data); 741 | } 742 | 743 | //---------------------------- 744 | 745 | function _send(req, res, arr) { 746 | var d = arr[0]; 747 | res.set({ 748 | "Server": d.headers.server, 749 | "Content-Type": "application/json; charset=utf-8", 750 | "Date": d.headers.date, 751 | "Cache-Control": d.headers["cache-control"] 752 | }); 753 | if (d.headers["set-cookie"]) { 754 | res.set({"Set-Cookie": d.headers["set-cookie"]}); 755 | } 756 | else if (req.cookies && req.cookies.AuthSession) { 757 | res.cookie("AuthSession", req.cookies.AuthSession); 758 | } 759 | res.status(d.statusCode || 200); 760 | res.send(arr[1]); 761 | } 762 | } 763 | -------------------------------------------------------------------------------- /cvr/worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CoverCouch 0.1.5 Router 3 | * 4 | * 5 | * Created by ermouth on 18.01.15. 6 | */ 7 | 8 | module.exports = function (runtime) { 9 | 10 | var isA = Object.isArray, 11 | isB = Object.isBoolean, 12 | isS = Object.isString, 13 | isO = Object.isObject, 14 | isN = Object.isNumber, 15 | isR = Object.isRegExp, 16 | isF = Object.isFunction, 17 | log = require('./logger')(runtime); 18 | 19 | 20 | // Cluster 21 | 22 | if (runtime && runtime.cluster) { 23 | // monitors controllable shutdown 24 | // and starts shutdown proc 25 | process.on('message', function (msg) { 26 | log("Worker received " + msg.event + " event"); 27 | if (msg && msg.event == "shutdown") runtime.cluster.worker.kill(); 28 | }); 29 | } 30 | 31 | var Q = require("q"), 32 | conf = require('./config')(runtime), 33 | bodyParser = require('body-parser'), 34 | cookieParser = require('cookie-parser'), 35 | basicAuth = require('basic-auth'), 36 | request = require('request'), 37 | http = require('http'), 38 | https = require('https'), 39 | fs = require("fs"), 40 | nano = require('nano')(conf.couch.nano), 41 | express = require('express'), 42 | URL = require('url'), 43 | app = express(), 44 | router = express.Router(), 45 | server = http.createServer(app), 46 | couch = conf.couch.url, 47 | cvr = { 48 | log: log, 49 | ddoc: require('./ddoc')(), 50 | config: conf, 51 | db: {}, 52 | user: { 53 | _anonymous: { 54 | _id: "org.couchdb.user:_anonymous", name: "_anonymous", 55 | type: "user", roles: [], _acl: ["u-_anonymous"] 56 | } 57 | }, 58 | session: {}, 59 | bodyParser: bodyParser, 60 | nano:nano, 61 | 62 | Q: Q, 63 | URL: URL, 64 | Estream: require('event-stream'), 65 | Couch: { 66 | _pending: {}, 67 | request: Q.denodeify(nano.request), 68 | cacheDb: _cacheDb 69 | }, 70 | Request: Q.denodeify(request), 71 | request: request 72 | }; 73 | 74 | 75 | // lib 76 | require('./lib')(cvr); 77 | 78 | // ACL-related fns 79 | require('./acl')(cvr); 80 | 81 | // Sandbox for _list and reduce 82 | require('./listreduce')(cvr); 83 | 84 | // Middleware 85 | ([ 86 | require('./rater')(conf), 87 | require('compression')({ threshold: 4096 }), 88 | cookieParser(), 89 | function (req, res, next) { 90 | // Identify user 91 | req.basicAuth = basicAuth(req); 92 | _userBySession(req) 93 | .done(function () { 94 | next(); 95 | }); 96 | }, 97 | function (req, res, next) { 98 | // CORS and other headers, 99 | // unjsonned query 100 | res.set(conf.headers); 101 | if (conf.origins && conf.origins[req.headers.origin]) { 102 | res.set('Access-Control-Allow-Origin', req.headers.origin); 103 | } 104 | next(); 105 | }, 106 | require('./router')(router, cvr) 107 | ]) 108 | .forEach(function (e) { 109 | app.use(conf.server.mount, e); 110 | }); 111 | 112 | // -- end middleware --- 113 | 114 | 115 | // ##### PRELOAD ##### 116 | _readUsers(conf.couch.users) 117 | .then(function () { 118 | return Q.denodeify(nano.db.list)() 119 | }) 120 | .then(_stashDbs) 121 | .then(_followCouch) 122 | .done(function () { 123 | server.listen(conf.server.port); 124 | log("CoverCouch start"); 125 | }); 126 | 127 | 128 | // ##### END INIT ##### 129 | 130 | //---------------------------- 131 | 132 | function _followCouch() { 133 | var pi = Q.defer(), 134 | feed = nano.followUpdates({since: "now"}); 135 | 136 | feed.on('change', function (c) { 137 | var msg = false, 138 | db = c.db_name, 139 | t = c.type; 140 | if (t == 'created') { 141 | cvr.db[db] = _newDb(db); 142 | _cacheDb(db); 143 | msg = true; 144 | 145 | } else if (t == 'deleted') { 146 | if (cvr.db[db].feed) cvr.db[db].feed.stop(); 147 | cvr.db[db] = undefined; 148 | msg = true; 149 | } 150 | if (msg) log("CouchDB change: " + t + " " + db); 151 | }); 152 | 153 | feed.on('error', function () { 154 | // swallow errors 155 | }); 156 | 157 | feed.follow(); 158 | pi.resolve(); 159 | return pi.promise; 160 | } 161 | 162 | //---------------------------- 163 | 164 | function _userBySession(req, force) { 165 | var cookie = req.cookies.AuthSession || "", 166 | pi = Q.defer(), 167 | p, 168 | h = {headers: {'accept-encoding': 'utf-8'}}, 169 | sid = (req.basicAuth ? 170 | new Buffer(req.basicAuth.name + ':' + req.basicAuth.pass).toString('base64') 171 | : 172 | cookie 173 | ); 174 | 175 | 176 | if ( 177 | !force 178 | && cvr.session[sid] 179 | && cvr.session[sid].stamp > Date.now()-conf.couch.renewSessionInterval*1e3 180 | ) { 181 | req.session = cvr.session[sid]; 182 | _h(); 183 | pi.resolve(); 184 | } 185 | else { 186 | p = { 187 | url: couch + '/_session', 188 | headers: { 189 | 'Content-Type': 'application/json', 190 | Accept: 'application/json' 191 | } 192 | }; 193 | if (req.basicAuth) h.headers.Authorization = "Basic " + sid; 194 | else h.headers.Cookie = 'AuthSession=' + (cookie || ""); 195 | 196 | cvr.Request(Object.merge(p, h, true)).done(function (data) { 197 | var ok = true, 198 | d = JSON.parse(data[1]), 199 | u, c, s; 200 | if (d && d.userCtx) { 201 | u = d.userCtx; 202 | c = sid; 203 | if (u.name != null) { 204 | //save user/session 205 | s = { id: c, stamp: Date.now(), user: u.name, headers: h.headers}; 206 | if (cvr.user[u.name] && !cvr.user[u.name].inactive) { 207 | cvr.user[u.name]._userCtx = Object.clone(u, true); 208 | cvr.session[c] = s; 209 | } else { 210 | ok = false; 211 | cvr.session[c] = void 0; 212 | } 213 | } 214 | else if (c) { 215 | // drop session 216 | cvr.session[c] = void 0; 217 | } 218 | } 219 | 220 | if (ok) { 221 | req.session = cvr.session[c] || {id: c, stamp: Date.now(), user: "_anonymous", h: {}}; 222 | _h(); 223 | pi.resolve(data); 224 | } 225 | else { 226 | req.session = null; 227 | pi.reject(data); 228 | } 229 | }); 230 | } 231 | 232 | return pi.promise; 233 | 234 | function _h() { 235 | req.h = Object.merge( 236 | Object.clone( 237 | Object.reject(req.headers, ["Authorization", "Cookie", 'accept-encoding', 'content-length']), 238 | true 239 | ), 240 | req.session.h, 241 | true 242 | ); 243 | } 244 | } 245 | 246 | 247 | //---------------------------- 248 | 249 | function _readUsers(usersDb) { 250 | var udb = nano.use(usersDb), 251 | ulist = Q.denodeify(udb.list), 252 | pi = ulist({include_docs: true}); 253 | 254 | pi.then(function (d) { 255 | 256 | // Memoize users 257 | _stashUsers(d[0].rows); 258 | 259 | // Follow _users db 260 | var feed = udb.follow({since: "now", include_docs: true}); 261 | feed.on('change', function (a) { 262 | var id = a.id, u = a.doc; 263 | if (/^org\.couchdb\.user:[a-z0-9_]+$/.test(id)) { 264 | if (a.deleted) delete cvr.user[u.name]; 265 | else _stashUsers([a]) 266 | } 267 | }); 268 | feed.follow(); 269 | 270 | }); 271 | 272 | return pi; 273 | } 274 | 275 | 276 | //---------------------------- 277 | 278 | function _stashUsers(rows) { 279 | rows.forEach(function (obj) { 280 | var e = obj.doc; 281 | if (/^org\.couchdb\.user:[a-z0-9_]+$/.test(e._id) && e.type == "user") { 282 | var u = Object.clone(e, true); 283 | if (u.password === null) { 284 | u.admin = true; 285 | u.roles = u.roles.union('_admin'); 286 | } 287 | u._acl = ['r-*', 'u-' + u.name].union(u.roles.map(function (e) { 288 | return 'r-' + e; 289 | })) 290 | cvr.user[u.name] = u; 291 | } 292 | }); 293 | log("Cached " + Object.size(cvr.user) + " users") 294 | } 295 | 296 | 297 | //---------------------------- 298 | 299 | function _stashDbs(data) { 300 | var i, tmp, pre = {}, dbs = data[0], all = [], pi = Q.defer(); 301 | dbs.forEach(function (e) { 302 | cvr.db[e] = _newDb(e); 303 | }); 304 | 305 | for (i = 0; i < conf.couch.preload.length; i++) { 306 | tmp = conf.couch.preload[i]; 307 | pre[tmp] = true; 308 | if (cvr.db[tmp]) all.push(_cacheDb(tmp, true)); 309 | } 310 | 311 | dbs.forEach(function (e) { 312 | if (!pre[e]) all.push(_cacheDbDdocs(e)); 313 | }); 314 | 315 | Q.all(all).done(function (data) { 316 | log(data.length + " DBs precached"); 317 | pi.resolve(); 318 | }); 319 | 320 | return pi.promise; 321 | } 322 | 323 | //---------------------------- 324 | 325 | function _newDb(name) { 326 | return { 327 | name:name, 328 | acl: {}, 329 | ddoc: {}, 330 | viewnames:{}, 331 | cached: false, 332 | noacl: false, 333 | isforall: true, 334 | restricted: false, 335 | nano: nano.use(name), 336 | feed: null 337 | } 338 | } 339 | 340 | //---------------------------- 341 | 342 | function _cacheDbDdocs (db, create, ddockey) { 343 | var dbv = cvr.db[db], 344 | pi = Q.defer(); 345 | 346 | Q.denodeify(dbv.nano.list)({ 347 | include_docs: true, 348 | startkey:ddockey || "_design/", 349 | endkey:ddockey || "_design0" 350 | }) 351 | .then(function (all) { 352 | if (all[0].rows.length) _unwindDdocs(dbv, all[0].rows); 353 | 354 | if (dbv.ddoc['_design/acl']) log('Found _design/acl for ' + db); 355 | 356 | if (create && !dbv.ddoc['_design/acl']) { 357 | // Create ddoc 358 | Q.denodeify(dbv.nano.insert)(JSON.parse(cvr.lib.json(cvr.ddoc))) 359 | .then(function () { 360 | log('Created _design/acl for ' + db); 361 | _cacheDbDdocs(db, false, '_design/acl'). 362 | then(function () { 363 | pi.resolve(); 364 | }); 365 | }); 366 | } 367 | else pi.resolve(); 368 | }); 369 | 370 | return pi.promise; 371 | } 372 | 373 | //---------------------------- 374 | 375 | function _cacheDb(db, create) { 376 | var dbv = cvr.db[db], 377 | dbc = dbv.nano, 378 | pi = Q.defer(), 379 | _view = Q.denodeify(dbc.view), 380 | _list = Q.denodeify(dbc.list); 381 | 382 | if (cvr.Couch._pending[db]) return cvr.Couch._pending[db]; 383 | else cvr.Couch._pending[db] = pi.promise; 384 | 385 | _cacheDbDdocs (db, create) 386 | .then(function () { 387 | if (!dbv.noacl) { 388 | _view("acl", "acl", {startkey: null, endkey: []}) 389 | .then(function (all) { 390 | all[0].rows.forEach(function (e) { 391 | dbv.acl[e.id] = e.value; 392 | }); 393 | dbv.cached = true; 394 | pi.resolve(dbv); 395 | }); 396 | } else { 397 | dbv.cached = true; 398 | pi.resolve(dbv); 399 | } 400 | }); 401 | 402 | pi.promise.then(function () { 403 | 404 | // Follow bucket 405 | 406 | cvr.Couch._pending[db] = undefined; 407 | log( 408 | "Cached DB " 409 | +db+'. ' 410 | +Object.size(dbv.ddoc)+' design docs, ' 411 | +(dbv.noacl?'no ACL.':Object.size(dbv.acl)+' ACL docs read.')); 412 | var feed = dbc.follow({since: "now"}); 413 | feed.on('change', function (a) { 414 | var id = a.id, 415 | ddoc = id === "_design/acl"; 416 | 417 | if (a.deleted) { 418 | if (dbv.acl[id]) dbv.acl[id].s = +a.seq; 419 | if (ddoc) dbv.ddoc[id] = undefined; 420 | } 421 | else { 422 | // Update ACL 423 | cvr.ACL.load(db, id, a.seq); 424 | // Reload if ddoc 425 | if (ddoc) { 426 | _list({include_docs: true, startkey: id, endkey: id}) 427 | .then(function (data) { 428 | log('Updated ' + id + ' for DB ' + db); 429 | _unwindDdocs(dbv, data[0].rows); 430 | }); 431 | } 432 | } 433 | }); 434 | dbv.feed = feed; 435 | feed.follow(); 436 | }); 437 | 438 | return pi.promise; 439 | 440 | } 441 | 442 | //---------------------------- 443 | 444 | function _unwindDdocs(dbv, docs) { 445 | 446 | // Prepare some ddoc fields 447 | 448 | var i, j, tmp, dacl, racl, ddoc; 449 | for (i = 0; i < docs.length; i++) { 450 | try { 451 | tmp = cvr.lib.unjson(Object.clone(docs[i].doc, true)); 452 | dbv.ddoc[tmp._id] = tmp; 453 | 454 | // Enumerate map/reduce 455 | if (tmp.views && Object.size(tmp.views)) { 456 | ddoc = tmp._id.split("/")[1]; 457 | dbv.viewnames[ddoc] = {}; 458 | for (j in tmp.views) { 459 | if (isF(tmp.views[j].map)) { 460 | if ( 461 | isF(tmp.views[j].reduce) 462 | || 463 | /^_(sum|count|stats)$/.test(tmp.views[j].reduce) 464 | ) { 465 | dbv.viewnames[ddoc][j] = "reduce"; 466 | } 467 | else dbv.viewnames[ddoc][j] = "map"; 468 | } 469 | } 470 | } 471 | 472 | // Acl for later parse 473 | if (tmp._id == "_design/acl") { 474 | dacl = dbv.ddoc[tmp._id]; 475 | } 476 | } catch (e) { 477 | console.log(e.stack, e.message) 478 | } 479 | } 480 | if (dacl) { 481 | // unwind dbacl rules 482 | if (isA(dacl.dbacl)) dacl.dbacl = cvr.ACL.unwind(dacl.dbacl); 483 | if (isO(dacl.restrict)) { 484 | dbv._restrict = {}; 485 | racl = dacl.restrict; 486 | if (isA(racl["*"])) dacl.restrict["*"] = cvr.ACL.unwind(racl["*"]); 487 | for (i in racl) if (i != "*") { 488 | var pair, rule, rules = racl[i], dest = []; 489 | if (isO(rules)) for (j in rules) { 490 | rule = rules[j]; 491 | if (isA(rule)) { 492 | pair = []; 493 | try { 494 | pair[0] = new RegExp( 495 | RegExp.escape( 496 | j.replace('*', 'ᴥ').replace('+', 'ᴣ') 497 | ) 498 | .replace('ᴥ', '.+') 499 | .replace('ᴣ', '[^\\/]+') 500 | ); 501 | pair[1] = cvr.ACL.unwind(rule); 502 | } catch (e) { 503 | } 504 | if (pair.length == 2) dest.push(pair); 505 | } 506 | } 507 | if (dest.length) dbv._restrict[i.toUpperCase()] = dest; 508 | } 509 | dbv.restricted = !!Object.size(dbv._restrict); 510 | } 511 | } 512 | else dacl = dbv.ddoc["_design/acl"]; 513 | 514 | dbv.noacl = !dacl || !isF(cvr.lib.getref(dacl, "views.acl.map")); 515 | dbv.isforall = !dacl || !isO(cvr.lib.getref(dacl, "restrict.*")); 516 | 517 | dacl = tmp = null; 518 | } 519 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "covercouch", 3 | "description": "Read/write/delete per-document ACL for CouchDB.", 4 | "version": "0.1.5", 5 | "homepage": "https://github.com/ermouth/covercouch", 6 | "author": { 7 | "name": "Dmitri Tabanin", 8 | "email": "ermouth@gmail.com" 9 | }, 10 | "keywords":[ 11 | "couchdb", 12 | "acl", 13 | "nosql", 14 | "proxy" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/ermouth/covercouch.git" 19 | }, 20 | "scripts": { 21 | "start": "node ./covercouch.js" 22 | }, 23 | "engines": { 24 | "node": ">= 0.10.0" 25 | }, 26 | "dependencies": { 27 | "sugar": "~1.4.1", 28 | "memory-rate": ">=0.0.7", 29 | "basic-auth": "^1.0.0", 30 | "event-stream": "3.2.x", 31 | "q": ">=1.1.0", 32 | "cookie-parser": "^1.3.0", 33 | "compression": "^1.3.0", 34 | "body-parser": "^1.10.0", 35 | "request": "^2.51.0", 36 | "express": "^4.10.0", 37 | "nano": "^6.0.2" 38 | }, 39 | "files":[ 40 | "covercouch.js", 41 | "README.md", 42 | "cvr/" 43 | ], 44 | "license":"MIT" 45 | } 46 | 47 | --------------------------------------------------------------------------------