From 86725e91ceef0c713be0d989a0496ae03920c448 Mon Sep 17 00:00:00 2001 From: Christopher Garvis Date: Fri, 19 Oct 2012 17:43:53 +0000 Subject: [PATCH] Adds v1.0.0 of module --- .gitignore | 1 + LICENSE | 19 +++++++ README.md | 50 +++++++++++++++++++ lib/rampart.js | 110 +++++++++++++++++++++++++++++++++++++++++ package.json | 40 +++++++++++++++ test/middleware.coffee | 13 +++++ test/rampart.coffee | 98 ++++++++++++++++++++++++++++++++++++ test/server.coffee | 29 +++++++++++ 8 files changed, 360 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib/rampart.js create mode 100644 package.json create mode 100644 test/middleware.coffee create mode 100644 test/rampart.coffee create mode 100644 test/server.coffee diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..73a9474 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Moveline Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..67bb77f --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Rampart [![Build Status](https://secure.travis-ci.org/Moveline/rampart.png)](http://travis-ci.org/Moveline/rampart) + +Authorization module with Connect/Express support + +## Installation + +```bash +$ npm install rampart +``` + +## Usage + +```coffee-script +Auth = require './auth' +Rampart = require 'rampart' +express = require 'express' + +class Ability extends Rampart.Ability + constructor: (user) -> + user = user || new User + + if user.role is 'admin' + @can 'manage', User + + else + @can 'manage', User, {_id: user.id} + +app = express() +app.use Auth.session() +app.use Rampart.express(Ability) + +app.get '/', (req, res, next) -> + res.send 401 unless req.user.isAllowed 'read', User + +``` + +## Tests + +```bash +$ npm test +``` + +## Authors [Christopher Garvis][0] & [Moveline][1] + +[0]: http://christophergarvis.com +[1]: http://www.moveline.com + +## License + +MIT diff --git a/lib/rampart.js b/lib/rampart.js new file mode 100644 index 0000000..82164bd --- /dev/null +++ b/lib/rampart.js @@ -0,0 +1,110 @@ +_ = require('underscore'); + +var Ability = function Ability() { + this.rules = []; +}; + +Ability.prototype.can = function can(action, subject, conditions) { + this.rules.push(new Rule(true, action, subject, conditions)); +}; + +Ability.prototype.cannot = function cannot(action, subject, conditions) { + this.rules.push(new Rule(false, action, subject, conditions)); +}; + +/** + * @param {String} + * @param {Object} + * @returns boolean + */ +Ability.prototype.isAllowed = function isAllowed(action, subject) { + var rules = this.relevantRules(action, subject); + var match = _.detect(rules, function(rule) { + return rule.matchesConditions(action, subject); + }); + + return match ? match.base_behavior : false; +}; + +/** + * @see Ability::isAllowed + */ +Ability.prototype.isNotAllowed = function isNotAllowed() { + return !this.isAllowed.apply(this, arguments); +}; + +Ability.prototype.relevantRules = function relevantRules(action, subject) { + return this.rules.filter(function(rule) { + return rule.isRelevant(action, subject); + }, this); +}; + +var Rule = function(base_behavior, action, subject, conditions) { + var flatten = function flatten(items) { + if('array' === typeof items) { + return items.reduce(function(a,b) { return a.concact(b); }); + } + + return [items]; + }; + + this.base_behavior = base_behavior; + this.actions = flatten(action); + this.subjects = flatten(subject); + this.conditions = conditions; +}; + +/** + * @param {String} + * @param {Object} + * @returns boolean + */ +Rule.prototype.isRelevant = function isRelevant(action, subject) { + return this.matchesAction(action) && this.matchesSubject(subject); +}; + +/** + * @param {String} + * @returns boolean + */ +Rule.prototype.matchesAction = function matchesAction(action) { + return this.actions.indexOf('manage') !== -1 || + this.actions.indexOf(action) !== -1; +}; + +Rule.prototype.matchesConditions = function matchesConditions(action, subject) { + if(_.isUndefined(this.conditions) || _.isNull(this.conditions)) { + return true; + } + + return _.reduce(this.conditions, function(memo, value, name) { + return memo && (subject[name] === value); + }, true); +}; + +/** + * @param {Object} + * @returns boolean + */ +Rule.prototype.matchesSubject = function matchesSubject(subject) { + return _.contains(this.subjects, subject) || + this.subjects.some(function(value) { return subject instanceof value; }); +}; + +var middleware = function(Ability) { + return function(req, res, next) { + var abilities = new Ability(req.user); + + req.user.isAllowed = function() { return abilities.isAllowed.apply(abilities, arguments); }; + req.user.isNotAllowed = function() { return abilities.isNotAllowed.apply(abilities, arguments); }; + req.abilities = abilities; + + next(); + }; +}; + +module.exports = { + Ability: Ability, + connect: middleware, + express: middleware +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..21f574f --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "rampart", + "version": "1.0.0", + "description": "Authorization module for Node.js.", + "main": "./lib/rampart.js", + "dependencies":{ + "underscore": "~1.4.0" + }, + "devDependencies":{ + "chai": "~1.3.0", + "coffee-script": "~1.3.3", + "express": "~3.0.0rc5", + "mocha": "~1.6.0", + "supertest": "~0.3.1" + }, + "directories": { + "test": "test" + }, + "scripts": { + "test": "./node_modules/mocha/bin/mocha --compilers coffee:coffee-script" + }, + "keywords": [ "acl", "authorization", "access", "authorize", "connect", "express" ], + "author": { + "name": "Moveline Inc.", + "email": "info@moveline.com", + "url": "https://www.moveline.com" + }, + "contributors": [ + { + "name": "Christopher Garvis", + "email": "christopher.garvis@moveline.com", + "url": "https://github.com/cgarvis" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/Moveline/rampart.git" + }, + "license": "MIT" +} diff --git a/test/middleware.coffee b/test/middleware.coffee new file mode 100644 index 0000000..0e9b60a --- /dev/null +++ b/test/middleware.coffee @@ -0,0 +1,13 @@ +app = require './server' +chai = require 'chai' +request = require 'supertest' + +chai.should() + +describe 'Rampart Middleware', -> + describe 'logged in', -> + it 'should return 200', (done) -> + request(app).get('/1').expect 200, done + + it 'should return 401', (done) -> + request(app).get('/2').expect 401, done diff --git a/test/rampart.coffee b/test/rampart.coffee new file mode 100644 index 0000000..f174dcf --- /dev/null +++ b/test/rampart.coffee @@ -0,0 +1,98 @@ +connect = require 'connect' +chai = require 'chai' + +chai.should() + +Rampart = require '../lib/rampart' +Ability = new Rampart.Ability() + +class User + +describe 'Rampart', -> + afterEach -> + Ability.rules = [] + + describe 'can', -> + it 'should create a rule', -> + Ability.can 'read', User + Ability.rules.should.have.length 1 + + describe 'cannot', -> + it 'should create a rule', -> + Ability.cannot 'read', User + Ability.rules.should.have.length 1 + + describe 'isAllowed', -> + describe 'class level', -> + beforeEach -> + Ability.can 'read', User + + it 'should be true on `read`', -> + Ability.isAllowed('read', User).should.be.ok + + it 'should be false on `write`', -> + Ability.isAllowed('write', User).should.not.be.ok + + describe 'instance level', -> + beforeEach -> + Ability.can 'read', User + + it 'should be true on `read`', -> + Ability.isAllowed('read', new User).should.be.ok + + it 'should be true on `write`', -> + Ability.isAllowed('write', new User).should.not.be.ok + + describe 'manage action', -> + beforeEach -> + Ability.can 'manage', User + + it 'should be true on `read`', -> + Ability.isAllowed('read', User).should.be.ok + + it 'should be true on `write`', -> + Ability.isAllowed('write', User).should.be.ok + + it 'should be true on `create`', -> + Ability.isAllowed('create', User).should.be.ok + + it 'should be true on `destroy`', -> + Ability.isAllowed('destory', User).should.be.ok + + describe 'isNotAllowed', -> + describe 'class level', -> + beforeEach -> + Ability.can 'read', User + + it 'should be false on `read`', -> + Ability.isNotAllowed('read', User).should.not.be.ok + + it 'should be true on `write`', -> + Ability.isNotAllowed('write', User).should.be.ok + + describe 'instance level', -> + beforeEach -> + Ability.can 'read', User + + it 'should be false on `read`', -> + Ability.isNotAllowed('read', new User).should.not.be.ok + + it 'should be true on `write`', -> + Ability.isNotAllowed('write', new User).should.be.ok + + describe 'manage action', -> + beforeEach -> + Ability.can 'manage', User + + it 'should be false on `read`', -> + Ability.isNotAllowed('read', User).should.not.be.ok + + it 'should be false on `write`', -> + Ability.isNotAllowed('write', User).should.not.be.ok + + it 'should be false on `create`', -> + Ability.isNotAllowed('create', User).should.not.be.ok + + it 'should be false on `destroy`', -> + Ability.isNotAllowed('destory', User).should.not.be.ok + diff --git a/test/server.coffee b/test/server.coffee new file mode 100644 index 0000000..0faea96 --- /dev/null +++ b/test/server.coffee @@ -0,0 +1,29 @@ +Rampart = require '../' +express = require 'express' + +class User + constructor: (id) -> + @id = parseInt id + +class Article + constructor: (author) -> + @author = parseInt author + +class Ability extends Rampart.Ability + constructor: (user) -> + super + @can 'read', Article, {author: user.id} + +app = express() +app.use express.bodyParser() +app.use (req, res, next) -> + req.user = new User(1) + next() +app.use Rampart.express(Ability) +app.get '/:id', (req, res, next) -> + article = new Article(req.params.id) + unless req.user.isAllowed 'read', article + return res.send 401 + res.send article + +module.exports = app