Permalink
Browse files

The basics

  • Loading branch information...
tschaub committed Apr 23, 2013
1 parent 6bc6ba6 commit b4014f35217a8dc48375c311b9c5884dc2b1247f
Showing with 693 additions and 6 deletions.
  1. +1 −0 .gitignore
  2. +99 −1 README.md
  3. +27 −0 lib/authorized.js
  4. +233 −0 lib/manager.js
  5. +38 −0 lib/role.js
  6. +17 −0 lib/view.js
  7. +20 −5 package.json
  8. +119 −0 test/lib/manager.spec.js
  9. +100 −0 test/lib/role.spec.js
  10. +39 −0 test/lib/view.spec.js
View
@@ -0,0 +1 @@
+/node_modules/
View
100 README.md
@@ -1,3 +1,101 @@
# Authorized!
-Action based authorization middleware.
+** Action based authorization middleware **
+
+The authorized package is available on npm.
+
+ $ npm install authorized
+
+## Quick start
+
+Import an authorization manager.
+
+```js
+var auth = require('authorized');
+```
+
+Provide getters for your application roles.
+
+```js
+auth.role('admin', function(req, done) {
+ done(null, req.user && req.user.admin);
+});
+```
+
+Roles can use `<entity>.<relation>` syntax.
+
+```js
+// getters for entity.relation type roles are called with the entity
+auth.role('organization.owner', function(org, req, done) {
+ if (!req.user) {
+ done();
+ } else {
+ done(null, !!~org.owners.indexOf(req.user.id));
+ }
+});
+```
+
+Provide getters for your application entities.
+
+```js
+auth.entity('organization', function(req, done) {
+ // assume url like /organizations/:orgId
+ var match = req.url.match(/^\/organizations\/(\w+)/);
+ if (!match) {
+ done(new Error('Expected url like /organizations/:orgId'));
+ }
+ // pretend we're going to the db for the organization
+ process.nextTick(function() {
+ // mock org
+ var org = {id: match[1], owners: ['user.1']};
+ done(null, org);
+ });
+});
+```
+
+Now define what roles are required for your actions.
+
+```js
+auth.action('add members to organization', ['admin', 'organization.owner']);
+```
+
+Now you're ready to generate authorization middleware.
+
+```js
+var middleware = auth.can('add members to organization');
+```
+
+This middleware can be used in Connect/Express apps in your route definitions.
+
+```js
+var assert = require('assert');
+var express = require('express');
+var app = express();
+app.post(
+ '/organizations/:orgId/members',
+ auth.can('add members to organization'),
+ function(req, res, next) {
+ // you can safely let the user add members to the org here
+ // you can also access entities, roles, and actions for your view
+ var view = auth.view(req);
+ assert.ok(view.entities.organization);
+ assert.strictEqual(view.roles['admin'], false);
+ assert.strictEqual(view.roles['organization.owner'], true);
+ assert.strictEqual(view.actions['add members to organization'], true);
+ });
+```
+
+## What else?
+
+This package is strictly about authorization. For a full-featured
+authentication package, see [PassportJS](http://passportjs.org/).
+
+Inspiration is drawn here from [connect-roles](https://github.com/ForbesLindesay/connect-roles).
+One major difference is that this is all async (you don't have to determine
+if a user can perform an action synchronously).
+
+## Check out the tests for more
+
+Tests are run with mocha.
+
+ npm test
View
@@ -0,0 +1,27 @@
+var Manager = require('./manager').Manager;
+var Role = require('./role').Role;
+var View = require('./view').View;
+
+
+/**
+ * Singleton with default options.
+ */
+exports = module.exports = new Manager();
+
+
+/**
+ * @type {Manager}
+ */
+exports.Manager = Manager;
+
+
+/**
+ * @type {Role}
+ */
+exports.Role = Role;
+
+
+/**
+ * @type {View}
+ */
+exports.View = View;
View
@@ -0,0 +1,233 @@
+var async = require('async');
+
+var Role = require('./role').Role;
+var View = require('./view').View;
+
+
+/**
+ * Used to store authorized info on a request object.
+ * @const
+ */
+var LOOKUP_ID = '__authorized';
+
+
+
+/**
+ * Create a new authorization manager.
+ * @constructor
+ */
+function Manager() {
+ this.roleGetters_ = {};
+ this.entityGetters_ = {};
+ this.actionDefs_ = {};
+}
+
+
+/**
+ * Register the roles for a specific action.
+ * @param {string} name Action name (e.g. 'add member to organization').
+ * @param {Array.<string>} roles Roles allowed to perform this action. If
+ * the current user has any one of the supplied roles, they can perform the
+ * action (e.g. ['admin', 'organization.owner']).
+ */
+Manager.prototype.action = function(name, roles) {
+ if (!Array.isArray(roles)) {
+ roles = [roles];
+ }
+ roles = roles.map(function(role) {
+ if (typeof role == 'string') {
+ role = new Role(role);
+ }
+ return role;
+ });
+ this.actionDefs_[name] = roles;
+};
+
+
+/**
+ * Check if an action is allowed.
+ * @param {string} action Action name.
+ * @param {Object} req Request object.
+ * @param {function(Error, boolean)} done Callback.
+ * @private
+ */
+Manager.prototype.actionAllowed_ = function(action, req, done) {
+ var self = this;
+ function cacheBeforeDone(role, done) {
+ self.hasRole_(role, req, done);
+ }
+
+ var view = this.view(req);
+ if (action in view.actions) {
+ process.nextTick(function() {
+ done(null, view.actions[action]);
+ });
+ } else {
+ var roles = this.actionDefs_[action];
+ async.each(roles, cacheBeforeDone, function(err) {
+ if (err) {
+ return done(err);
+ }
+ // user must have one of the roles to perform the action
+ var can = roles.some(function(role) {
+ return !!view.roles[role.name];
+ });
+ view.actions[action] = can;
+ done(null, can);
+ });
+ }
+};
+
+
+/**
+ * Create action based authorization middleware.
+ * @param {string} action Action name (e.g. 'add members to organization').
+ * @return {function(Object, Object, function)} Authorization middleware.
+ */
+Manager.prototype.can = function(action) {
+ if (!this.actionDefs_.hasOwnProperty(action)) {
+ throw new Error('ConfigError: Action not found: ' + action);
+ }
+ var self = this;
+
+ return function(req, res, next) {
+ self.actionAllowed_(action, req, function(err, can) {
+ if (err) {
+ return next(err);
+ }
+ if (can) {
+ next();
+ } else {
+ next(new Error('UnauthorizedError: Action not allowed: ' + action));
+ }
+ });
+ }
+};
+
+
+/**
+ * Register a getter for an entity.
+ * @param {string} type Entity type (e.g. 'organization').
+ * @param {function(req, done)} getter Function called to get an entity from
+ * the provided request. The `done` function has the form
+ * {function(Error, Object)} where `err` is any error value
+ * generated while getting the entity and `entity` is the target entity.
+ */
+Manager.prototype.entity = function(type, getter) {
+ this.entityGetters_[type] = getter;
+};
+
+
+/**
+ * Get an entity from a request.
+ * @param {string} type Entity type.
+ * @param {Object} req Request object.
+ * @param {function(Error, Object)} done Callback.
+ * @private
+ */
+Manager.prototype.getEntity_ = function(type, req, done) {
+ if (!type) {
+ process.nextTick(function() {
+ done();
+ });
+ } else {
+ if (!(type in this.entityGetters_)) {
+ done(new Error('ConfigError: No getter found for entity: ' + type));
+ } else {
+ var view = this.view(req);
+ var entity = view.entities[type];
+ if (entity) {
+ process.nextTick(function() {
+ done(null, entity);
+ });
+ } else {
+ this.entityGetters_[type](req, function(err, entity) {
+ if (!err && entity) {
+ // cache entity for future access
+ view.entities[type] = entity;
+ }
+ done(err, entity);
+ });
+ }
+ }
+ }
+};
+
+
+/**
+ * Check if a user has the given role.
+ * @param {Role} role Target role.
+ * @param {Object} req Current request.
+ * @param {function(Error, boolean)} done Callback.
+ * @private
+ */
+Manager.prototype.hasRole_ = function(role, req, done) {
+ var getter = this.roleGetters_[role.name];
+ if (!getter) {
+ done(new Error(
+ 'ConfigError: No getter found for role: ' + role.name));
+ } else {
+ var view = this.view(req);
+ if (role.name in view.roles) {
+ process.nextTick(function() {
+ done(null, view.roles[role.name]);
+ });
+ } else {
+ this.getEntity_(role.entity, req, function(err, entity) {
+ if (err) {
+ return done(err);
+ }
+ function cacheBeforeDone(err, has) {
+ if (!err) {
+ view.roles[role.name] = !!has;
+ }
+ done(err, has);
+ }
+ var args = entity ?
+ [entity, req, cacheBeforeDone] : [req, cacheBeforeDone];
+ getter.apply(null, args);
+ });
+ }
+ }
+};
+
+
+/**
+ * Register a getter for a role.
+ * @param {string} role Role name (e.g. 'organization.owner').
+ * @param {function(req, done)} getter Function that determines if the current
+ * user has the given role. This function will be called with the request
+ * object and a callback. The callback has the form
+ * {function(Error, boolean)} where `err` is any error value
+ * generated while checking for the given role and `has` is a boolean
+ * indicating whether the user has the role.
+ */
+Manager.prototype.role = function(role, getter) {
+ if (typeof role == 'string') {
+ role = new Role(role);
+ }
+ this.roleGetters_[role.name] = getter;
+};
+
+
+/**
+ * Get cached authorization info for a request.
+ * @param {Object} req Request object.
+ * @return {View} A cache of authorization info.
+ */
+Manager.prototype.view = function(req) {
+ var storage = req[LOOKUP_ID];
+ if (!storage) {
+ storage = req[LOOKUP_ID] = {};
+ }
+ if (!('view' in storage)) {
+ storage.view = new View();
+ }
+ return storage.view;
+};
+
+
+/**
+ * @type {Manager}
+ */
+exports.Manager = Manager;
Oops, something went wrong.

0 comments on commit b4014f3

Please sign in to comment.