Merge branch '0.7' of github.com:deployd/deployd into 0.7

This commit is contained in:
DallonF
2012-12-14 08:55:31 -07:00
15 changed files with 320 additions and 159 deletions

View File

@@ -0,0 +1,20 @@
/**
* Add any helpers you need in other events.
* This is useful to expose node modules to events
* or to reuse code between events.
*/
// var request = require('request');
//
// helpers.request = function (url, options, fn) {
// request(url, options, function (err, res, body) {
// if(err) throw err;
//
// if(res.statusCode >= 400) {
// fn(null, body);
// } else {
// fn(body);
// }
// });
// }

View File

@@ -0,0 +1,17 @@
/**
* Use the request event to define global business logic.
* This will execute whenever a resource is requested.
*/
/**
* Use prevent() and allow() to change your default permissions.
* The following disables any unauthorized access to your app.
*/
// only allow logged in users any permissions
// if(!me) {
// prevent('*');
// }
// with the logging in being the only exception
// allow('login');

View File

@@ -32,8 +32,21 @@ function Context(resource, req, res, server) {
this.query = req.query || {};
this.server = server;
this.session = req.session;
this.resource = resource;
this.method = req && req.method;
if(resource && resource.getDefaultPermissions) {
this.permissions = resource.getDefaultPermissions(this);
} else {
this.permissions = {};
}
if(resource && resource.getRequiredPermissions) {
this.requiredPermissions = resource.getRequiredPermissions(this);
} else {
this.requiredPermissions = {};
}
// always bind done to this
var done = this.done;
this.done = function() {
@@ -111,4 +124,75 @@ Context.prototype.done = function(err, res) {
}
};
/**
* Permissions
*/
Context.prototype.allow = function (permission) {
var ctx = this;
if(permission === '*' || permission === 'all') {
Object.keys(this.requiredPermissions).forEach(function (key) {
ctx.permissions[key] = true;
});
} else {
ctx.permissions[permission] = true;
}
}
Context.prototype.prevent = function (permission) {
var ctx = this;
if(permission === '*' || permission === 'all') {
ctx.permissions = {};
} else {
ctx.permissions[permission] = false;
}
}
Context.prototype.isAllowed = function (permission) {
return !!this.permissions[permission];
}
Context.prototype.allowByDefault = function (permission) {
this.permissions[permission] = true;
}
Context.prototype.requirePermission = function (permission) {
this.requiredPermissions[permission] = true;
}
Context.prototype.verifyPermissions = function (fn) {
if(this.req.internal || this.req.isRoot) return fn();
var ctx = this;
var requiredKeys = Object.keys(this.requiredPermissions);
var requested = this.permissions;
var failed;
if(requiredKeys.length) {
requiredKeys.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 an event script');
} else {
error('permission denied when ' + permission);
}
}
});
if(!failed) {
fn();
}
} else {
fn();
}
function error(msg) {
failed = true;
ctx.res.statusCode = 401;
ctx.done(new Error(msg));
}
}
module.exports = Context;

View File

