Skip to content

Commit

Permalink
Half-way through refactoring to make errors its own module - needs fu…
Browse files Browse the repository at this point in the history
…rther work
  • Loading branch information
MrTrick committed Jun 7, 2013
1 parent 3c642c6 commit 20d14da
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 37 deletions.
7 changes: 4 additions & 3 deletions app/activity.js
Expand Up @@ -30,6 +30,7 @@
* Parts of the application concerned with handling activity requests
*/
var Activity = require("../lib/activity.js");
var Errors = require("../lib/errors.js");

/**
* Used with app.param to pre-load any referenced activity into the request
Expand All @@ -42,7 +43,7 @@ exports.loadActivity = function(req, res, next, id) {
success: function(activity) {
//If the user is not allowed to read it, error
if (!activity.allowed('read', req.context)) {
next(new Activity.Error("Reading this activity not permitted", 403));
next(new Errors[req.context&&req.context.user ? 'Forbidden' : 'Unauthorized']("Reading this activity not permitted") );
} else {
req.activity = activity;
next();
Expand Down Expand Up @@ -114,10 +115,10 @@ exports.fire = function(req, res, next) {
console.log("[Route] Firing action '"+action_id+"' on activity '"+activity.id+"'");

var action = activity.action(req.param('action'));
if (!action) return next(new Activity.Error("No such action '"+action_id+"'", 404));
if (!action) return next(new Errors.NotFound("No such action '"+action_id+"'"));

//Check authorisation
if (!action.allowed(req.context)) return next(new Activity.Error("Action '"+action_id+"' forbidden", 403));
if (!action.allowed(req.context)) return next(new Errors.Forbidden("Action '"+action_id+"' forbidden"));

//Fire the given action - if successful output the updated activity.
console.log("Firing: Logging", req.body);
Expand Down
3 changes: 2 additions & 1 deletion app/auth.js
Expand Up @@ -31,6 +31,7 @@
*/

var Activity = require("../lib/activity.js");
var Errors = require("../lib/errors.js");

/**
* Check the user's authentication against the adapter,
Expand Down Expand Up @@ -79,5 +80,5 @@ exports.self = function(req, res, next) {
if (req.user)
res.send(req.user.toJSON());
else
next(new Activity.Error("No identity given", 401));
next(new Errors.Unauthorized());
};
5 changes: 3 additions & 2 deletions app/design.js
Expand Up @@ -30,6 +30,7 @@
* Parts of the application concerned with handling design requests
*/
var Activity = require("../lib/activity.js");
var Errors = require("../lib/errors.js");
var exec = require('child_process').exec;

