node package manager

comply

The comply node.js module aims to become a useful starting point when building user based apps in Koa. It is implemented as both Koa middleware and as a command line interface. Koa Middleware controls the authentication and authorization flow of your app, while the cli allows administration of the database independent of the web server.

The comply module exports multiple functions (this readme document will refer to them as Public Functions). Complys main export function wrapps all of its public functions within koa middleware. This allows you to use the same building blocks in multiple ways, be it middleware, the cli, or your custom functions.

TOC

Install

npm install comply --save

Command Line Usage

You can set the mongoDB url with the environment variable 'DATABASE_URL', this can be useful when managing multiple projects on the same computer. Otherwise the mongoDB url will be set to mongodb://localhost/default.

To call individual comply commands (not using the comply shell):

$ comply [command] [arguments...]

shell

Calling 'comply' in your terminal will open the comply shell. The shell has bash like commands 'clear', 'exit', 'help', 'man', and uses its own history.

$ comply
Comply version 1.0.0
Connected to: mongodb://localhost/default
>

help

Use the 'help' command to list available commands with a short summary.

$ comply help
assign   assign roles to users
clear    clear terminal screen
create   create new privileges, roles, and users
exit     exit comply shell
find     list privileges, roles, or users
grant    grant privileges to roles and users
help     display this help page
man      show help manual
remove   delete roles, privileges, and users
reset    restore initial roles/privileges
revoke   revoke privileges from roles and users
show     display privileges, roles, and/or users
unassign unassign roles from users
 

man

Use the 'man' command to display manual pages. They will open in a pager for easy navigation. less is the default pager but you can specify a different one with the "--pager" option.

example:

$ comply man find
FIND(1)                      Comply Manual                      FIND(1)
 
NAME
      find - list privileges, roles, or users
 
SYNOPSIS
      find [--help] [--email PATTERN]
           [--username PATTERN]
           [--role PATTERN]
           [--privilege PATTERN]
           [--name PATTERN]
           [--description PATTERN]
           [--limit NUMBER] [--skip NUMBER]
           [--sort KEY] [TYPE]
           [SEARCH]
 
DESCRIPTION
      List privileges, roles, or users, and filter results by searching
      individual database fields, or by searching all fields at once
      using the "SEARCH" argument.  You control what kind of list you
      are creating with the "TYPE" argument, it can be one of:
      privilege/privileges, role/roles, or user/users. TYPE can be
      written singular or plural (user/users) and will make no
      difference. If you do not specify TYPE the list defaults to users.
 
      -e, --email PATTERN
             filter users by email
 
      -u, --username PATTERN
             filter users by username
 
      -r, --role PATTERN
             filter users by roles names
 
      -p, --privilege PATTERN
             filter roles and users by privileges names
 
      -n, --name PATTERN
             filter roles and privileges by name
 
      -d, --description PATTERN
             filter privileges by description
 
      -l, --limit NUMBER
             limit to NUMBER results (default is 100)
 
      -s, --skip NUMBER
             skip first NUMBER results, use with limit to page your
             results into sets.
 
      -k, --sort KEY
             sort fileds by KEY. User lists default to username, roles
             and privileges default to name
 
EXAMPLES
      find
      find roles -p privilege_a
      find role -n role_a
      find privileges -d "some text"
      find users -u a_username
      find user -e email@address.com
 
FIND(1)                      Comply Manual                      FIND(1)
 
(END)

Koa Middleware Usage

var comply = require('comply');

The examples below are using a router and a template renderer. You will need to be familiar with how koa processes middleware, as well as how koa-router routes requests.

comply(string, object)

The main comply method wraps its public functions into koa middleware. It accepts two arguments, a string (the name of the function it will wrap), and an optional object. If comply() method succeeds it sets context.state.result to the return value of the wrapped function. If context.request.body is not already set and the request method is POST, then comply will attempt to parse POST form data into the context.request.body object. if object is specified it will be merged into the context.request.body object, overriding any request body parameters already set. context.request.body will be called as the wrapped functions obj parameter.

