mirror of
https://github.com/zhigang1992/deployd.git
synced 2026-05-13 12:37:17 +08:00
initial permissions api
This commit is contained in:
@@ -7,5 +7,6 @@
|
||||
### New Features
|
||||
|
||||
- resources now support custom events by default
|
||||
- new collection permission api
|
||||
|
||||
### Major Bugfixes
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
//
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
15
test-app/resources/empty-users/custom.js
Normal file
15
test-app/resources/empty-users/custom.js
Normal 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
|
||||
});
|
||||
}
|
||||
13
test-app/resources/perms/config.json
Normal file
13
test-app/resources/perms/config.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "Collection",
|
||||
"properties": {
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"typeLabel": "string",
|
||||
"required": false,
|
||||
"id": "title",
|
||||
"order": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
1
test-app/resources/perms/delete.js
Normal file
1
test-app/resources/perms/delete.js
Normal file
@@ -0,0 +1 @@
|
||||
prevent('*');
|
||||
3
test-app/resources/perms/query.js
Normal file
3
test-app/resources/perms/query.js
Normal file
@@ -0,0 +1,3 @@
|
||||
if(query.$PREVENT_ALL) {
|
||||
prevent('*');
|
||||
}
|
||||
5
test-app/resources/perms/validate.js
Normal file
5
test-app/resources/perms/validate.js
Normal file
@@ -0,0 +1,5 @@
|
||||
if(this.title === '$ALLOW') {
|
||||
allow('*');
|
||||
} else {
|
||||
prevent('*');
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
7
test-app/resources/todos/query.js
Normal file
7
test-app/resources/todos/query.js
Normal 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');
|
||||
}
|
||||
7
test-app/resources/users/query.js
Normal file
7
test-app/resources/users/query.js
Normal 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');
|
||||
}
|
||||
Reference in New Issue
Block a user