Sign Up for Free

RunKit +

Try any Node.js package right in your browser

This is a playground to test code. It runs a full Node.js environment and already has all of npm’s 400,000 packages pre-installed, including permit-authorize with all npm packages installed. Try it out:

var permitAuthorize = require("permit-authorize")

This service is provided by RunKit and is not affiliated with npm, Inc or the package authors.

permit-authorize v0.6.0

Permit Authorizer

Authorization via Permits for Javascript.

This library has been developed for Node.js but can be used on any platform which supports either the [CommonJS[(http://wiki.commonjs.org/wiki/CommonJS) or RequireJS module system.

Design and Architecture

See wiki for an overview.

Most important sections:

This project was extracted from authorize-mw to provide a self contained authorization solution. The authorize-mw project is part of a general purpose middleware stack.

Concepts

The basic idea is as follows:

A subject (fx a user) can perform an action on a given object if the subject has a permit for that action.

Main features

  • Simple DSL based permit configuration (via permit-for)
  • Ability Caching for enhanced performance (2ms lookup!)
  • Turn debugging on/off on classes or instances
  • Load permit rules from JSON (f.ex from file or data store)
  • Huge test suite included
  • Only 53kb minified :)

Code

The code has been developed in LiveScript which is very similar too Coffee script. See coffee-to-ls

Installation

The main file is index.js which exposes the following:

Authorizer :   requires.lib 'authorizer'
Ability :      requires.lib 'ability'
Allower :      requires.lib 'allower'
Permit :       requires.lib 'permit'
permit-for:    requires.permit 'permit-for'

To use this library...

authorize = require('permit-authorize');

Then require what you need as demonstrated in the usage examples below.

Permit      = authorize.Permit;
permit-for  = authorize.permit-for;

permit-for is a factory method to create permits. The other keys all point to constructor functions in the form of LiveScript "classes".

Non CommonJS (Node) usage

To use this library outside a CommonJS environment (using require for module loading), you can generate a single file that contains all the library code concatenated.

Try browserify

browserify index.js -o permit-authorize.js

To uglify and minimize...

uglifyjs permit-authorize.js -cm > permit-authorize.min.js

Standalone files available:

  • permit-authorize.js
  • permit-authorize.ugly.js
  • permit-authorize.min.js

Using bower install will install permit-authorize.min.js in the bower components folder of your app (default: bower_components).

Usage examples

The following is a complete example, using LiveScript syntax for a clearer picture.

First we require the basic modules

authorize   = require 'permit-authorize'
Permit      = authorize.Permit
permit-for  = authorize.permit-for

Then we define a Book model to be used as a "protected resource" (object).

class Book extends Base
  (obj) ->
    super ...

book = (title) ->
  new Book title

a-book = book 'some book'

Load Ability class and define convenience helper method

Ability     = authorize.Ability

ability = (user) ->
  new Ability user

Then we create a GuestUser class and a guest-user (subject)

class GuestUser extends User
  (obj) ->
    super ...

  role: 'guest'

user = (name) ->
  new User name

guest-user = (name) ->
  new GuestUser name

Define some useful variables

a-guest-user = guest-user 'unknown'

current-user = a-guest-user

current-ability = ability(current-user)

Now we need to define a permit that matches for a guest user (role) and defines what actions the subject (guest user) can perform on a Book (object).

guest-permit = permit-for('guest',
  matches-on: 
    role: 'guest'

  rules:
    read: ->
      @ucan 'read' 'Book'
    write: ->
      @ucan 'write' 'Book'
    default: ->
      @ucan 'read' 'any'
)

Define helper method user-can

user-can = (access-request) ->
  current-ability.can access-request

And use it like this

if user-can action: 'read', subject: a-book
  # code to read the book

The same example in Javascript (see usage-examples folder):

First we include the main authorize modules to be used


var lo, authorize, Permit, permitFor, Ability,
    Book, book,
    GuestUser, guestUser,
    GuestPermit, guestPermit,
    user, ability, aBook, currentUser, currentAbility,
    userCan, readBook;

authorize = require('permit-authorize');

Ability = authorize.Ability;
Permit = authorize.Permit;
permitFor = authorize.permitFor;

Then we set up some user models:

User = require(./models/user');

GuestUser = function(properties){
  // User prototypical inheritance?
  // guest user constructor code...
}
// all guest users have a role of guest
GuestUser.prototype.role = 'guest';

guestUser = new GuestUser({
  name: 'a guest'
});

We define a permit to use for authorization

guestPermit = permitFor('guest', {
  // Determine when the permit applies
  matches-on: {
    role: 'guest'
  },
  // authorization rules to apply when permit applies
  rules: {
    // action rules (dynamic)
    read: function(){
      return this.ucan('read', 'Book');
    },
    write: function(){
      return this.ucan('write', 'Book');
    },
    // default rule always applies for any user, action, subject or context
    // static rules
    'default': function(){
      return this.ucan('read', 'any');
    }
  }
});

// utility functions and constructors...
user = function(name){
  return new User(name);
};

Book = function(properties){
  // book constructor code...
}

book = function(title){
  return new Book(title);
};

ability = function(user){
  return new Ability(user);
};

userCan = function(accessRequest){
  return currentAbility.can(accessRequest);
};

userCannot = function(accessRequest){
  return currentAbility.cannot(accessRequest);
};


readBook = function(user, book){
  // code for user to read the book
};


aBook           = book('some book');
currentUser     = user('kris');
currentAbility  = ability(currentUser);

// here we go :)

if (userCan({action: 'read', subject: aBook})) {
  readBook(currentUser, aBook);
}

// or using implicit hash in the implied order: action, subject, context

if (userCannot('read', 'Book')) {
  throw new Error("Stupid illiterate user!");
}

Current status

All tests are passing :)

To facilitate testing, each class implements Debugger which allows using debug-on! on the class or instance level to track what goes on inside.

Use xdescribe, describe.skip and describe.only to select which tests to execute.

Caching

A caching strategy has been implemented as CachedAbility.

When using a CachedAbility, a cached authorization result for an AccessRequest will be retrieved from the cache and returned if present. If not found, a result will be generated and cached. The caching solution uses a fingerprint of the AccessRequest to determine the cache key.

Fingerprinting

For each of the elements making up the AccessRequest to get the "fingerprint":

  • Object: hash function is attempted called defaulting to JSON stringify if not present.
  • String: value is fingerprint
  • Array: values combined with '.'

Each of these fingerprints are concatenated into one fingerprint to be used as the cache key. If an AccessRequest with the same fingerprint (hash) is evaluated again later, the cached authorization result is fetched immediately for much better performance!

Please not that it is highly recommended to add a hash method to your User and subject models in order for the fingerprinting to work correctly and efficiently.

Performance using CachedAbility

The result can be seen by running cached_ability_test.js

for i from 1 to 10
  ability.guest.not-allowed-for(action: 'write', subject: book).should.be.true

guest ability: uncached: 123ms vs guest ability: cached: 2ms

Pretty cool :)

Loading rules from JSON file

# my/rules/editor_rules.json
{
    "editor": {
        "can": {
            "edit": "book",
            "publish": "paper"
        }
    }
}
authorize = require 'permit-authorize'
RulesLoader  = authorize.RulesLoader
 
editor-permit.rules = new RulesLoader.load('my/rules/editor_rules.json')

Some extras to facilitate creating permits from rule files or data stores

rules-loader  = new RulesLoader('my/rules/editor_rules.json')
permit        = rules-loader.create-permit 'editor permit'

# or subclass permit from existing AdminPermit class
rules-loader  = new RulesLoader('my/rules/admin_rules.json')
permit        = rules-loader.create-permit 'admin permit', AdminPermit

Load rules from a Data store/base

You can easily extend the lib/permit/permit_rules_loader.ls to load authorization rules from a Database. See the lib/permit/permit_rules_db_loader.ls for a skeleton you can extend to suit your needs.

  load-db: (@options = {}) ->
    @connect-db!
    @load-data!
    @loaded-rules = JSON.parse data
    @process-rules!

  # connect to DB
  connect-db: ->

  # load the rules from DB into a JSON structure
  load-data: ->

Simply override the connect-db and load-data functions as needed. Then use it something like this.

DbRulesLoader  = authorize.DbRulesLoader
rules = DbRulesLoader.load-db('http://my/connect/url:12345', {user: 'myname', password: 'secret'})

Design

Why LiveScript?

Since it is faster/easier to develop the basic functionality. Should be easy later refactor the code to use another approach.

Why classes and not prototypical inheritance?

See reasoning for Livescript. Was simply easier/faster to implement using classes.

Feel free to fork this project and provide a version without classes if that is a MUST for you...

TODO

  • rule-applier needs more tests...
  • refactor rule-applier and some other core modules that have functions of more than 3 lines!
  • optimize for speed! I think the engine could be at least twice as fast with some optimizations... (mostly: caching and lazy loads)

Testing

Run mocha on all files in test folder

Just run all test like this:

$ mocha

To execute individual test, do like this:

$ mocha test/authorize-mw/permit_test.js

Test coverage

The library istanbul is used for code coverage.

See code-coverage-with-mocha for use with mocha.

npm install -g istanbul
istanbul cover _mocha -- -R spec
open coverage/lcov-report/index.html

$ istanbul cover _mocha

To measure coverage of individual test:

$ istanbul cover _mocha test/authorize-mw/permit_test.js

Contribution

Please help improve this project, suggest improvements, add better tests etc. ;)

Licence

MIT License Copyright 2014-2015 Kristian Mandrup

See LICENSE file

RunKit is a free, in-browser JavaScript dev environment for prototyping Node.js code, with every npm package installed. Sign up to share your code.
Sign Up for Free