comply('authenticate')

This middleware will check the session for a user id. If ctx.session.uid exists it will search mongoDB for that user, and if found, it converts the found mongoose document into a plain object. This user object is stripped of the 'passwordhash', 'roles', and '__v' fields. The "privileges" field is populated with the users privileges, including all of the privileges assigned to the users roles. The 'roles' field is not returned, because you should not be validating a user based on their role, your functions and templates downstream should authorize your user based solely on their privileges.

The example below calls comply('authenticate') before mounting any routers. You don't have to put this middleware before your router, but if you do then your user object will be available in all middleware downstream (ctx.state.user). You do have to put this middleware after your session middleware as it uses the session variable 'uid' to keep track of the logged in user.

in server.js:

var koa = require('koa');
var session = require('koa-session');
var views = require('koa-views');
var mount = require('koa-mount');
var routes = require('./routes');
var comply = require('comply');
 
var app = koa();
 
... other koa middleware ...
 
// template render is used in examples below 
app.use(views('./views', { "default": 'jade' }));
 
// there are multiple session modules to choose from, 
// this example is using a cookie based session store, but you 
// could just as easily be using mongoDB, redis, levelDB etc.. 
 
app.keys = ['some secret key here'];
app.use(session(app));
 
... other koa middleware ...
 
// authenticate the user if context.session.uid exists 
app.use(comply('authenticate'));
 
// use a koa router 
app.use(mount('/', routes.middleware()))
 

* all koa middleware examples below are assumed to be in a module named ./routes.js

routes.js:

var Router = require('koa-router');
var router = module.exports = new Router();
var comply = require('comply');
 

There are only a few middleware examples shown here (login, logout, authorize), the rest of the public functions would be set up identical to the login example

comply('login')

In this example the router is accepting a POST request (whose formdata is populating the public comply.login functions obj variable). It sets an error handler that will render the '/login' template if login fails, and redirect to '/profile' if login succeeds.

router.post('/login',
 
  // catch errors 
  function *(next){
    try{ yield next; }
    catch(err){
      yield this.render('/login', {
        warning: err.message
      });
    }
  },
 
  comply('login'),
 
  // success 
  function *(){
    this.redirect('/profile');
  }
 
);

comply('logout')

In this example logout deletes context.session.uid and context.state.user so that future middleware and requests will not be authorized.

router.get('/logout',
 
  comply('logout'),
 
  function *(){
    this.redirect('/login');
  }
 
);

comply('authorize', privileges)

This middleware restricts access to resources downstream. The 'privileges' argument can either be a string or an array of strings. It will check that a user is authenticated and has every privilege specified, or it will throw an error. If the user does have all specified privileges it will pass off the request to the next middleware. If comply('authenticate') has not already been called upstream it will first run comply.authenticate(ctx).

This example sets an error handler that will redirect to '/login' if authorization fails, and render the 'profile' template if authorization succeeds.

router.get('/profile',
 
  function *(next) {
    // catch errors 
    try { yield next; }
    catch(err) {
      console.log(err);
      this.redirect('/login');
    }
  },
 
  comply('authorize', 'login'),
 
  // success 
  function *(next){
    yield this.render('profile', {
      title: 'profile',
      message: 'yay I\'m logged in!'
    });
  }
 
);

Public Functions

All of Complys public functions are written as generators, this means you must use a flow control library like co to use them directly. They can be used as-is inside koa middleware because koa uses co to process middleware. The most common use of these functions will be wrapped in the main comply function (see comply(string, object)), doing so will convert the function into middleware and automatically parse POST request parameters into the wrapped function.