/**
Expand Down Expand Up @@ -101,10 +102,10 @@ exports.create = function(req, res, next) {

var design = req.design;
var action = design.action('create');
if (!action) return next(new Activity.Error("Design error - no create action", 500, design.id));
if (!action) return next(new Errors.ServerError("Design error - no create action: ", design.id));

//Check authorisation
if (!action.allowed(req.context)) return next(new Activity.Error("Create forbidden", 403, design.id));
if (!action.allowed(req.context)) return next(new Errors.Forbidden("Create forbidden", design.id));

//Fire the create action - if successful output the newly-created activity
action.fire(req.body, req.context, {
Expand Down
2 changes: 1 addition & 1 deletion config.example/designs/computer_request_v1.js
Expand Up @@ -84,7 +84,7 @@ module.exports = {
fire: function() {
//Set the nominated approver
//TODO: Check existence using EBAT Person instead of User
if (!inputs.approver) error(new Activity.Error("Invalid approver"));
if (!inputs.approver) error(new Errors.ServerError("Invalid approver"));
var approver = new User.Model({id:inputs.approver});
user.fetch({
success: function() {
Expand Down
6 changes: 3 additions & 3 deletions config.example/settings.js
Expand Up @@ -33,10 +33,10 @@ exports.sync.user = function(method, model, options) {
if (method == 'read' && model instanceof Backbone.Model) {
var user = _.find(users, function(u) { return u.id == model.id; });
if (user) options.success(user);
else options.error(new Activity.Error("No user found with id " + model.id, 404));
else options.error(new Errors.NotFound("No user found with id " + model.id));
} else {
console.error("[Sync] Illegal method/model on user sync", method);
options.error(new Activity.Error("Illegal method/model on user sync", 403));
options.error(new Errors.Forbidden("Illegal method/model on user sync"));
}
};

Expand All @@ -58,7 +58,7 @@ exports.auth = {
if (user && credentials.password == user.id) {
options.success(user.id);
} else {
options.error(new Activity.Error("Incorrect credentials", 403));
options.error(new Errors.Unauthorized("Incorrect credentials"));
}
}

Expand Down
22 changes: 6 additions & 16 deletions lib/activity.js
Expand Up @@ -34,6 +34,7 @@
if (typeof Backbone == "undefined") Backbone = require("backbone");
if (typeof JSV == "undefined") JSV = require("JSV").JSV;
if (typeof Allowed == "undefined") Allowed = require("./allowed.js");
if (typeof Errors == "undefined") Errors = require("./errors.js");
}

/**
Expand Down Expand Up @@ -86,20 +87,6 @@
return baseUrl;
};

//------------------------------------------------------------------------------
// Errors
//------------------------------------------------------------------------------

Activity.Error = function(message, status_code, inner) {
this.name = "Activity.Error";
this.message = message || "An error occurred";
this.status_code = status_code || 500;
this.inner = inner || null;
this.stack = (new Error()).stack;
};
Activity.Error.prototype = new Error();
Activity.Error.prototype.constructor = Activity.Error;

//------------------------------------------------------------------------------
// Designs
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -534,7 +521,7 @@
if (options.timeout > 0) {
var timeout_id = setTimeout(function() {
timed_out = true;
action.trigger('fire:error', new Activity.Error("Fire handler timed out"));
action.trigger('fire:error', new Errors.ServerError("Fire handler timed out"));
}, options.timeout);
this.once('fire:complete', function() { clearTimeout(timeout_id); });
};
Expand Down Expand Up @@ -606,7 +593,10 @@
action.trigger("fire:complete");
};
context.error = function(error, status_code, inner) { //'Smart' error handler - Parses errors into a 'real' ones, if they're not.
if (!_.isObject(error) || !error.message) error = new Activity.Error( _.isString(error) ? error : "Firing error", status_code || 500, inner || error);
if (!_.isObject(error) || !error.message) {
var E = Errors[status_code] || Errors.ServerError;
error = new E( _.isString(error) ? error : "Firing error", inner || error);
}
action.trigger("fire:error", error);
};

Expand Down
8 changes: 4 additions & 4 deletions lib/allowed.js
Expand Up @@ -26,7 +26,7 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

var Activity = require("./activity.js");
var Errors = require("./errors.js");

/**
* Enforce access control according to the given user and rules.
Expand Down Expand Up @@ -77,18 +77,18 @@ var Activity = require("./activity.js");

//Require the user to have authenticated
if (!req.user)
return next(new Activity.Error("Authenticated user required", 401));
return next(new Errors.Unauthorized());

//Enforce all-route rules
if (rules._all && !rules._all.call(req.context))
return next(new Activity.Error("Access forbidden", 403));
return next(new Errors.Forbidden());

//Find the relevant rule
var rule = rules[ req.route.path ];
if (typeof rule == "undefined")
console.warn("Route " + req.route.path + " was invoked with acl middleware, but no rule for that route was known."); //TODO: How else to log warnings?
else if (rule && !rule.call(req.context))
return next(new Activity.Error("Access forbidden", 403));
return next(new Errors.Forbidden());

return next();
};
Expand Down
99 changes: 99 additions & 0 deletions lib/errors.js
@@ -0,0 +1,99 @@
/*
* FireEngine - Javascript Activity Engine
*
* @license Simplified BSD
* Copyright (c) 2013, Patrick Barnes
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/**
* Simple error library to wrap and include all HTTP errors.
*
* Helps to provide semantic clues when errors occur.
*
* @author Patrick Barnes
*/

(function() {
//Housekeeping - ensures the file is usable on node.js and on browser
var Errors = this.Errors = this.window ? {} : exports;

var status_codes = {
400: 'Bad request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Resource Conflict',
410: 'Resource Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Request Entity Too Large',
414: 'Request-URI Too Long',
415: 'Unsupported Media Type',
416: 'Requested Range Not Satisfiable',
417: 'Expectation Failed',
418: 'I\'m A Teapot',
420: 'Enhance Your Calm',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
500: 'Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates',
511: 'Network Authentication Required'
};

//Build an error type for each code
for (var status in status_codes) (function(status){
var status_msg = status_codes[status];
var name = status_msg.replace(/\W/g,'');

var CodeError = function(message, inner) {
this.constructor.prototype.__proto__ = Error.prototype;
this.message = message || status_msg;
if (inner) this.inner = inner;

//Grab info from the closure
this.status = status;
this.name = name;
this.status_msg = status_msg;

//Store the stacktrace - how did we get here?
if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
else this.stack = (new Error()).stack;
};

//Store the error for that code
Errors[status] = CodeError;
Errors[name] = CodeError;
})(status);
}).call(this);
10 changes: 6 additions & 4 deletions lib/session.js
Expand Up @@ -27,9 +27,11 @@
*/

