diff --git a/HISTORY.md b/HISTORY.md index a7cdccd..b12eb7d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,5 +7,6 @@ ### New Features - resources now support custom events by default +- new collection permission api ### Major Bugfixes \ No newline at end of file diff --git a/lib/db.js b/lib/db.js index ad084ad..71de035 100644 --- a/lib/db.js +++ b/lib/db.js @@ -418,7 +418,7 @@ Store.prototype.update = function (query, object, fn) { if(typeof query != 'object') throw new Error('update requires a query object or string id'); if(query.id) { store.identify(query); - } else { + } else { multi = true; } @@ -443,9 +443,9 @@ Store.prototype.update = function (query, object, fn) { debug('update - command', command); collection(this, function (err, col) { - col.update(query, command, {multi: multi}, function(err) { + col.update(query, command, {multi: multi}, function(err) { store.identify(query); - fn(err); + if(fn) fn(err); }, multi); }); }; diff --git a/lib/resource.js b/lib/resource.js index e1b5256..e0087fe 100644 --- a/lib/resource.js +++ b/lib/resource.js @@ -149,6 +149,68 @@ Resource.prototype.parseEvent = function (ctx) { if(ctx.url) return ctx.url.split('/')[1]; } +Resource.prototype.verifyPermissions = function (ctx, fn) { + if(ctx.req.internal || ctx.req.isRoot) return fn(); + + var required = this.requiredPermissions[ctx.method]; + var requested = ctx.permissions || this.defaultPermissions; + var isSingle = !!ctx.query.id; + var failed = false; + + if(required) { + Object.keys(required[isSingle ? 'single' : 'multi']).forEach(function (permission) { + if(!requested[permission]) { + if(ctx.server.options.env === 'development') { + error('permission denied when ' + permission + ' - to allow this action, include `allow("'+ permission +'")` in a "query" event script'); + } else { + error('permission denied when ' + permission); + } + } + }); + + if(!failed) { + fn(); + } + } else { + error('an unkown error has occured'); + } + + function error(msg) { + failed = true; + ctx.res.statusCode = 401; + ctx.done(new Error(msg)); + } +} + +Resource.prototype.setDefaultPermissions = function (ctx) { + if(ctx.permissions) return; + ctx.permissions = {}; + if(!this.defaultPermissions) return; + Object.keys(this.defaultPermissions).forEach(function (key) { + ctx.permissions[key] = true; + }); +} + +Resource.prototype.forEachPermission = function (type, fn) { + var resource = this; + + if(type === 'default') { + if(this.defaultPermissions) { + Object.keys(this.defaultPermissions).forEach(fn); + } + } else { + if(this.requiredPermissions) { + Object.keys(resource.requiredPermissions).forEach(function (event) { + Object.keys(resource.requiredPermissions[event]).forEach(function (type) { + Object.keys(resource.requiredPermissions[event][type]).forEach(function (permission) { + fn(permission, type, event); + }); + }); + }); + } + } +} + /** * Handle an incoming request. This gets called by the router. * Call `next()` if the resource cannot handle the request. @@ -177,6 +239,14 @@ Resource.prototype.handle = function (ctx, next) { ctx.end(); }; +Resource.prototype.beforeHandle = function (ctx) { + this.setDefaultPermissions(ctx); +} + +Resource.prototype.afterHandle = function (ctx) { + // hook +} + /** * Turn a resource constructor into an object ready * for JSON. It should atleast include the `type` diff --git a/lib/resources/collection/index.js b/lib/resources/collection/index.js index aaa1d3e..32f9cf0 100644 --- a/lib/resources/collection/index.js +++ b/lib/resources/collection/index.js @@ -30,11 +30,56 @@ function Collection(name, options) { if (options) { this.store = options.db && options.db.createStore(this.name); } + + this.defaultPermissions = { + 'querying multiple objects': true, + 'querying an object by id': true, + 'creating an object': true, + 'deleting an object by id': true, + 'updating an object by id': true + }; + + this.requiredPermissions = { + 'GET': { + multi: { + 'querying multiple objects': true + }, + single: { + 'querying an object by id': true + } + }, + 'POST': { + multi: { + 'creating multiple objects': true + }, + single: { + 'creating an object': true + } + }, + 'PUT': { + multi: { + 'querying multiple objects': true, + 'updating multiple objects': true + }, + single: { + 'updating an object by id': true, + 'querying an object by id': true + } + }, + 'DELETE': { + multi: { + 'deleting multiple objects': true + }, + single: { + 'deleting an object by id': true + } + } + }; } util.inherits(Collection, Resource); Collection.external = {}; Collection.prototype.clientGeneration = true; -Collection.events = ['Get', 'Validate', 'Post', 'Put', 'Delete']; +Collection.events = ['Get', 'Validate', 'Post', 'Put', 'Delete', 'Query']; Collection.dashboard = { path: path.join(__dirname, 'dashboard') @@ -171,47 +216,79 @@ Collection.prototype.sanitizeQuery = function (query) { Collection.prototype.handle = function (ctx) { // set id one wasnt provided in the query ctx.query.id = ctx.query.id || this.parseId(ctx) || (ctx.body && ctx.body.id); + + function handle(err) { + if(err) return ctx.done(err); + + if (ctx.req.method == "GET" && ctx.query.id === 'count') { + delete ctx.query.id; + this.count(ctx, ctx.done); + return; + } - if (ctx.req.method == "GET" && ctx.query.id === 'count') { - delete ctx.query.id; - this.count(ctx, ctx.done); - return; - } + if (ctx.req.method == "GET" && ctx.query.id === 'index-of') { + delete ctx.query.id; + var id = ctx.url.split('/').filter(function(p) { return p; })[1]; + this.indexOf(id, ctx, ctx.done); + return; + } + + var eventScript = this.getEventScript(ctx) + , event = this.parseEvent(ctx); + + if(eventScript) { + debug('running %s event', eventScript) + return this.run(event, eventScript, ctx); + } - if (ctx.req.method == "GET" && ctx.query.id === 'index-of') { - delete ctx.query.id; - var id = ctx.url.split('/').filter(function(p) { return p; })[1]; - this.indexOf(id, ctx, ctx.done); - return; + switch(ctx.req.method) { + case 'GET': + this.find(ctx, ctx.done); + break; + case 'PUT': + if (typeof ctx.query.id != 'string') { + return this.saveAll(ctx, ctx.done); + } + /* falls through */ + case 'POST': + this.save(ctx, ctx.done); + break; + case 'DELETE': + this.remove(ctx, ctx.done); + break; + } } - var eventScript = this.getEventScript(ctx) - , event = this.parseEvent(ctx); - - if(eventScript) { - debug('running %s event', eventScript) - return this.run(event, eventScript, ctx); - } - - switch(ctx.req.method) { - case 'GET': - this.find(ctx, ctx.done); - break; - case 'PUT': - if (typeof ctx.query.id != 'string' && !ctx.req.isRoot) { - ctx.done("must provide id to update an object"); - break; - } - /* falls through */ - case 'POST': - this.save(ctx, ctx.done); - break; - case 'DELETE': - this.remove(ctx, ctx.done); - break; + if(ctx.req.method === 'POST' || ctx.query.id || ctx.url !== '/') { + handle.call(this); + } else { + this.beforeQuery(ctx, handle.bind(this)); } }; +Collection.prototype.beforeQuery = function (ctx, fn) { + var queryScript = this.events.Query + , collection = this; + + if(queryScript) { + var domain = createDomain(this, ctx); + + domain.event = ctx.method; + domain.method = ctx.method; + domain.action = ctx.method; + domain.query = ctx.query; + domain.data = ctx.body; + + queryScript.run(ctx, domain, function (err) { + if(err) return ctx.done(err); + + collection.verifyPermissions(ctx, fn); + }); + } else { + collection.verifyPermissions(ctx, fn); + } +} + /** * Parse the `ctx.url` for an id * @@ -320,7 +397,7 @@ Collection.prototype.find = function (ctx, fn) { if(!remaining) return done(err, result); result.forEach(function (data) { // domain for onGet event scripts - var domain = createDomain(data, errors); + var domain = createDomain(collection, ctx, data, errors); collection.events.get.run(ctx, domain, function (err) { if (err) { @@ -342,7 +419,7 @@ Collection.prototype.find = function (ctx, fn) { } else { // domain for onGet event scripts data = result; - var domain = createDomain(data, errors); + var domain = createDomain(collection, ctx, data, errors); collection.events.get.run(ctx, domain, function (err) { if(err) return done(err); @@ -370,7 +447,6 @@ Collection.prototype.remove = function (ctx, fn) { , sanitizedQuery = this.sanitizeQuery(query) , errors; - if(!(query && query.id)) return fn('You must include a query with an id when deleting an object from a collection.'); store.find(sanitizedQuery, function (err, result) { if(err) { return fn(err); @@ -378,12 +454,16 @@ Collection.prototype.remove = function (ctx, fn) { function done(err) { if(err) return fn(err); - store.remove(sanitizedQuery, fn); - if(session.emitToAll) session.emitToAll(collection.name + ':changed'); + collection.verifyPermissions(ctx, function (err) { + if(err) return fn(err); + + store.remove(sanitizedQuery, fn); + if(session.emitToAll) session.emitToAll(collection.name + ':changed'); + }); } if(collection.shouldRunEvent(collection.events.Delete, ctx)) { - var domain = createDomain(result, errors); + var domain = createDomain(collection, ctx, result, errors); domain['this'] = domain.data = result; collection.events.Delete.run(ctx, domain, done); @@ -393,6 +473,20 @@ Collection.prototype.remove = function (ctx, fn) { }); }; +function buildCommands(item) { + var commands = {}; + Object.keys(item).forEach(function (key) { + if(item[key] && typeof item[key] === 'object' && !Array.isArray(item[key])) { + Object.keys(item[key]).forEach(function (k) { + if(k[0] == '$') { + commands[key] = item[key]; + } + }); + } + }); + return commands; +} + /** * Execute the onPost or onPut listener. If it succeeds, * save the given item in the collection. @@ -406,25 +500,14 @@ Collection.prototype.save = function (ctx, fn) { , store = this.store , session = ctx.session , item = ctx.body - , query = ctx.query || {} , client = ctx.dpd , errors = {}; - + if(!item) return done('You must include an object when saving or updating.'); // build command object - var commands = {}; - Object.keys(item).forEach(function (key) { - if(item[key] && typeof item[key] === 'object' && !Array.isArray(item[key])) { - Object.keys(item[key]).forEach(function (k) { - if(k[0] == '$') { - commands[key] = item[key]; - } - }); - } - }); - + var commands = buildCommands(item); item = this.sanitize(item); // handle id on either body or query @@ -440,7 +523,7 @@ Collection.prototype.save = function (ctx, fn) { fn(errors || err, item); } - var domain = createDomain(item, errors); + var domain = createDomain(collection, ctx, item, errors); domain.protect = function(property) { delete domain.data[property]; @@ -457,7 +540,7 @@ Collection.prototype.save = function (ctx, fn) { var id = query.id , sanitizedQuery = collection.sanitizeQuery(query) , prev = {}; - + store.first(sanitizedQuery, function(err, obj) { if(!obj) { if (Object.keys(sanitizedQuery) === 1) { @@ -482,7 +565,7 @@ Collection.prototype.save = function (ctx, fn) { collection.execCommands('update', item, commands); var errs = collection.validate(item); - + if(errs) return done({errors: errs}); function runPutEvent(err) { @@ -496,20 +579,26 @@ Collection.prototype.save = function (ctx, fn) { commit(); } } - + function commit(err) { if(err || domain.hasErrors()) { return done(err || errors); } + + collection.verifyPermissions(ctx, function (err) { + if(err) { + return done(err); + } - delete item.id; - store.update({id: query.id}, item, function (err) { - if(err) return done(err); - item.id = id; - - done(null, item); + delete item.id; + store.update({id: query.id}, item, function (err) { + if(err) return done(err); + item.id = id; + + done(null, item); - if(session && session.emitToAll) session.emitToAll(collection.name + ':changed'); + if(session && session.emitToAll) session.emitToAll(collection.name + ':changed'); + }); }); } @@ -523,7 +612,7 @@ Collection.prototype.save = function (ctx, fn) { } }); } - + function post() { var errs = collection.validate(item, true); @@ -531,7 +620,7 @@ Collection.prototype.save = function (ctx, fn) { // generate id before event listener item.id = store.createUniqueIdentifier(); - + if(collection.shouldRunEvent(collection.events.Post, ctx)) { collection.events.Post.run(ctx, domain, function (err) { if(err) { @@ -540,12 +629,18 @@ Collection.prototype.save = function (ctx, fn) { } if(err || domain.hasErrors()) return done(err || errors); debug('inserting item', item); - store.insert(item, done); - if(session && session.emitToAll) session.emitToAll(collection.name + ':changed'); + collection.verifyPermissions(ctx, function (err) { + if(err) return done(err); + store.insert(item, done); + if(session && session.emitToAll) session.emitToAll(collection.name + ':changed'); + }); }); } else { - store.insert(item, done); - if(session && session.emitToAll) session.emitToAll(collection.name + ':changed'); + collection.verifyPermissions(ctx, function (err) { + if(err) return done(err); + store.insert(item, done); + if(session && session.emitToAll) session.emitToAll(collection.name + ':changed'); + }); } } @@ -561,9 +656,140 @@ Collection.prototype.save = function (ctx, fn) { } }; -function createDomain(data, errors) { +Collection.prototype.saveAll = function (ctx, fn) { + var errors = {} + , results = [] + , updateBatch = [] + , objectsToUpdate + , failed + , collection = this + , query = ctx.query + , item = ctx.body + , sanitizedQuery = collection.sanitizeQuery(query) + , commands = buildCommands(item); + + this.store.find(sanitizedQuery, function (err, objects) { + if(err) return fn(err); + var remaining; + + objectsToUpdate = objects; + + if(Array.isArray(objects)) { + remaining = objects.length; + for(var i = 0; i < objects.length && !failed; i++) { + update(objects[i]); + }; + } else { + done(); + } + }); + + function update(obj) { + var id = query.id + , prev = {} + , updated = {} + , domain = createDomain(collection, ctx, obj, errors); + + // copy item + Object.keys(obj).forEach(function (key) { + updated[key] = obj[key]; + prev[key] = obj[key]; + if(item[key]) updated[key] = item[key]; + }); + Object.keys(item).forEach(function (key) { + updated[key] = item[key]; + }); + + domain['this'] = updated; + domain.data = updated; + domain.previous = prev; + + domain.protect = function(property) { + delete domain.data[property]; + }; + + domain.changed = function (property) { + if(domain.data.hasOwnProperty(property)) return true; + return false; + }; + + collection.execCommands('update', updated, commands); + + var errs = collection.validate(updated); + + if(errs) return done({errors: errs}); + + if(collection.shouldRunEvent(collection.events.Validate, ctx)) { + collection.events.Validate.run(ctx, domain, function (err) { + if(err || domain.hasErrors()) return done(err || errors); + runPutEvent(err); + }); + } else { + runPutEvent(); + } + + function runPutEvent() { + if(collection.shouldRunEvent(collection.events.Put, ctx)) { + collection.events.Put.run(ctx, domain, add); + } else { + add(); + } + } + + function add(err) { + if(err) return done(err); + collection.verifyPermissions(ctx, function (err) { + if(err) return done(err); + + updateBatch.push(updated); + + if(updateBatch.length === objectsToUpdate.length) { + done(); + } + }); + } + } + + function done(err) { + if(err) { + debug('errors: %j', err); + fn(err); + } else if(updateBatch.length) { + updateBatch.forEach(function (obj) { + var id = obj.id; + delete obj.id; + results.push(id); + collection.store.update({id: id}, obj); + }); + + fn(null, results); + } else { + fn(null, []); + } + } +} + +function createDomain(collection, ctx, data, errors) { var hasErrors = false; var domain = { + allow: function (permission) { + if(permission === '*') { + collection.forEachPermission('required', function (perm, event) { + ctx.permissions[perm] = true; + }); + } else { + ctx.permissions[permission] = true; + } + }, + prevent: function (permission) { + if(permission === '*') { + collection.forEachPermission('required', function (perm, type, event) { + ctx.permissions[perm] = false; + }); + } else { + delete ctx.permissions[permission]; + } + }, error: function(key, val) { debug('error %s %s', key, val); errors[key] = val || true; diff --git a/lib/resources/user-collection.js b/lib/resources/user-collection.js index b3adac8..115cf5c 100644 --- a/lib/resources/user-collection.js +++ b/lib/resources/user-collection.js @@ -50,96 +50,116 @@ UserCollection.dashboard = Collection.dashboard; UserCollection.prototype.handle = function (ctx) { var uc = this; - - if (ctx.req.method == "GET" && (ctx.url === '/count' || ctx.url.indexOf('/index-of') === 0)) { - return Collection.prototype.handle.apply(uc, arguments); - } - - if(ctx.url === '/logout') { - if (ctx.res.cookies) ctx.res.cookies.set('sid', null); - ctx.session.remove(ctx.done); - return; - } - + // set id one wasnt provided in the query ctx.query.id = ctx.query.id || this.parseId(ctx) || (ctx.body && ctx.body.id); - // make sure password will never be included - if(ctx.query.$fields) ctx.query.$fields.password = 0; - else ctx.query.$fields = {password: 0}; + if(ctx.method === 'POST' || ctx.query.id || ctx.url !== '/') { + handle.call(this); + } else { + this.beforeQuery(ctx, handle.bind(this)); + } - switch(ctx.req.method) { - case 'GET': - if(ctx.url === '/me') { - debug('session %j', ctx.session.data); - if(!(ctx.session && ctx.session.data && ctx.session.data.uid)) { - ctx.res.statusCode = 204; - return ctx.done(); - } - - ctx.query = {id: ctx.session.data.uid, $fields: {password: 0}}; - - return this.find(ctx, ctx.done); - } + function handle() { + if (ctx.req.method == "GET" && (ctx.url === '/count' || ctx.url.indexOf('/index-of') === 0)) { + return Collection.prototype.handle.apply(uc, arguments); + } - this.find(ctx, ctx.done); - break; - case 'POST': - if(ctx.url === '/login') { - var path = this.path - , credentials = ctx.req.body || {}; + if(ctx.url === '/logout') { + if (ctx.res.cookies) ctx.res.cookies.set('sid', null); + ctx.session.remove(ctx.done); + return; + } - debug('trying to login as %s', credentials.username); + // make sure password will never be included + if(ctx.query.$fields) ctx.query.$fields.password = 0; + else ctx.query.$fields = {password: 0}; - this.store.first({username: credentials.username}, function(err, user) { - if(err) return ctx.done(err); - - if(user) { - var salt = user.password.substr(0, SALT_LEN) - , hash = user.password.substr(SALT_LEN); - - if(hash === uc.hash(credentials.password, salt)) { - debug('logged in as %s', credentials.username); - ctx.session.set({path: path, uid: user.id}).save(ctx.done); - return; - } + switch(ctx.req.method) { + case 'GET': + if(ctx.url === '/me') { + debug('session %j', ctx.session.data); + if(!(ctx.session && ctx.session.data && ctx.session.data.uid)) { + ctx.res.statusCode = 204; + return ctx.done(); } + + ctx.query = {id: ctx.session.data.uid, $fields: {password: 0}}; + + return this.find(ctx, ctx.done); + } - ctx.res.statusCode = 401; - ctx.done('bad credentials'); - }); - break; - } - /* falls through */ - case 'PUT': - if(ctx.body && ctx.body.password) { - var salt = uuid.create(SALT_LEN); - ctx.body.password = salt + this.hash(ctx.body.password, salt); - } - var isSelf = ctx.session.user && ctx.session.user.id === ctx.query.id || ctx.body.id; - if ((ctx.query.id || ctx.body.id) && ctx.body && !isSelf && !ctx.session.isRoot && !ctx.req.internal) { - delete ctx.body.username; - delete ctx.body.password; - } + this.find(ctx, ctx.done); + break; + case 'POST': + if(ctx.url === '/login') { + var path = this.path + , credentials = ctx.req.body || {}; - function done(err, res) { - if (res) delete res.password; - ctx.done(err, res); - } + debug('trying to login as %s', credentials.username); - if(ctx.query.id || ctx.body.id) { - this.save(ctx, done); - } else { - this.store.first({username: ctx.body.username}, function (err, u) { - if(u) return ctx.done({errors: {username: 'is already in use'}}); - uc.save(ctx, done); - }); - } - break; - case 'DELETE': - debug('removing', ctx.query, ctx.done); - this.remove(ctx, ctx.done); - break; + this.store.first({username: credentials.username}, function(err, user) { + if(err) return ctx.done(err); + + if(user) { + var salt = user.password.substr(0, SALT_LEN) + , hash = user.password.substr(SALT_LEN); + + if(hash === uc.hash(credentials.password, salt)) { + debug('logged in as %s', credentials.username); + ctx.session.set({path: path, uid: user.id}).save(ctx.done); + return; + } + } + + ctx.res.statusCode = 401; + ctx.done('bad credentials'); + }); + break; + } else if(ctx.url !== '/') { + var eventScript = this.getEventScript(ctx) + , event = this.parseEvent(ctx); + + if(eventScript) { + debug('running %s event', eventScript) + return this.run(event, eventScript, ctx); + } + } + /* falls through */ + case 'PUT': + if(ctx.body && ctx.body.password) { + var salt = uuid.create(SALT_LEN); + ctx.body.password = salt + this.hash(ctx.body.password, salt); + } + var isSelf = ctx.session.user && ctx.session.user.id === ctx.query.id || ctx.body.id; + if ((ctx.query.id || ctx.body.id) && ctx.body && !isSelf && !ctx.session.isRoot && !ctx.req.internal) { + delete ctx.body.username; + delete ctx.body.password; + } + + function done(err, res) { + if (res) delete res.password; + ctx.done(err, res); + } + + if(ctx.query.id || ctx.body.id) { + this.save(ctx, done); + } else { + this.store.first({username: ctx.body.username}, function (err, u) { + if(u) return ctx.done({errors: {username: 'is already in use'}}); + if(ctx.method === 'POST') { + uc.save(ctx, done); + } else { + uc.saveAll(ctx, done); + } + }); + } + break; + case 'DELETE': + debug('removing', ctx.query, ctx.done); + this.remove(ctx, ctx.done); + break; + } } }; diff --git a/lib/router.js b/lib/router.js index f81100c..4d59565 100644 --- a/lib/router.js +++ b/lib/router.js @@ -78,7 +78,9 @@ Router.prototype.route = function (req, res) { if(resource.external && resource.external[furl]) { resource.external[furl](ctx.body, ctx, ctx.done); } else { + resource.beforeHandle(ctx); resource.handle(ctx, nextResource); + resource.afterHandle(ctx); } } else { debug('404 %s', req.url); diff --git a/test-app/public/test/collection.test.js b/test-app/public/test/collection.test.js index f54f10e..918fafc 100644 --- a/test-app/public/test/collection.test.js +++ b/test-app/public/test/collection.test.js @@ -896,9 +896,118 @@ describe('Collection', function() { afterEach(function (done) { this.timeout(10000); - cleanCollection(dpd.changed, done); + cleanCollection(dpd.todos, done); }); }); - + describe('batch PUTs', function(){ + it('should be available to internal requests', function(done) { + chain(function(next) { + dpd.todos.post({title: 'foo'}, next); + }).chain(function(next) { + dpd.todos.post({title: 'bar'}, next); + }).chain(function(next) { + dpd.todos.post({title: 'bat'}, next); + }).chain(function(next, res, err) { + dpd.todos.emit('custom', {$BATCH_PUT: 'foo bar'}, next); + }).chain(function (next, res, err) { + dpd.todos.get(function (todos) { + todos.forEach(function (todo) { + expect(todo.title).to.equal('foo bar'); + }); + + done(err); + }); + }); + }); + + afterEach(function (done) { + this.timeout(10000); + cleanCollection(dpd.todos, done); + }); + }); + + describe('custom permissions', function(){ + describe('allow("updating multiple objects")', function(){ + it('should allow batch put', function(done) { + chain(function(next) { + dpd.todos.post({title: 'foo'}, next); + }).chain(function(next) { + dpd.todos.post({title: 'bar'}, next); + }).chain(function(next) { + dpd.todos.post({title: 'bat'}, next); + }).chain(function (next, res, err) { + dpd.todos.put({}, {title: '$CUSTOM_PERMISSIONS_PUT'}, function (todos) { + dpd.todos.get(function (todos) { + todos.forEach(function (todo) { + expect(todo.title).to.equal('$CUSTOM_PERMISSIONS_PUT'); + }); + + done(err); + }) + }); + }); + }); + }); + + describe('allow("deleting multiple objects")', function(){ + it('should allow batch delete', function(done) { + chain(function(next) { + dpd.todos.post({title: 'foo'}, next); + }).chain(function(next) { + dpd.todos.post({title: 'bar'}, next); + }).chain(function(next) { + dpd.todos.post({title: 'bat'}, next); + }).chain(function (next, res, err) { + dpd.todos.del({test: '$CUSTOM_PERMISSIONS_DELETE'}, function (todos) { + dpd.todos.get(function (todos) { + expect(todos.length).to.equal(0); + done(err); + }) + }); + }); + }); + }); + + describe('prevent("*")', function(){ + it('should not allow get', function(done) { + dpd.perms.get({$PREVENT_ALL: true}, function (things, err) { + expect(err).to.exist; + done(); + }); + }); + + it('should not allow post', function(done) { + dpd.perms.post({title: 'foo'}, function (things, err) { + expect(err).to.exist; + done(); + }); + }); + + it('should not allow put', function(done) { + dpd.perms.post({title: '$ALLOW'}, function (thing, err) { + dpd.perms.put(thing.id, {title: 'bar'}, function (thing, err) { + expect(err).to.exist; + done(); + }); + }); + }); + + it('should not allow delete', function(done) { + dpd.perms.post({title: '$ALLOW'}, function (thing, err) { + dpd.perms.del(thing.id, {title: 'bar'}, function (thing, err) { + expect(err).to.exist; + done(); + }); + }); + }); + }); + + afterEach(function (done) { + this.timeout(10000); + cleanCollection(dpd.perms, function () { + cleanCollection(dpd.perms, done); + }); + }); + }); }); diff --git a/test-app/public/test/user-collection.test.js b/test-app/public/test/user-collection.test.js index eef5a09..62b35cd 100644 --- a/test-app/public/test/user-collection.test.js +++ b/test-app/public/test/user-collection.test.js @@ -279,5 +279,74 @@ describe('User Collection', function() { }); }); }); + + describe('.emit("custom", {foo: "bar"})', function(){ + it('should send a custom message', function(done) { + var input = {foo: 'bar'}; + dpd.emptyusers.emit('custom', input, function (data, err) { + expect(data).to.eql({foo: 'bar', baz: 'baz'}); + done(err); + }); + }); + + it('should respond with something beside the body sent', function(done) { + var input = {$TEST_RESPOND: true}; + dpd.emptyusers.emit('custom', input, function (data, err) { + expect(data).to.equal('foo bar bat baz'); + done(err); + }); + }); + + + afterEach(function (done) { + this.timeout(10000); + cleanCollection(dpd.users, done); + }); + }); + + describe('custom permissions', function(){ + it('should allow batch put', function(done) { + chain(function(next) { + dpd.users.post({username: 'foo', password: 'foo'}, next); + }).chain(function(next) { + dpd.users.post({username: 'bar', password: 'foo'}, next); + }).chain(function(next) { + dpd.users.post({username: 'bat', password: 'foo'}, next); + }).chain(function (next, res, err) { + dpd.users.put({test: '$CUSTOM_PERMISSIONS_PUT'}, {reputation: 22}, function (todos) { + dpd.users.get(function (users) { + users.forEach(function (user) { + expect(user.reputation).to.equal(22); + }); + + done(err); + }) + }); + }); + }); + + it('should allow batch delete', function(done) { + chain(function(next) { + dpd.users.post({username: 'foo', password: 'foo'}, next); + }).chain(function(next) { + dpd.users.post({username: 'bar', password: 'foo'}, next); + }).chain(function(next) { + dpd.users.post({username: 'bat', password: 'foo'}, next); + }).chain(function (next, res, err) { + dpd.users.del({test: '$CUSTOM_PERMISSIONS_DELETE'}, function (todos) { + dpd.users.get(function (todos) { + expect(todos.length).to.equal(0); + done(err); + }) + }); + }); + }); + + afterEach(function (done) { + this.timeout(10000); + cleanCollection(dpd.users, done); + }); + // + }); }); diff --git a/test-app/resources/empty-users/custom.js b/test-app/resources/empty-users/custom.js new file mode 100644 index 0000000..4f3e059 --- /dev/null +++ b/test-app/resources/empty-users/custom.js @@ -0,0 +1,15 @@ +dpd.todos.get({$limit: 2}, function (results) { + this.baz = 'baz'; +}); + +if(this.$TEST_RESPOND) { + dpd.todos.get({$limit: 2}, function (results) { + respond('foo bar bat baz'); + }); +} + +if(this.$BATCH_PUT) { + dpd.users.put({}, {username: this.$BATCH_PUT}, function () { + // wait + }); +} diff --git a/test-app/resources/perms/config.json b/test-app/resources/perms/config.json new file mode 100644 index 0000000..fb81821 --- /dev/null +++ b/test-app/resources/perms/config.json @@ -0,0 +1,13 @@ +{ + "type": "Collection", + "properties": { + "title": { + "name": "title", + "type": "string", + "typeLabel": "string", + "required": false, + "id": "title", + "order": 0 + } + } +} \ No newline at end of file diff --git a/test-app/resources/perms/delete.js b/test-app/resources/perms/delete.js new file mode 100644 index 0000000..3074e36 --- /dev/null +++ b/test-app/resources/perms/delete.js @@ -0,0 +1 @@ +prevent('*'); \ No newline at end of file diff --git a/test-app/resources/perms/query.js b/test-app/resources/perms/query.js new file mode 100644 index 0000000..8637de3 --- /dev/null +++ b/test-app/resources/perms/query.js @@ -0,0 +1,3 @@ +if(query.$PREVENT_ALL) { + prevent('*'); +} \ No newline at end of file diff --git a/test-app/resources/perms/validate.js b/test-app/resources/perms/validate.js new file mode 100644 index 0000000..61e691b --- /dev/null +++ b/test-app/resources/perms/validate.js @@ -0,0 +1,5 @@ +if(this.title === '$ALLOW') { + allow('*'); +} else { + prevent('*'); +} \ No newline at end of file diff --git a/test-app/resources/todos/custom.js b/test-app/resources/todos/custom.js index c353c10..4385a80 100644 --- a/test-app/resources/todos/custom.js +++ b/test-app/resources/todos/custom.js @@ -6,4 +6,10 @@ if(this.$TEST_RESPOND) { dpd.todos.get({$limit: 2}, function (results) { respond('foo bar bat baz'); }); -} \ No newline at end of file +} + +if(this.$BATCH_PUT) { + dpd.todos.put({}, {title: this.$BATCH_PUT}, function () { + // wait + }); +} diff --git a/test-app/resources/todos/query.js b/test-app/resources/todos/query.js new file mode 100644 index 0000000..f6d40d5 --- /dev/null +++ b/test-app/resources/todos/query.js @@ -0,0 +1,7 @@ +if(event === 'PUT' && this.title === '$CUSTOM_PERMISSIONS_PUT') { + allow('updating multiple objects'); +} + +if(event === 'DELETE' && query.test === '$CUSTOM_PERMISSIONS_DELETE') { + allow('deleting multiple objects'); +} \ No newline at end of file diff --git a/test-app/resources/users/query.js b/test-app/resources/users/query.js new file mode 100644 index 0000000..cb8334d --- /dev/null +++ b/test-app/resources/users/query.js @@ -0,0 +1,7 @@ +if(query.test === '$CUSTOM_PERMISSIONS_PUT') { + allow('updating multiple objects'); +} + +if(query.test === '$CUSTOM_PERMISSIONS_DELETE') { + allow('deleting multiple objects'); +} \ No newline at end of file