Generators can be used to execute multiple asynchronous functions in a more elegant way, without callbacks. Using asynchronous callbacks inside loops require async libraries and more complexity. The beauty of generators in co is that you can throw errors anywhere in your asynchronous code, it will stop executing and bubble the error up the stack until it is handled. This gives you much more control over how you handle errors. You can use 'then' and 'catch' functions of co to control the outcome of a block of code, and try/catch blocks inside co to handle individual yields.

example using comply public functions outside of koa middleware:

var co = require('co');
 
co(function *(){
 
  yield comply.removeRole({
    name: 'a_role'
  });
 
  // if 'a_role' doesn't exist execution stops 
  // here and co's catch function will call the 
  // "revoke" function that is specified 
 
  try{
    yield comply.removeRole({
      name: 'b_role'
    });
  } catch(error){
    // if 'b_role' doesn't exist you could silence 
    // the error here and continue executing, 
    // but instead we will throw an error 
    // with a different error message 
    throw new Error('b_role does not exist, but a_role was deleted!')
  }
 
  return 'you have deleted both a_role and b_role';
 
}).then(resolve).catch(revoke);
 

comply.assign(obj)

assign a role to a user

required:

  • obj.user - can be string or object
    • string username or email
    • object at least one:
      • obj.user.username
      • obj.user.email
      • obj.user.id
  • obj.role - can be string or object
    • string role name
    • object at least one:
      • obj.role.name
      • obj.role.id

example:

var result = yield comply.assign({
  user: 'temp_user',
  role: 'temp_role'
});
 

result:

result = {
  role: [mongoose document],
  user: [mongoose document]
}

comply.createPrivilege(obj)

create a new privilege

required:

  • obj.name (string)

optional:

  • obj.description (string)

example:

var privilege = yield comply.createPrivilege({
  name: 'temp_privilege',
  description: 'a temporary privilege'
});
 

result:

result = [mongoose document]

comply.createRole(obj)

create a new role

required:

  • obj.name (string)

example:

var role = yield comply.createRole({
  name: 'temp_role'
});
 

result:

result = [mongoose document]

comply.createUser(obj)

create a new user

required:

  • obj.username (string)
  • obj.email (string)
  • obj.password (string)
  • obj.passwordconfirmation (string)

example:

var user = yield comply.createUser({
  username: 'temp_user',
  email: 'temp_user@example.com',
  password:'testing123',
  passwordconfirmation:'testing123'
});
 
// passwordconfirmation is required because 
// createUser will most likely be called 
// as middleware using POST parameters. 
 

result:

result = [mongoose document]

comply.grantUser(obj)

grant a privilege to a user

required:

  • obj.user - can be string or object
    • string username or email
    • object at least one:
      • obj.user.username
      • obj.user.email
      • obj.user.id
  • obj.privilege - can be string or object
    • string privilege name
    • object at least one:
      • obj.privilege.name
      • obj.privilege.id

example:

var result = yield comply.grantUser({
  user: 'temp_user',
  privilege: 'temp_privilege'
});
 

result:

result = {
  privilege: [mongoose document],
  user: [mongoose document]
}

comply.grantRole(obj)

grant a privilege to a role

required:

  • obj.role - can be string or object
    • string role name
    • object at least one:
      • obj.role.name
      • obj.role.id
  • obj.privilege - can be string or object
    • string privilege name
    • object at least one:
      • obj.privilege.name
      • obj.privilege.id

example:

var result = yield comply.grantRole({
  role: 'temp_role',
  privilege: 'temp_privilege'
});
 

result:

result = {
  privilege: [mongoose document],
  role: [mongoose document]
}

comply.listPrivileges(obj)

search criteria (optional):

  • obj.name (string) - filter results by pattern matching privilege names
  • obj.description (string) - filter results by pattern matching privilege descriptions
  • obj.any (string) - filter results by pattern matching either privilege names or descriptions
  • obj.limit (number) - limit results to number results (default is 100)
  • obj.skip (number) - skip first number results
  • obj.sort (string) - sort results by keywords (default is 'name')

example:

var result = yield comply.listPrivileges();

