Permits module for permit-authorize
LiveScript JavaScript
Switch branches/tags
Nothing to show
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
sandbox
test
.gitignore
Changelog.md
LICENSE
Modular-Design.md
README.md
bower.json
index.js
index.ls
package.json

README.md

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 consecutive 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
  • Less than ~50 kb minified :)

Code

The code has been developed in LiveScript which is very similar too Coffee script.

See coffee-to-ls

Installation

Bower

Standalone files available in /dist folder:

  • permit-authorize.js
  • permit-authorize.min.js
  • es6/permit-authorize.js

Using bower install will install all these files in the bower components folder of your app (default: bower_components).

bower info permit-authorize bower install permit-authorize --save

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

  ability
  accessRequest
  allower
  authorizer
  permit
  rule
  util

This should allow you great flexibility to override/customize any functionality you need by "monkey patching", ie. change whatever functions or classes to your liking (if need be).

Usage examples

index.html

Loads global variable permitAuthorize object with all the exposed keys outlined above (see index.js)

<script src="/bower_components/permit-authorize/dist/permit-authorize.js"></script>

Node.js

Simply use require

var pa   = require('permit-authorize');

// define local "shorthand" vars
var Ability       = pa.ability.Ability;
var RulesLoader   = pa.permit.rule.loader.RulesFileLoader;
var permitFor     = pa.permit.factory.permitFor;

ES6 modules

For an Ember CLI app:

AMD module imported into app as 'permit-authorize'.

app.import('/bower_components/permit-authorize/dist/permit-authorize.js', {
  'permit-authorize': [
    'ability',
    'permit',
    'rule'
  ]
})

Alternatively directly import permit-authorize as an ES6 module from the /dist/es6 folder.

app.import('/bower_components/permit-authorize/dist/es6/permit-authorize.js')

Now import the modules you need in each of your local app module ;)

import { ability, permit } from 'permit-authorize';

Ability is a useful wrapper for a User object, such as currentUser which is sent along with the AccessRequestobject to the Allower to be resolved. permitFor is a factory method to create permits that hold permission rules. RulesLoader can be used to load rules into a permit from a JSON source, such as a JSON file.

Full usage example (LiveScript)

Note: LiveScript is very similar to coffeescript. One extra feature is that it turns dashed names into camelCase

permit-for => (resolved to) permitFor

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

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

# helper facotory method
book = (title) ->
  new Book title

# create sciFiBook
sci-fi-book = book 'A journey to Mars'

Load Ability class and define convenience helper method

Ability     = authorize.Ability

# factory method
ability = (user) ->
  new Ability user

Then we create a GuestUser class and a guest-user (subject). We assume here that we already have a User class.

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

  role: 'guest'

# user factory method
user = (name) ->
  new User name

# guestUser factory method
guest-user = (name) ->
  new GuestUser name

Define some users

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

current-user = a-guest-user

wrap the current user in an ability

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

Permit Container

All permits are registered in the same PermitRegistry singleton. When filtering, which permits should be taken into account, the default implementation is to iterate through the whole registry. However for many use cases, it makes sense to group permits in categories, f.ex by environment (dev, test, prod) or by domain (guest, user, admin) etc. To enable this, you can add a permit to a specific PermitContainer, then set one container as active.

Example:

permit-container = (name, desc)
    new PermitContainer(name, desc)

containers = 
    dev: permit-container 'dev', 'permits for development only'
    prod: permit-container 'prod', 'permits for production' 

containers.prod.activate!

admin-permit = permit-for 'admin user', ->
    ...
    
# add this permit to dev container (will be ignored, since this container is not active)
containers.dev.add admin-permit

# add this permit to prod container (will be used, since this container is active)
containers.prod.add prod-admin-permit

# you can active/deactivate containers as you like
containers.prod.deactivate!
containers.dev.activate!
containers.test.activate!
containers.admin.activate!

Debugging

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

Testing

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

Caching

A caching strategy for Ability 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. The fingerprinting can be customized...

Fingerprinting

The incoming AccessRequest is an object with keys and values. For each of the values making up the AccessRequest, to create the fingerprint:

  • Object: hash function is attempted called defaulting to JSON stringify if not present.
  • String: value is fingerprint
  • Array: fingeprints of all items concatenated with '.'

Each of these fingerprints are concatenated into one fingerprint to be used as the full 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'})

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

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.

Roadmap towards 1.0

  • RuleApplier* needs more tests...
  • refactor RuleApplier and some other core modules for more granularity and better testing
  • optimize for speed!

Contribution

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

Utility functions + dependencies

Currently dependencies to a few lodash functions

lodash custom builds

$ npm install -g lodash-cli
$ lodash include=extend,filter,find,map,unique

Browserify

browserify

Exposes a single global variable permitAuthorize

browserify index.js --s permitAuthorize > permit-authorize.js

To uglify (minimize)

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

For convenience, simply run the browserify-all.sh shell script in the project root.

ES6 compatible modules

Experimental e6ify.js now included:

Licence

MIT License Copyright 2014-2015 Kristian Mandrup

See LICENSE file