initial permissions api

This commit is contained in:
Ritchie Martori
2012-11-16 12:03:30 -08:00
parent 14c3c0a3ba
commit 4164a61ad0
16 changed files with 711 additions and 157 deletions

View File

@@ -7,5 +7,6 @@
### New Features
- resources now support custom events by default
- new collection permission api
### Major Bugfixes

View File

@@ -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);
});
};

View File

@@ -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`

View File

@@ -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;

View File

@@ -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;
}
}
};

View File

@@ -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);

View File

@@ -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);
});
});
});
});

View File

@@ -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);
});
//
});
});

View File

@@ -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
});
}

View File

@@ -0,0 +1,13 @@
{
"type": "Collection",
"properties": {
"title": {
"name": "title",
"type": "string",
"typeLabel": "string",
"required": false,
"id": "title",
"order": 0
}
}
}

View File

@@ -0,0 +1 @@
prevent('*');

View File

@@ -0,0 +1,3 @@
if(query.$PREVENT_ALL) {
prevent('*');
}

View File

@@ -0,0 +1,5 @@
if(this.title === '$ALLOW') {
allow('*');
} else {
prevent('*');
}

View File

@@ -6,4 +6,10 @@ if(this.$TEST_RESPOND) {
dpd.todos.get({$limit: 2}, function (results) {
respond('foo bar bat baz');
});
}
}
if(this.$BATCH_PUT) {
dpd.todos.put({}, {title: this.$BATCH_PUT}, function () {
// wait
});
}

View File

@@ -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');
}

View File

@@ -0,0 +1,7 @@
if(query.test === '$CUSTOM_PERMISSIONS_PUT') {
allow('updating multiple objects');
}
if(query.test === '$CUSTOM_PERMISSIONS_DELETE') {
allow('deleting multiple objects');
}