result:

result = {
  name: obj.name,
  description: obj.description,
  any: obj.any,
  total: [number of total documents found matching criteria],
  limit: obj.limit,
  pages: [total number of pages sent, determined by limit and total],
  page: [current page, determined by pages and skip],
  sort: obj.sort,
  list: [array of results]
}
 
 

comply.listRoles(obj)

search criteria (optional):

  • obj.name (string) - filter results by pattern matching role names
  • obj.privilege (string) - filter results by pattern matching role privileges names
  • obj.any (string) - filter results by pattern matching either role name or role privileges names
  • obj.limit (number) - limit results to number results (default is 100)
  • obj.skip (number) - skip first number results
  • obj.sort (string) - sort results by keywords (default is 'name')

example:

var result = yield comply.listRoles();

result:

result = {
  name: obj.name,
  privilege: obj.privilege,
  any: obj.any,
  total: [number of total documents found matching criteria],
  limit: obj.limit,
  pages: [total number of pages sent, determined by limit and total],
  page: [current page, determined by pages and skip],
  sort: obj.sort,
  list: [array of results]
}

comply.listUsers(obj)

search criteria (optional):

  • obj.username (string) - filter results by pattern matching username
  • obj.email (string) - filter results by pattern matching email address
  • obj.privilege (string) - filter results by pattern matching user privileges
  • obj.role (string) - filter results by pattern matching user roles names
  • obj.any (string) - filter results by pattern matching any of the previous fields
  • obj.limit (number) - limit results to number results (default is 100)
  • obj.skip (number) - skip first number results
  • obj.sort (string) - sort results by keywords (default is 'username')

example:

var result = yield comply.listUsers();

result:

result = {
  username: obj.username,
  email: obj.username,
  privilege: obj.privilege,
  role: obj.role,
  any: obj.any,
  total: [number of total documents found matching criteria],
  limit: obj.limit,
  pages: [total number of pages sent, determined by limit and total],
  page: [current page, determined by pages and skip],
  sort: obj.sort,
  list: [array of results]
}

comply.removePrivilege(obj)

delete a privilege

required at least one of:

  • obj.name (string)
  • obj.id (string)

example:

var privilege = yield comply.removePrivilege({
  name: 'temp_privilege'
});
 

result:

result = [mongoose document]

comply.removeRole(obj)

delete a role

required at least one of:

  • obj.name (string)
  • obj.id (string)

example:

var role = yield comply.removeRole({
  name: 'temp_role'
});
 

result:

result = [mongoose document]

comply.removeUser(obj)

delete a user

required at least one of:

  • obj.user (string) username or email
  • obj.username (string)
  • obj.email (string)
  • obj.id (string)

example:

var user = yield comply.removeUser({
  user: 'temp_user'
});
 

result:

result = [mongoose document]

comply.revokeRole(obj)

revoke a privilege from a role

required:

  • obj.role - can be string or object
    • string role name
    • object at least one:
      • obj.role.name
      • obj.role.id
  • obj.privilege - can be string or object
    • string privilege name
    • object at least one:
      • obj.privilege.name
      • obj.privilege.id

example:

var result = yield comply.revokeRole({
  role: 'temp_role',
  privilege: 'temp_privilege'
});
 

result:

result = {
  privilege: [mongoose document],
  role: [mongoose document]
}

comply.revokeUser(obj)

revoke a privilege from a user

required:

  • obj.user - can be string or object
    • string username or email
    • object at least one:
      • obj.user.username
      • obj.user.email
      • obj.user.id
  • obj.privilege - can be string or object
    • string privilege name
    • object at least one:
      • obj.privilege.name
      • obj.privilege.id

example:

var result = yield comply.revokeUser({
  user: 'temp_user',
  privilege: 'temp_privilege'
});
 

result:

result = {
  privilege: [mongoose document],
  user: [mongoose document]
}

comply.unassign(obj)

