├── .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 |
--------------------------------------------------------------------------------