From be6d46f5c3da58388b479c62ef7bae0f87dfba8c Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Sat, 19 Nov 2011 15:38:00 -0800 Subject: [PATCH] various model cleanup, permissions, unique key bugfix --- lib/app.js | 3 +- lib/db.js | 1 + lib/model.js | 145 ++++++++++++++++++++++++++------ lib/plugins/app/app.js | 7 ++ lib/plugins/app/index.js | 3 + lib/plugins/search/index.js | 2 +- lib/plugins/settings/index.js | 28 ++++-- lib/plugins/settings/setting.js | 4 +- lib/plugins/user/index.js | 34 +++----- lib/plugins/user/user.js | 8 ++ public/test/deployd.test.js | 17 +++- 11 files changed, 189 insertions(+), 63 deletions(-) diff --git a/lib/app.js b/lib/app.js index 577f256..2cc76cb 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,5 +1,6 @@ var express = require('express') - , app = express.createServer(); + , app = express.createServer() +; // expose the app as a module so everyone can use it module.exports = app; diff --git a/lib/db.js b/lib/db.js index 64d4437..5b04f7a 100644 --- a/lib/db.js +++ b/lib/db.js @@ -29,6 +29,7 @@ module.exports = { find: ready(function(model) { var query = model.toQuery() + , allows = model.allowed , promise ; diff --git a/lib/model.js b/lib/model.js index 571fbe7..46e08ba 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1,9 +1,9 @@ var Model , states = { - ready: 0, - read: 1, - write: 2, - remove: 3 + ready: 'ready', + read: 'read', + write: 'write', + remove: 'remove' } , EventEmitter = require('events').EventEmitter , db = require('./db') @@ -23,17 +23,29 @@ Model = module.exports = emitter.spawn({ }, sync: function(state) { - this.state = state; + var model = this; + model.state = state; + + // prevent a bad state, errors and read/write/remove states + // do not mix - when a model has errors its state is ready + if(this.hasErrors()) { + this.state = states.ready; + } + + this.emit('change:state'); + + // nothing else needs to happen if the model is ready + if(model.state === states.ready) return; switch(state) { case states.read: - db.find(this); + db.find(model); break; case states.write: - db.upsert(this); + db.upsert(model); break; case states.remove: - db.remove(this); + db.remove(model); break; } }, @@ -63,12 +75,7 @@ Model = module.exports = emitter.spawn({ }, save: function() { - if(this.isValid()) { - this.sync(states.write); - } else { - // manually continue the sync - this.emit('change:state'); - } + this.sync(states.write); return this; }, @@ -83,22 +90,90 @@ Model = module.exports = emitter.spawn({ }, refresh: function(changes) { + var model = this; + if(Array.isArray(changes)) { changes = changes[0]; } - if(!changes) { - this.error('Does not exist', 'Not Found'); - changes = {}; + if(!changes && !model.hasErrors()) { + model.error('Does not exist', 'Not Found'); } + + // reset attributes + model.attributes = {}; + model.set(changes || model.attributes); - this.set(changes, true); - this.state = states.ready; - this.emit('change:state'); - return this; + model.isAllowed(model.state, function() { + model.sync(states.ready); + }); + + return model; }, strict: true, + + allowed: { + read: 'super-user', + write: 'super-user', + remove: 'super-user', + special: { + _id: 'super-user' + } + }, + + isAllowed: function(action, fn) { + var permissions = this.allowed + , rights = permissions[action] + , actor = this.actor() + , allowed = true + , model = this + ; + + if(permissions && rights && rights !== 'public') { + + if(rights === 'creator') { + if(actor !== this.get('creator') && actor === this.get('_id')) { + model.error('The current user must be the creator to ' + action, 'Not Allowed'); + } + } else { + // check permission against actor + Model + .spawn({collection: 'users', allowed: false}) + .set({_id: actor}) + .notify(function(json) { + allowed = !!(json.groups && json.groups[rights]); + if(!allowed) + model.error('The current user does not have permissions to ' + action, 'Not Allowed'); + fn(); + }) + .fetch() + ; + + return; + } + } + + // default to responding + fn(); + }, + + for: function(req) { + if(req.session && req.session.user) { + this.actor(req.session.user._id); + } + + return this; + }, + + actor: function(id) { + if(id) { + this._actor = id; + return this; + } else { + return this._actor; + } + }, isValid: function(key, value) { var _self = this @@ -127,14 +202,14 @@ Model = module.exports = emitter.spawn({ isValid = validator(value, error); - if(!isValid) this.error('Wrong type for ' + key + '. Expected: "' + type + '"', 'Validation'); + if(!isValid) this.error('Wrong type for "' + key + '". Expected: "' + type + '"', 'Validation'); return isValid; }, - set: function(changes, reset) { + set: function(changes) { var _self = this; - reset && (_self.attributes = {}); + Object.getOwnPropertyNames(changes).forEach(function(p) { if(_self.attributes[p] != changes[p]) { _self.attributes[p] = changes[p]; @@ -178,8 +253,12 @@ Model = module.exports = emitter.spawn({ return this; }, + hasErrors: function() { + return (this.errors && this.errors.length > 0); + }, + toJSON: function() { - if(this.errors && this.errors.length > 0) return _.clone({errors: this.errors}); + if(this.hasErrors()) return _.clone({errors: this.errors}); return _.clone(this.attributes); }, @@ -203,22 +282,32 @@ Model = module.exports = emitter.spawn({ return this; }, + unlock: function() { + // allow anyone to edit this specific model + // this might be removed from the final API + this.allowed = false; + return this; + }, + + plugin: 'graph', + updateSettings: function() { var settings = Model.spawn() , _self = this ; + settings.unlock(); settings.collection = 'settings'; if(this.description) { settings - .find({type: this.collection}) - .set({type: this.collection, description: this.description}) + .find({name: this.name || this.collection}) + .set({name: this.name || this.collection, description: this.description, plugin: this.plugin}) .notify(function(json) { if(json && json._id) { delete json._id; _self.description = _self.description || {}; - _self.description.extend(json); + _self.description.extend(json.description); // update any special properties _self.configure(_self.description, function(err) { diff --git a/lib/plugins/app/app.js b/lib/plugins/app/app.js index 554c4a7..bb61b43 100644 --- a/lib/plugins/app/app.js +++ b/lib/plugins/app/app.js @@ -8,6 +8,7 @@ var Model = require('../../model') module.exports = Model.spawn({ collection: 'apps', + plugin: 'app', description: { name: {type: 'string', unique: true}, @@ -16,6 +17,12 @@ module.exports = Model.spawn({ host: 'string' }, + allowed: { + read: 'creator', + write: 'creator', + remove: 'creator' + }, + save: function() { var name = this.get('name') , host = this.toHostName() diff --git a/lib/plugins/app/index.js b/lib/plugins/app/index.js index 0aa1501..296c438 100644 --- a/lib/plugins/app/index.js +++ b/lib/plugins/app/index.js @@ -17,6 +17,7 @@ app.post('/app', function(req, res) { App .spawn() + .for(req) .set({name: req.param('name'), creator: me}) .notify(res) .save() @@ -26,6 +27,7 @@ app.post('/app', function(req, res) { app.get('/app/:id', function(req, res) { App .spawn() + .for(req) .set({_id: req.param('id')}) .notify(res) .fetch() @@ -35,6 +37,7 @@ app.get('/app/:id', function(req, res) { app.del('/app/:id', function(req, res) { App .spawn() + .for(req) .notify(res) .remove() ; diff --git a/lib/plugins/search/index.js b/lib/plugins/search/index.js index d8218ec..9789e36 100644 --- a/lib/plugins/search/index.js +++ b/lib/plugins/search/index.js @@ -4,7 +4,7 @@ var app = require('../../app') app.all('/search/:type?', function(req, res) { - var c = Collection.spawn() + var c = Collection.spawn().for(req) , find = req.param('find') , select = req.param('select') , type = req.param('type') diff --git a/lib/plugins/settings/index.js b/lib/plugins/settings/index.js index 60fb3d0..d0af2d1 100644 --- a/lib/plugins/settings/index.js +++ b/lib/plugins/settings/index.js @@ -7,25 +7,35 @@ var app = require('../../app') app.get('/settings', function(req, res) { Settings .spawn() + .for(req) .notify(res) .fetch() ; }); -app.get('/settings/:type', function(req, res) { +app.get('/settings/:name', function(req, res) { Setting .spawn() - .find({type: req.param('type')}) + .for(req) + .find({name: req.param('name')}) .notify(res) .fetch() ; }); +var Model = require('../../model').spawn(); +Model.allowed = false; +Model.collection = 'system.indexes'; +Model.notify(function(data) { + config.indexes = data; + + Setting + .spawn() + .find({name: 'app'}) + .set(config) + .set({name: 'app'}) + .save() + ; +}).fetch(); + // create or update app settings -Setting - .spawn() - .find({type: 'app'}) - .set(config) - .set({type: 'app'}) - .save() -; \ No newline at end of file diff --git a/lib/plugins/settings/setting.js b/lib/plugins/settings/setting.js index 2f469ce..918a22a 100644 --- a/lib/plugins/settings/setting.js +++ b/lib/plugins/settings/setting.js @@ -3,9 +3,11 @@ var Model = require('../../model'); module.exports = Model.spawn({ collection: 'settings', + plugin: 'setting', description: { - type: {type: 'string', unique: true}, + name: {type: 'string', unique: true}, + type: 'string', description: 'object' }, diff --git a/lib/plugins/user/index.js b/lib/plugins/user/index.js index 4988bc4..c0e030e 100644 --- a/lib/plugins/user/index.js +++ b/lib/plugins/user/index.js @@ -3,9 +3,10 @@ var app = require('../../app') , Users = require('./users') ; -function user(action, params, res) { +function user(action, params, req, res) { User .spawn() + .for(req) .set(params) .notify(res) [action]() @@ -13,11 +14,11 @@ function user(action, params, res) { } app.post('/user', function(req, res) { - user('save', req.body, res); + user('save', req.body, req, res); }); app.post('/user/login', function(req, res) { - user('login', req.body, { + user('login', req.body, req, { send: function(u) { u.auth = req.sessionID; res.send(req.session.user = u); @@ -27,33 +28,25 @@ app.post('/user/login', function(req, res) { app.get('/user/logout', function(req, res) { req.session.destroy(function() { - res.send({auth: null}) - }) + res.send({auth: null}); + }); }); app.get('/me', function(req, res) { - user('fetch', {email: req.session.user.email}, res); + user('fetch', {email: req.session.user && req.session.user.email}, req, res); }); app.del('/me', function(req, res) { var u = req.session.user; - if(u) user('remove', u, res); + if(u) user('remove', u, req, res); }); -app.get('/user/:uid', function(req, res) { - user('fetch', {email: req.param('uid')}, res); +app.get('/user/:email', function(req, res) { + user('fetch', {email: req.param('email')}, req, res); }); -app.post('/user/:user/group', function(req, res) { - var user = req.param('user'); - if(user) { - User - .spawn() - .set({email: user, group: req.param('group')}) - .notify(res) - .save() - ; - } +app.post('/user/:email/group', function(req, res) { + user('save', {email: req.param('email'), group: req.param('group')}, req, res); }); app.get('/users', function(req, res) { @@ -61,7 +54,8 @@ app.get('/users', function(req, res) { .spawn({ query: req.params }) + .for(req) .notify(res) .fetch() ; -}); \ No newline at end of file +}); diff --git a/lib/plugins/user/user.js b/lib/plugins/user/user.js index 5bcd73e..f3f87d9 100644 --- a/lib/plugins/user/user.js +++ b/lib/plugins/user/user.js @@ -7,6 +7,8 @@ module.exports = Model.spawn({ collection: 'users', + plugin: 'user', + description: { email: {type: 'email', unique: true}, password: 'password', @@ -16,6 +18,12 @@ module.exports = Model.spawn({ group: 'string' }, + allowed: { + read: 'creator', + write: 'creator', + remove: 'creator' + }, + toQuery: function() { var email = this.get('email') , q = {email: email} diff --git a/public/test/deployd.test.js b/public/test/deployd.test.js index 8148115..f4fb6e5 100644 --- a/public/test/deployd.test.js +++ b/public/test/deployd.test.js @@ -67,7 +67,7 @@ var tests = { }, '5. searching users': { - route: '/search?type=users&find={"uid": "skawful@gmail.com"}', + route: '/search?type=users&find={"email": "skawful@gmail.com"}', expect: { results: 'toExist', errors: 'toNotExist' @@ -118,7 +118,7 @@ var tests = { }, '10. add a user to group': { - route: '/user/test@user.com/group', + route: '/user/' + user.email + '/group', data: {group: 'author'}, expect: { group: 'author' @@ -139,12 +139,23 @@ var tests = { expect: { results: 'toContainOne' } + }, + + '13. only 1 user': { + route: '/search/users', + data: {}, + expect: { + results: 'toContainOne' + } } }; var testNames = Object.keys(tests) - , sorted = testNames.sort() + , sorted = testNames.sort(function(a, b) { + // sort by the number in front of the test name + return (Number(a.split('.')[0]) < Number(b.split('.')[0])) ? -1 : 1; + }) ; // execute tests