unassign a role from a user

required:

  • obj.user - can be string or object
    • string username or email
    • object at least one:
      • obj.user.username
      • obj.user.email
      • obj.user.id
  • obj.role - can be string or object
    • string role name
    • object at least one:
      • obj.role.name
      • obj.role.id

example:

var result = yield comply.unassign({
  user: 'temp_user',
  role: 'temp_role'
});
 

result:

result = {
  role: [mongoose document],
  user: [mongoose document]
}

Mongoose models

The following is for advanced users and gives access to the user, role, and privilege mongoose models. This will allow you to manually create, query, update, and remove documents (users, roles, and privileges) from mongoDB. Learn more about mongoose models at http://mongoosejs.com/docs/models.html

comply.User

The user model has the following schema:

var userSchema = mongoose.Schema({
  username: {
    type: String,
    unique: true,
    lowercase: true,
    trim: true,
    required: 'username is required',
    validate: [
      /^[a-z0-9_]+$/,
      'username must only contain lowercase letters, numbers and underscores'
    ]
  },
  email: {
    type: String,
    trim: true,
    lowercase: true,
    unique: true,
    required: 'email is required'
  },
  passwordhash: { type: String, required: 'password is required' },
  roles: [{ type : mongoose.Schema.Types.ObjectId, ref: 'Role' }],
  privileges: [{ type : mongoose.Schema.Types.ObjectId, ref: 'Privilege' }]
});
  • you do not set passwordhash directly, rather you set the virtual path 'password', and the schema validator automatically creates a bCrypt hash for you, the password is never saved in plain text.
  • roles and privileges are arrays that link to mongoDB collections.

validation

  • username, email, and passwordhash (password) are required.
  • username must be 3 to 15 alphanumeric characters and allows underscores.
  • email must be a valid email address.
  • password must be 6 to 64 characters.
// new user example: 
 
var user = new comply.User({
  username: 'myusername',
  email: 'my@email.com',
  password: 'apassword'
});
 
user.save(function(err, user){
  // ... handle error 
});

Also added to the schema is a promise based save method:

user.savePromise().then(
  function(item) { /* success */ },
  function(err) { /* error */ }
);

comply.Role

The role model has the following schema:

var roleSchema = mongoose.Schema({
  name: {
    type: String,
    trim: true,
    lowercase: true,
    unique: true,
    required: 'role name is required',
    validate: [
      /^[a-z0-9_-]+$/,
      'role name must only contain lowercase letters, dashes, underscores, and numbers'
    ]
  },
  privileges: [{ type : mongoose.Schema.Types.ObjectId, ref: 'Privilege'}]
});

validation

  • name is required.
  • name must be 1 to 64 alphanumeric characters and allows underscores and dashes.
// new role example: 
 
var role = new comply.Role({
  name: 'myrole'
});
 
role.save(function(err, user){
  // ... handle error 
});

Also added to the schema is a promise based save method:

role.savePromise().then(
  function(item) { /* success */ },
  function(err) { /* error */ }
);

comply.Privilege

The privilege model has the following schema:

var privilegeSchema = module.exports = mongoose.Schema({
  name: {
    type: String,
    trim: true,
    lowercase: true,
    unique: true,
    required: 'privilege name is required',
    validate: [
      /^[a-z0-9_-]+$/,
      'privilege name must only contain lowercase letters, dashes, underscores, and numbers'
    ]
  },
  description: String
});

validation

  • name is required.
  • name must be 1 to 64 alphanumeric characters and allows underscores and dashes.
  • description is optional
// new privilege example: 
 
var privilege = new comply.Privilege({
  name: 'myprivilege',
  description: 'this is an optional description'
});
 
privilege.save(function(err, user){
  // ... handle error 
});

Also added to the schema is a promise based save method:

privilege.savePromise().then(
  function(item) { /* success */ },
  function(err) { /* error */ }
);
Author Rob Higgins © 2015