var Activity = require("./activity.js"); //Needed for the error class
var Errors = require("./errors.js");
var querystring = require('querystring');
var crypto = require('crypto');


/**
* Provides a mechanism for retrieving identity from an HMAC-signed request,
* and verifying the validity and currency of the request signature.
Expand Down Expand Up @@ -57,16 +59,16 @@ module.exports = function(settings) {

//Parse the header
if (! (m=/^HMAC (.*)$/.exec(header)) )
throw new Activity.Error("Invalid Signature; Incorrect header format", 401);
throw new Errors.Unauthorized("Invalid Signature; Incorrect header format");
var params = querystring.parse(m[1]);
if (!params.identity || !params.expiry || !params.signature)
throw new Activity.Error("Missing one or more paramater - expect 'identity', 'expiry', 'signature'", 401);
throw new Errors.Unauthorized("Missing one or more paramater - expect 'identity', 'expiry', 'signature'");

//Check expiry
// (The signature is only valid from time of issue to expiry)
var now = Math.round(Date.now()/1000);
if (now > params.expiry || now < params.expiry - settings.lifetime)
throw new Activity.Error("Credentials are expired", 403);
throw new Errors.Unauthorized("Credentials are expired");

//Re-derive the client key from the identity/expiry and the server key
var client_key = Session.generateKey(params.identity, params.expiry);
Expand All @@ -76,7 +78,7 @@ module.exports = function(settings) {
var computed_signature = crypto.createHmac('sha256', client_key).update(request.method+url).digest('hex');
if (computed_signature !== params.signature) {
console.error("Forged signature: ", {identity: params.identity, expiry: params.expiry, url: url, signature: params.signature, computed_signature: computed_signature});
throw new Activity.Error("Credentials are invalid", 403);
throw new Errors.Unauthorized("Credentials are invalid");
}

//Success! Found the client identity
Expand Down
5 changes: 3 additions & 2 deletions lib/sync_design.js
Expand Up @@ -4,6 +4,7 @@

var Sanitize = require("./sanitizer.js");
var Activity = require("./activity.js");
var Errors = require("./errors.js");
var fs = require('fs');

/**
Expand Down Expand Up @@ -36,7 +37,7 @@ module.exports = function(path) {
if (model.isNew()) options.error(model, "Can't load new model", options);
console.log("[Sync] Reading model", model.id);
if (!designs[model.id])
options.error(new Activity.Error("No design found", 404));
options.error(new Errors.NotFound("No design found"));
else
options.success(designs[model.id]);
} else {
Expand All @@ -46,7 +47,7 @@ module.exports = function(path) {
}
} else {
console.log("[Sync] Illegal method on design sync", method);
options.error(model, new Activity.Error("Illegal method on design sync", 403), options);
options.error(model, new Errors.Forbidden("Illegal method on design sync", method), options);
}
};

Expand Down
5 changes: 4 additions & 1 deletion test/test_app.js
Expand Up @@ -33,6 +33,7 @@ var _ = require("underscore");
var Activity = require("../lib/activity.js");
var AppActivity = require("../app/activity.js");
var User = require("../lib/user.js");
var Errors = require("../lib/errors.js");

var activity_skeleton = {
design: {
Expand Down Expand Up @@ -78,7 +79,7 @@ exports.activity = {
process.nextTick(function() {
data = activity_data[parseInt(model.id)];
if (data) options.success(data);
else options.error(new Activity.Error("Not found", 404));
else options.error(new Errors.NotFound());
});
};
ready();
Expand Down Expand Up @@ -111,6 +112,7 @@ exports.activity = {
var req = {};
var next = function(error) {
t.ok(error, "Read not permitted");
t.ok(error instanceof Errors.Unauthorized, 'Expect an unauthorized response');
t.equal(error.message, "Reading this activity not permitted");
t.done();
};
Expand All @@ -121,6 +123,7 @@ exports.activity = {
var req = { context: { user: new User.Model({ id: 'fred', roles: ['noob'] }) } };
var next = function(error) {
t.ok(error, "Read not permitted");
t.ok(error instanceof Errors.Forbidden, 'Expect a forbidden response');
t.equal(error.message, "Reading this activity not permitted");
t.done();
};
Expand Down

0 comments on commit 20d14da

Please sign in to comment.