connect-roles
Advanced tools
Comparing version 1.0.1 to 2.0.0
181
index.js
"use strict"; | ||
var debug = require('debug')('connect-roles'); | ||
var pathToRegexp = require('path-to-regexp'); | ||
var functionList = []; | ||
var failureHandler = function failureHandler(req, res, action) { | ||
res.send(403); | ||
res.send(403); | ||
}; | ||
var defaultUser = {}; | ||
module.exports = function middleware(req, res, next) { | ||
if (res.locals) attachHelpers(req, res.locals); | ||
attachHelpers(req, req); | ||
next(); | ||
var exports = module.exports = function middleware(req, res, next) { | ||
if (res.locals) attachHelpers(req, res.locals); | ||
attachHelpers(req, req); | ||
next(); | ||
}; | ||
exports.use = use; | ||
function use() { | ||
if (arguments.length === 1) { | ||
use1.apply(this, arguments); | ||
} else if (arguments.length === 2) { | ||
use2.apply(this, arguments); | ||
} else if (arguments.length === 3) { | ||
use3.apply(this, arguments); | ||
} else { | ||
throw new Error('use can have 1, 2 or 3 arguments, not ' + arguments.length); | ||
} | ||
} | ||
function attachHelpers(req, obj) { | ||
var oldUser = req.user; | ||
obj.user = req.user || Object.create(defaultUser); | ||
if(oldUser){ | ||
obj.user.isAuthenticated = true; | ||
}else{ | ||
obj.user.isAuthenticated = false; | ||
function use1(fn) { | ||
if (typeof fn !== 'function') throw new Error('Expected fn to be of type function'); | ||
functionList.push(fn); | ||
} | ||
function use2(action, fn) { | ||
if (typeof action !== 'string') throw new Error('Expected action to be of type string'); | ||
if (action[0] === '/') throw new Error('action can\'t start with `/`'); | ||
use1(function (req, act) { | ||
if (act === action) { | ||
return fn(req); | ||
} | ||
if(obj.user){ | ||
obj.user.is = tester(req,'is'); | ||
obj.user.can = tester(req,'can'); | ||
}); | ||
} | ||
function use3(action, path, fn) { | ||
if (typeof path !== 'string') throw new Error('Expected path to be of type string'); | ||
var keys = []; | ||
var exp = pathToRegexp(path); | ||
use2(action, function (req) { | ||
var match; | ||
if (match = exp.exec(req.path)) { | ||
req = Object.create(req); | ||
req.params = Object.create(req.params || {}); | ||
keys.forEach(function (key, i) { | ||
req.params[key.name] = match[i+1]; | ||
}); | ||
return fn(req); | ||
} | ||
}); | ||
} | ||
module.exports.log = false; | ||
module.exports.can = routeTester('can'); | ||
module.exports.is = routeTester('is'); | ||
module.exports.isAuthenticated = isAuthenticated; | ||
exports.can = routeTester('can'); | ||
exports.is = routeTester('is'); | ||
exports.isAuthenticated = isAuthenticated; | ||
function isAuthenticated(req,res,next) { | ||
if(arguments.length === 0){ return isAuthenticated; } | ||
if (req.user && req.user.isAuthenticated === true){ next(); } | ||
else if(req.user){ failureHandler(req, res, "isAuthenticated"); } | ||
else { throw new Error("Request.user was null or undefined, include middleware"); } | ||
if(arguments.length === 0){ return isAuthenticated; } | ||
if (req.user && req.user.isAuthenticated === true){ next(); } | ||
else if(req.user){ failureHandler(req, res, "isAuthenticated"); } | ||
else { throw new Error("Request.user was null or undefined, include middleware"); } | ||
}; | ||
module.exports.useAuthorisationStrategy = useAuthorizationStrategy; | ||
function useAuthorizationStrategy(path, fn) { | ||
if(typeof path === "function"){ | ||
fn = path; | ||
} | ||
functionList.push(function(user, action, stop){ | ||
if(typeof path === "string" && path !== action){ | ||
return null; | ||
} | ||
return fn.call(this, user, action, stop); | ||
}); | ||
return this; | ||
}; | ||
module.exports.setFailureHandler = setFailureHandler; | ||
exports.setFailureHandler = setFailureHandler; | ||
function setFailureHandler(fn) { | ||
failureHandler = fn; | ||
failureHandler = fn; | ||
}; | ||
module.exports.setDefaultUser = setDefaultUser; | ||
exports.setDefaultUser = setDefaultUser; | ||
function setDefaultUser(user) { | ||
defaultUser = user; | ||
defaultUser = user; | ||
}; | ||
@@ -65,42 +84,46 @@ | ||
function tester(req, verb){ | ||
return function(action){ | ||
var result = null, | ||
vote; | ||
var stop = false; | ||
function stopNow(vote){ | ||
stop = true; | ||
if (vote === false){ | ||
result = false; | ||
} else if (vote === true) { | ||
result = true; | ||
} | ||
} | ||
for (var i = 0; i<functionList.length && !stop; i++){ | ||
var fn = functionList[i]; | ||
vote = fn.call(req, req.user, action, stopNow); | ||
if(vote === false){ | ||
stop = true; | ||
result = false; | ||
} else if (vote === true){ | ||
result = true; | ||
} | ||
} | ||
if(module.exports.log){ | ||
console.log('Check Permission: ' + (req.user.id||req.user.name||"user") + | ||
"."+(verb||'can')+"('" + action + "') -> " + (result === true)); | ||
} | ||
return (result === true); | ||
return function(action){ | ||
var result = null, vote; | ||
var stop = false; | ||
for (var i = 0; i<functionList.length && !stop; i++){ | ||
var fn = functionList[i]; | ||
vote = fn(req, action); | ||
if(vote === false){ | ||
stop = true; | ||
result = false; | ||
} else if (vote === true){ | ||
result = true; | ||
} | ||
} | ||
debug('Check Permission: ' + (req.user.id||req.user.name||"user") + | ||
"." + (verb || 'can') + "('" + action + "') -> " + (result === true)); | ||
return (result === true); | ||
}; | ||
} | ||
function routeTester(verb) { | ||
return function (action){ | ||
return function (req, res, next) { | ||
if(tester(req,verb)(action)){ | ||
next(); | ||
}else{ | ||
//Failed authentication. | ||
failureHandler(req, res, action); | ||
} | ||
}; | ||
}; | ||
} | ||
function routeTester(verb){ | ||
return function (action){ | ||
return function(req,res,next){ | ||
if(tester(req,verb)(action)){ | ||
next(); | ||
}else{ | ||
//Failed authentication. | ||
failureHandler(req, res, action); | ||
} | ||
}; | ||
}; | ||
function attachHelpers(req, obj) { | ||
var oldUser = req.user; | ||
obj.user = req.user || Object.create(defaultUser); | ||
if(oldUser){ | ||
obj.user.isAuthenticated = true; | ||
}else{ | ||
obj.user.isAuthenticated = false; | ||
} | ||
if(obj.user){ | ||
obj.user.is = tester(req,'is'); | ||
obj.user.can = tester(req,'can'); | ||
} | ||
} |
{ | ||
"name": "connect-roles", | ||
"description": "Provides dynamic roles based authentication for node.js connect and express servers.", | ||
"version": "1.0.1", | ||
"version": "2.0.0", | ||
"homepage": "http://documentup.com/ForbesLindesay/connect-roles", | ||
@@ -30,3 +30,7 @@ "repository": { | ||
"everyauth" | ||
] | ||
], | ||
"dependencies": { | ||
"debug": "https://github.com/ForbesLindesay/debug/archive/master.tar.gz", | ||
"path-to-regexp": "https://github.com/ForbesLindesay/path-to-regexp/archive/master.tar.gz" | ||
} | ||
} |
292
readme.md
[![Build Status](https://secure.travis-ci.org/ForbesLindesay/connect-roles.png?branch=master)](http://travis-ci.org/ForbesLindesay/connect-roles) | ||
# Connect Roles | ||
Connect roles is designed to work with connect or express. It is an authorization provider, not an authentication provider. It is designed to support context sensitive roles/abilities, through the use of middleware style authorization strategies. If you're looking for an authentication system I suggest you check out [passport.js](https://github.com/jaredhanson/passport) | ||
Connect roles is designed to work with connect or express. It is an authorisation provider, not an authentication provider. It is designed to support context sensitive roles/abilities, through the use of middleware style authorisation strategies. | ||
All code samples assume you have already used: | ||
If you're looking for an authentication system I suggest you check out [passport.js](https://github.com/jaredhanson/passport) | ||
```javascript | ||
var app = require('express').createServer();//could also use connect | ||
var user = require('connect-roles'); | ||
## Installation | ||
app.use(/* Your authentication middleware goes here */); | ||
app.use(user);//Load the connect-roles middleware here | ||
``` | ||
$ npm install connect-roles | ||
For an example of this in use, see server.js (which requires you install express) | ||
## Usage | ||
## Installation | ||
```javascript | ||
var authentication = require('your-authentication-module-here'); | ||
var user = require('connect-roles'); | ||
var express = require('express'); | ||
var app = express(); | ||
npm install connect-roles | ||
app.use(authentication) | ||
app.use(user); | ||
## Authorization | ||
//anonymous users can only access the home page | ||
//returning false stops any more rules from being | ||
//considered | ||
app.use(function (req, action) { | ||
if (!req.user.isAuthenticated) return action === 'access home page'; | ||
}) | ||
Connect Roles assumes that you have authentication middleware to set the user. It expects the user to be on the request object as `req.user`. It makes no assumptions about what this value contains. If this value is not used, it does not matter as the authentication strategies also have access to the request object itself | ||
//moderator users can access private page, but | ||
//they might not be the only one so we don't return | ||
//false if the user isn't a moderator | ||
app.use('access private page', function (req) { | ||
if (req.user.role ==== 'moderator') { | ||
return true; | ||
} | ||
}) | ||
## Defining authentication strategies | ||
To define authentication strategies, call the useAuthorisationStrategy function: | ||
@param [path] {string} The action/path/ability/role that this strategy applies to. The strategy will be ignored for all other roles/abilities. If it is not present, the strategy is used for all roles/abilities. | ||
@param fn {function} The function to call to determine whether the user is authorized. | ||
@param fn.this {object} The value of this inside the function is the current request, useful for dynamic authorization. | ||
@param fn.user {object} The user found at req.user (also available as this.user), note that this could be null/undefined if the user is not authenticated. | ||
@param fn.action {string} The action/role/ability etc. that we are checking permission for. | ||
@param fn.stop {function} A function which can be called with or without the vote to make this the last strategy which is used (see anonymous example). | ||
@param [fn.stop.vote] {boolean} The vote, true, false or null as below. | ||
@param [fn.returns vote] {boolean} The function can optionally return a vote, if this is false, then access will be denied, if this is true and nothing returns false, access will be granted. | ||
```javascript | ||
user.useAuthorisationStrategy(function(user, action, stop){ | ||
//User logic here. | ||
//admin users can access all pages | ||
user.use(function (req) { | ||
if (req.user.role === 'admin') { | ||
return true; | ||
} | ||
}); | ||
//Or | ||
user.useAuthorisationStrategy("create user", function(user, action, stop){ | ||
//User logic here. | ||
//optionally controll the access denid page displayed | ||
user.setFailureHandler(function (req, res, action){ | ||
var accept = req.headers.accept || ''; | ||
res.status(403); | ||
if (~accept.indexOf('html')) { | ||
res.render('access-denied', {action: action}); | ||
} else { | ||
res.send('Access Denied - You don\'t have permission to: ' + action); | ||
} | ||
}); | ||
``` | ||
### Anonymous User | ||
You should probably handle anonymous users first. This is important because it means you then won't have to handle anonymous users individually in every other function, providing you call stop. | ||
If you have anything that an anonymous user is capeable of, you must then check before checking for "anonymous". | ||
```javascript | ||
user.useAuthorisationStrategy("register", function(user){ | ||
if(!user.isAuthenticated) return true; | ||
app.get('/', user.can('access home page'), function (req, res) { | ||
res.render('private'); | ||
}); | ||
app.get('/private', user.can('access private page'), function (req, res) { | ||
res.render('private'); | ||
}); | ||
app.get('/admin', user.can('access admin page'), function (req, res) { | ||
res.render('admin'); | ||
}); | ||
user.useAuthorisationStrategy(function(user, action, stop){ | ||
if(!user.isAuthenticated){ | ||
stop(action === "anonymous"); | ||
} | ||
}); | ||
app.listen(3000); | ||
``` | ||
### Roles | ||
## API | ||
If you have a user object which looks like `{id:10, roles:["RoleA", "RoleB"]}` you could use the following to provide roles checking. | ||
### roles.use(fn(req, action)) | ||
```javascript | ||
user.useAuthorisationStrategy(function(user, action){ | ||
if(user.isAuthenticated){//You can remove this if already checking for anonymous users | ||
for(var i = 0; i < user.roles.length; i++){ | ||
if(user.roles[i] === action) return true; | ||
} | ||
} | ||
}); | ||
``` | ||
Define and authorisation strategy which takes the current request and the action being performed. fn may return `true`, `false` or `undefined`/`null` | ||
### Dynamic | ||
If `true` is returned then no further strategies are considred, and the user is **granted** access. | ||
This example is what makes this library special. | ||
If `false` is returned, no further strategies are considered, and the user is **denied** access. | ||
```javascript | ||
user.useAuthorisationStrategy("edit user", function(user, action){ | ||
if(user.isAuthenticated){//You can remove this if already checking for anonymous users | ||
if(this.params.userid){//`this` refers to the current request object | ||
if(user.id === this.params.userid){ | ||
return true; | ||
} | ||
} | ||
} | ||
}); | ||
If `null`/`undefined` is returned, the next strategy is considerd. If it is the last strategy then access is **denied**. | ||
//Then you can use the following in express | ||
app.get('/user/:userid/edit', user.can("edit user"), function(req,res){ | ||
//Only called if the user is editing themselves, not other people. | ||
}); | ||
``` | ||
### roles.use(action, fn(req)) | ||
## Inline authorization for connect or express | ||
The strategy `fn` is only used when the action is equal to `action`. It has the same behaviour with regards to return values as `roles.use(fn(req, action))` (see above). | ||
Providing you have supplied the middleware (see the first section of this guide) you can use the following functions. | ||
It is equivallent to calling: | ||
### req.isAuthenticated | ||
```javascript | ||
roles.use(function (req, act) { | ||
if (act === action) { | ||
return fn(req); | ||
} | ||
}); | ||
``` | ||
This is a property that is either true or false to tell you whether the user object is present. | ||
**N.B.** The action must not start with a `/` character or it will call `roles.use(path, fn(req, action))` | ||
### req.user.can, req.user.is | ||
### roles.use(action, path, fn(req)) | ||
These functions are all the same, but be aware that methods of the form req.user.* will throw exceptions if user is null. | ||
Path must be an express style route. It will then attach any parameters to `req.params`. | ||
e.g. | ||
```javascript | ||
app.get("/canifly", function(req,res){ | ||
if(req.userCan("fly")) res.send("You can fly"); | ||
else res.send("You can't fly"); | ||
roles.use('edit user', '/user/:userID', function (req) { | ||
if (req.params.userID === req.user.id) return true; | ||
}); | ||
app.get("/logout", function(req,res){ | ||
//Note how we check authenticated first. | ||
if(req.isAuthenticated && req.user.can("logout")){ | ||
logout(); | ||
}else{ | ||
throw "user can't log out"; | ||
} | ||
}); | ||
``` | ||
### Inside view | ||
Inside a view, you can use `user.isAuthenticated`, `user.is` and `user.can` exactly as you would inside the route handler (Except they aren't attached to the request handler). This is useful for making small UI adjustments, but probably shouldn't be used as the main part of security, I recommend you do that before sending stuff to the view. Just use this to hide buttons that would cause authorization errors. | ||
Note that this authorisation strategy will only be used on routes that match `path`. | ||
## Route middleware for express | ||
It is equivallent to calling: | ||
In express you can provide route middleware. This is perfect for authentication, especially with wildcards. | ||
```javascript | ||
var keys = []; | ||
var exp = pathToRegexp(path); | ||
roles.use(function (req, act) { | ||
var match; | ||
if (act === action && match = exp.exec(req.path)) { | ||
req = Object.create(req); | ||
req.params = Object.create(req.params || {}); | ||
keys.forEach(function (key, i) { | ||
req.params[key.name] = match[i+1]; | ||
}); | ||
return fn(req); | ||
} | ||
}); | ||
``` | ||
### roles.can(action) and roles.is(action) | ||
### Protect entire admin section in one line | ||
`can` and `is` are synonyms everywhere they appear. | ||
Simply put this before you have any other routes beginning /admin | ||
You can use these as express route middleware: | ||
```javascript | ||
app.get("/admin*", user.is("admin")); | ||
var user = roles; | ||
app.get('/profile/:id', user.can('edit profile'), function (req, res) { | ||
req.render('profile-edit', { id: req.params.id }); | ||
}) | ||
app.get('/admin', user.is('admin'), function (req, res) { | ||
res.render('admin'); | ||
} | ||
``` | ||
### Only let people edit themselves | ||
### req.user.can(action) and req.user.is(action) | ||
```javascript | ||
user.useAuthorisationStrategy("edit user", function(user, action){ | ||
if(user.isAuthenticated){//You can remove this if already checking for anonymous users | ||
if(this.params.userid){ | ||
if(user.id === this.params.userid){ | ||
return true; | ||
} | ||
} | ||
} | ||
}); | ||
`can` and `is` are synonyms everywhere they appear. | ||
//Then you can use the following in express | ||
app.get('/user/:userid/edit', user.can("edit user"), function(req,res){ | ||
//Only called if the user is editing themselves, not other people. | ||
}); | ||
``` | ||
These functions return `true` or `false` depending on whether the user has access. | ||
### Chain things | ||
e.g. | ||
```javascript | ||
user.useAuthorisationStrategy("register", function(user){ | ||
if(!user.isAuthenticated) return true; | ||
}); | ||
app.get('/', function (req, res) { | ||
if (req.user.is('admin')) { | ||
res.render('home/admin'); | ||
} else if (user.can('login')) { | ||
res.render('home/login'); | ||
} else { | ||
res.render('home'); | ||
} | ||
}) | ||
``` | ||
user.useAuthorisationStrategy(function(user, action, stop){ | ||
if(!user.isAuthenticated){ | ||
stop(action === "anonymous"); | ||
} | ||
}); | ||
### user.can(action) and user.is(action) | ||
app.get("/register", user.is("anonymous"), user.can("register"), function(req,res){ | ||
//Only called if the user can register. | ||
}); | ||
Inside the views of an express application you may use `user.can` and `user.is` which are equivallent to `req.user.can` and `req.user.is` | ||
e.g. | ||
```html | ||
<% if (user.can('impersonate')) { %> | ||
<button id="impersonate">Impersonate</button> | ||
<% } %> | ||
``` | ||
## Failure handler | ||
**N.B.** not displaying a button doesn't mean someone can't do the thing that the button would do if clicked. The view is not where your security should go, but it is important for useability that you don't display buttons that will just result in 'access denied' where possible. | ||
You can (and should) set the failure handler. This is called whenever a user fails authorization in route middleware. | ||
### roles.setFailureHandler(fn(req, res, action)) | ||
It is set as follows: | ||
You can (and should) set the failure handler. This is called whenever a user fails authorisation in route middleware. | ||
Defaults to: | ||
```javascript | ||
user.setFailureHandler(function (req, res, action){ | ||
res.send(403); | ||
res.send(403); | ||
}); | ||
``` | ||
That, incidentally is the default implimentation. There is no "next" by design, to stop you accidentally calling it and allowing someone into a restricted part of your site. You are passed the action/role/ability which caused them to be denied access. | ||
There is no "next" by design, to stop you accidentally calling it and allowing someone into a restricted part of your site. You are passed the action requested which caused them to be denied access. | ||
### Redirect on failure | ||
You could using this to redirect the user or render an error page: | ||
You should probably consider using this to redirect the user, something like: | ||
```javascript | ||
user.setFailureHandler(function (req, res, action){ | ||
if(req.user){ | ||
res.redirect('/accessdenied?reason=' + action); | ||
} else { | ||
res.redirect('/login'); | ||
} | ||
var accept = req.headers.accept || ''; | ||
res.status(403); | ||
if(req.user.isAuthenticated){ | ||
if (~accept.indexOf('html')) { | ||
res.render('access-denied', {action: action}); | ||
} else { | ||
res.send('Access Denied - You don\'t have permission to: ' + action); | ||
} | ||
} else { | ||
res.redirect('/login'); | ||
} | ||
}); | ||
``` | ||
## Default User | ||
## License | ||
By default, the user middleware will set the user up to be `{}` and will then add the property `isAuthenticated = false`. | ||
Roles will always add `isAuthenticated = false` but you can configure a default user object as follows. | ||
```javascript | ||
user.setDefaultUser({id:"anonymous"}); | ||
``` | ||
MIT |
HTTP dependency
Supply chain riskContains a dependency which resolves to a remote HTTP URL which could be used to inject untrusted code and reduce overall package reliability.
Found 2 instances in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
0
15157
2
6
220
216
2