@@ -61,13 +61,27 @@ Files.prototype.load = function(fn) {
Files.prototype.handle = function (ctx, next) {
if(ctx.req && ctx.req.method !== 'GET') return next();
send(ctx.req, url.parse(ctx.url).pathname)
.root(path.resolve(this['public']))
.on('error', function (err) {
ctx.res.statusCode = 404;
respond('Resource Not Found', ctx.req, ctx.res);
})
.pipe(ctx.res);
ctx.verifyPermissions(function () {
send(ctx.req, url.parse(ctx.url).pathname)
.root(path.resolve(this['public']))
.on('error', function (err) {
ctx.res.statusCode = 404;
respond('Resource Not Found', ctx.req, ctx.res);
})
.pipe(ctx.res);
}.bind(this));
};
Files.prototype.getDefaultPermissions = function (ctx) {
return {'reading a file': true};
}
Files.prototype.getRequiredPermissions = function (ctx) {
// a resource should return an object defining
// required permissions for the given context
return {'reading a file': true};
}
module.exports = Files;

View File

@@ -23,7 +23,8 @@ module.exports = Resource.extend("InternalModules", {
exclude: {
Collection: 1,
UserCollection: 1,
internal: 1
internal: 1,
'global-events': 1
},
getModuleConfig: function(moduleId, fn) {

View File

@@ -37,49 +37,13 @@ function Collection(name, options) {
'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.prototype.external = {};
Collection.prototype.clientGeneration = true;
Collection.events = ['Get', 'Validate', 'Post', 'Put', 'Delete', 'Query'];
Collection.events = ['Get', 'Validate', 'Post', 'Put', 'Delete', 'Query', 'Request'];
Collection.prototype.eventNames = ['Get', 'Validate', 'Post', 'Put', 'Delete'];
Collection.prototype.dashboard = {
@@ -205,6 +169,64 @@ Collection.prototype.sanitizeQuery = function (query) {
return sanitized;
};
Collection.prototype.getRequiredPermissions = function (ctx) {
var requiredPermissions = {}
, hasId = !!(ctx.query.id || this.parseId(ctx) || (ctx.body && ctx.body.id));
if(hasId) {
requiredPermissions['querying an object by id'] = true;
}
switch(ctx.method) {
case 'GET':
if(hasId) {
requiredPermissions['querying an object by id'] = true;
} else {
requiredPermissions['querying multiple objects'] = true;
}
break;
case 'POST':
// TODO ~ account for custom methods
if(Array.isArray(ctx.body)) {
requiredPermissions['creating multiple objects'] = true;
} else if(hasId) {
requiredPermissions['updating an object by id'] = true;
} else {
requiredPermissions['creating an object'] = true;
}
break;
case 'PUT':
if(hasId) {
requiredPermissions['updating an object by id'] = true;
requiredPermissions['querying an object by id'] = true;
} else {
requiredPermissions['querying multiple objects'] = true;
requiredPermissions['updating multiple objects'] = true;
}
break;
case 'DELETE':
if(hasId) {
requiredPermissions['deleting an object by id'] = true;
} else {
requiredPermissions['deleting multiple objects'] = true;
}
break;
}
return requiredPermissions;
}
Collection.prototype.getDefaultPermissions = function (ctx) {
return {
'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
};
}
/**
* Handle an incoming http `req` and `res` and execute
* the correct `Store` proxy function based on `req.method`.
@@ -283,10 +305,10 @@ Collection.prototype.beforeQuery = function (ctx, fn) {
queryScript.run(ctx, domain, function (err) {
if(err) return ctx.done(err);
collection.verifyPermissions(ctx, fn);
ctx.verifyPermissions(fn);
});
} else {
collection.verifyPermissions(ctx, fn);
ctx.verifyPermissions(fn);
}
}
@@ -455,7 +477,7 @@ Collection.prototype.remove = function (ctx, fn) {
function done(err) {
if(err) return fn(err);
collection.verifyPermissions(ctx, function (err) {
ctx.verifyPermissions(function (err) {
if(err) return fn(err);
store.remove(sanitizedQuery, fn);
@@ -593,7 +615,7 @@ Collection.prototype.save = function (ctx, fn) {
return done(err || errors);
}
collection.verifyPermissions(ctx, function (err) {
ctx.verifyPermissions(function (err) {
if(err) {
return done(err);
}
@@ -637,14 +659,14 @@ Collection.prototype.save = function (ctx, fn) {
}
if(err || domain.hasErrors()) return done(err || errors);
debug('inserting item', item);
collection.verifyPermissions(ctx, function (err) {
ctx.verifyPermissions(function (err) {
if(err) return done(err);
store.insert(item, done);
if(session && session.emitToAll) session.emitToAll(collection.name + ':changed');
});
});
} else {
collection.verifyPermissions(ctx, function (err) {
ctx.verifyPermissions(function (err) {
if(err) return done(err);
store.insert(item, done);
if(session && session.emitToAll) session.emitToAll(collection.name + ':changed');
@@ -746,7 +768,7 @@ Collection.prototype.saveAll = function (ctx, fn) {
function add(err) {
if(err) return done(err);
collection.verifyPermissions(ctx, function (err) {
ctx.verifyPermissions(function (err) {
if(err) return done(err);
updateBatch.push(updated);
@@ -780,24 +802,8 @@ Collection.prototype.saveAll = function (ctx, fn) {
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];
}
},
allow: ctx.allow.bind(ctx),
prevent: ctx.prevent.bind(ctx),
error: function(key, val) {
debug('error %s %s', key, val);
errors[key] = val || true;

View File

@@ -0,0 +1,13 @@
var Module = require('../module')
, path = require('path')
, Script = require('../script');
module.exports = Module.extend({
load: function(fn) {
var events = this.server.events = this.events = {};
Script.loaddir(path.join(this.server.options.dir, 'events'), events, fn);
}
});

View File

@@ -158,68 +158,6 @@ 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 || ctx.req.method];
var requested = ctx.permissions || this.defaultPermissions;
var isSingle = !!ctx.query.id || ctx.method === 'POST';
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 an 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.
@@ -264,8 +202,20 @@ Resource.prototype.handle = function (ctx, next) {
}
};
Resource.prototype.getDefaultPermissions = function (ctx) {
// a resource should return an object defining
// default permissions for the given context
return {};
}
Resource.prototype.getRequiredPermissions = function (ctx) {
// a resource should return an object defining
// required permissions for the given context
return {};
}
Resource.prototype.beforeHandle = function (ctx) {
this.setDefaultPermissions(ctx);
// hook
}
Resource.prototype.afterHandle = function (ctx) {

View File

@@ -73,16 +73,23 @@ Router.prototype.route = function (req, res) {
// default root to false
if(ctx.session) ctx.session.isRoot = req.isRoot || false;
// external functions
var furl = ctx.url.replace('/', '');
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);
}
router.runRequestEvent(ctx, function (err) {
if(err) {
ctx.done(err);
return;
}
// external functions
var furl = ctx.url.replace('/', '');
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);
res.statusCode = 404;
@@ -94,6 +101,34 @@ Router.prototype.route = function (req, res) {
};
/**
* Run the global request event using the given context.
*/
Router.prototype.runRequestEvent = function (ctx, fn) {
var domain = {
prevent: ctx.prevent.bind(ctx),
allow: ctx.allow.bind(ctx),
url: ctx.req.url
};
if(this.server.events && this.server.events.request) {
this.server.events.request.run(ctx, domain, resource);
} else {
resource();
}
function resource(err) {
if(err) return fn(err);
if(ctx.resource && ctx.resource.events && ctx.resource.events.request) {
ctx.resource.events.request.run(ctx, domain, fn);
} else {
fn();
}
}
}
/**
* Get resources whose base path matches the incoming URL, and order by specificness.

View File

@@ -140,7 +140,7 @@ Script.loaddir = function (configPath, events, fn) {
fs.readdir(configPath, function (err, files) {
if(err) {
console.error(err);
// console.error(err);
return fn();
}

View File

@@ -57,7 +57,8 @@ function Server(options) {
// defaults
this.options = options = extend({
port: 2403,
db: {port: 27017, host: '127.0.0.1', name: 'deployd'}
db: {port: 27017, host: '127.0.0.1', name: 'deployd'},
dir: process.cwd()
}, options);
debug('started with options %j', options);

View File

@@ -3,6 +3,8 @@ var Server = require('./server')
, Monitor = require('./monitor')
, commands = {};
require('longjohn');
/**
* Commands exposed to parent process.
*/

View File

View File

@@ -1,5 +1,23 @@
var Collection = require('../lib/modules/collection')
, db = require('../lib/db');
, db = require('../lib/db')
, Context = require('../lib/context');
function createMockContext(options) {
options.query = options.query || {};
options.path = options.path || '/';
options.req.url = options.req.url || '/';
var ctx = new Context({path: options.path}, options.req, options.res, {});
ctx.session = options.session || {};
ctx.done = function () {
options.res.end();
}
Object.keys(options).forEach(function (key) {
ctx[key] = options[key];
});
return ctx;
}
describe('collection', function(){
function createCollection(properties) {
@@ -97,7 +115,7 @@ describe('collection', function(){
// faux body
req.body = body;
req.query = query;
c.handle({req: req, res: res, query: query || {}, session: {}, done: function() {res.end();}});
c.handle(createMockContext({path: path, req: req, res: res, query: query}));
}, function (req, res) {
test(req, res, method, path, properties, body, query);
// cleanup
@@ -171,7 +189,7 @@ describe('collection', function(){
it('should save the provided data', function(done) {
var c = new Collection('counts', {db: db.create(TEST_DB), config: { properties: {count: {type: 'number'}}}});
c.save({session: {}, body: {count: 1}, query: {}, dpd: {}, req: {}, res: {}, done: done, method: 'POST'}, function (err, item) {
c.save(createMockContext({session: {}, body: {count: 1}, query: {}, dpd: {}, req: {}, res: {}, done: done, method: 'POST'}), function (err, item) {
expect(item.id).to.exist;
expect(err).to.not.exist;
done();
@@ -181,10 +199,10 @@ describe('collection', function(){
it('should pass commands like $inc', function(done) {
var c = new Collection('counts', {db: db.create(TEST_DB), config: { properties: {count: {type: 'number'}}}});
c.save({body: {count: 1}, req: {}, res: {}, method: 'POST', query: {}}, function (err, item) {
c.save(createMockContext({body: {count: 1}, req: {}, res: {}, method: 'POST', query: {}}), function (err, item) {
expect(item.id).to.exist;
expect(err).to.not.exist;
c.save({body: {count: {$inc: 100}}, query: {id: item.id}, req: {}, res: {}, done: done, method: 'PUT'}, function (err, updated) {
c.save(createMockContext({body: {count: {$inc: 100}}, query: {id: item.id}, req: {}, res: {}, done: done, method: 'PUT'}), function (err, updated) {
expect(err).to.not.exist;
expect(updated).to.exist;
expect(updated.count).to.equal(101);
@@ -209,7 +227,7 @@ describe('collection', function(){
it('should return the provided data', function(done) {
var c = new Collection('foo', {db: db.create(TEST_DB), config: { properties: {count: {type: 'number'}}}});
c.save({body: {count: 1}, query: {}, req: {}, res: {}, done: done, method: 'POST'}, function (err, item) {
c.save(createMockContext({body: {count: 1}, query: {}, req: {}, res: {}, done: done, method: 'POST'}), function (err, item) {
c.find({query: {}, req: {}, res: {}, done: done, method: 'GET'}, function (err, items) {
expect(items.length).to.equal(1);
done(err);
@@ -220,10 +238,10 @@ describe('collection', function(){
it('should return the provided data in sorted order', function(done) {
var c = new Collection('sort', { db: db.create(TEST_DB), config: { properties: {count: {type: 'number'}}}});
c.save({body: {count: 1}, query: {}, req: {}, res: {}, done: done, method: 'POST'}, function (err, item) {
c.save({body: {count: 3}, query: {}, req: {}, res: {}, done: done, method: 'POST'}, function (err, item) {
c.save({body: {count: 2}, query: {}, req: {}, res: {}, done: done, method: 'POST'}, function (err, item) {
c.find({query: {$sort: {count: 1}}, req: {}, res: {}, done: done, method: 'GET'}, function (err, items) {
c.save(createMockContext({body: {count: 1}, query: {}, req: {}, res: {}, done: done, method: 'POST'}), function (err, item) {
c.save(createMockContext({body: {count: 3}, query: {}, req: {}, res: {}, done: done, method: 'POST'}), function (err, item) {
c.save(createMockContext({body: {count: 2}, query: {}, req: {}, res: {}, done: done, method: 'POST'}), function (err, item) {
c.find(createMockContext({query: {$sort: {count: 1}}, req: {}, res: {}, done: done, method: 'GET'}), function (err, items) {
expect(items.length).to.equal(3);
for(var i = 0; i < 3; i++) {
delete items[i].id;

View File

@@ -37,8 +37,8 @@ describe('config-loader', function() {
if (err) return done(err);
var resources = result.resources;
expect(resources).to.have.length(7);
expect(resources.filter(function(r) { return r.name == 'foo';})).to.have.length(1);
expect(resources.filter(function(r) { return r.name == 'bar';})).to.have.length(1);
expect(resources.filter(function(r) { return r.name == 'foo'; })).to.have.length(1);
expect(resources.filter(function(r) { return r.name == 'bar'; })).to.have.length(1);
done();
});
});