tdp-ah-acl

A simple yet powerful NodeJS Access Control List (ACL) module which is designed to work with TDPAHAuth and thus ActionHero

Usage no npm install needed!

<script type="module">
  import tdpAhAcl from 'https://cdn.skypack.dev/tdp-ah-acl';
</script>

README

TDPAHACL

Overview

TDPAHACL is designed to be a simple and fast access control list (ACL) module written in NodeJS, built specifically for use with the awesome ActionHeroJS API server. TDPAHACL controls access to actionHero actions based on user roles (complete with recursive inheritance) based on a simple JSON config file.

Version

Master: 0.3.5-alpha - Not recommended for production use

Warning!

This module is under development and should not be considered complere or secure...yet

Operating system support

Operating system support should not be a major issue for this module though I always mention in my projects for transparency that I do not design or test for correct function on non-*nix-like OS's (e.g. Windows). If you want to test/develop and submit pull requests for Windows support then I will certainly consider them but likely only if they have minimal to no impact on performance and reliability for *nix-like OSs. The reason for this is simply that I personally don't consider Windows to be a serious server operating system and thus a good direction in which to spend my time and i'm not trying to offend anyone. I realise that others may completely differ in opinion and you're always welcome (obviously) to fork the repo and create your own version with Windows support. If my time were unlimited then I would likely spend some time on Windows support but sadly it's not.

Dependencies

TDPAHACL itself has a few dependencies (which are all NPM modules):

  • Normal usage:
    • fs (core nodejs module) - filesystem functions
    • path (core nodejs module) - filesystem path functions
    • util (core nodejs module) - general purpose utilities
    • semver - semantic version numbering functions
  • Installer:
  • Testing:
    • mocha - javascript test framework, used in tests
    • should - assertion library used with mocha in tests

Issues/problems

Please log any issues/problems in Github issues for this project.

Installation

Installation is simple, either:

  • From your command line (under the relevant user account): npm install tdp-ah-acl
    or
  • Include tdp-ah-acl as a dependency in your NPM package.json file and npm install your module
    or
  • clone the Github repo git clone git://github.com/neilstuartcraig/TDPAHACL.git (assuming you have git installed). You'll probably need to amend the require() path if you use this method (unless you clone into your node_modules directory)

Usage

Once you have installed the NPM module, you can require() the module as normal in your script, then use the new constructor and finially initialise e.g.

var TDPAHACL=require("tdp-ah-acl");
var tdpahacl=new TDPAHACL();
tdpahacl.init(api, "../config/TDPAHACL/TDPAHACLConfig.js", __dirname);

Then you'll need to run the ACL check, usually in an actionHero action preProcessor e.g:

var TDPAHACLPreProcessor=function(connection, actionTemplate, next)
{	
    tdpahacl.roleHasPermissionsOnAction(userRole, actionTemplate.name, actionTemplate.version, function TDPAHACLRoleHasPermissionsOnAction(roleAllowed)
    {
        if(roleAllowed)
        {
            next(connection, true);
        }
        else
        {
            next(connection, false);
        }
    });
};
    
// You could use unshift or push here
api.actions.preProcessors.unshift(TDPAHACLPreProcessor);	

For a complete, working example, see the example initialiser file in this module.

Configuration

Configuration is via either a directly passed-in standard javascript object or a file (as shown below) which contains a valid NodeJS module which exports exactly/only a javascript object in the correct format.

Configuration object format

The format for the config file is:

var ACLTestConfig=
{
    ruleProcessingOrder:"deny,allow", // "deny,allow" or "allow,deny"
    allowReinitialisation:false, // true/false
    exitOnRoleProcessingError:true, // true/false
    rules: // object containing sub-objects, one per role
    {
        role1:
        {
            inheritsFromRoles:["<ROLE-2>","<ROLE-3>"], // Array of roles to inherit from
            allow: // Array of actions (an optionally version ranges) to allow for this role
            [
                "<ACTION-NAME-1>[:<SEMVER-ACTION-1-VERSION-RANGE]", 
                "<ACTION-NAME-2>[:<SEMVER-ACTION-2-VERSION-RANGE]"
            ],
            deny: // Array of actions (an optionally version ranges) to deny for this role
            [
                "<ACTION-NAME-3>[:<SEMVER-ACTION-3-VERSION-RANGE]",
                "<ACTION-NAME-4>[:<SEMVER-ACTION-4-VERSION-RANGE]"
            ]
        },
        ...
        roleN:
        {
            ...
        },
        ...
    }
}

Configuration notes:

Most of the config should be self-explanatory but some notes are:

  • By default, access is denied i.e. if you do not explicitly allow access for a role to an action, it will be denied. This is not overrideable as it's a sensible behaviour which allows the least chance of incorrect configuration
  • ruleProcessingOrder controls the order in which rules are processed and calculated e.g. allow,deny order with an allow {"*"} then a deny{"admin"} would allow that role access to everything except the "admin" action
  • If a role inherits permissions from one or more other roles, permissions set on that user will override any inherited permissions ie.g. is user1 is denied access to actionA and user1 inherits from user2 who is allowed access to actionA, user1 will not be able to access actionA
  • Inherited roles are applied right to left in the order specified in inheritsFromRoles
  • Action version-specific rules are supported, you can (optionally) define semantic version compatible version ranges
  • Action versions which are not semver compatible will be automatically converted during processing e.g. a version of 2 will be converted to 2.0
  • allowReinitialisation is a simple control to prevent the init() function being re-run whilst actionHero is running - this is just a basic security measure to help prevent 3rd party modules overwriting the ACL
  • exitOnRoleProcessingError will cause the init() function to exit if it encounters errors whilst processing ACL rules
  • ACL rules for actions/versions which do not exist will be ignored and not rolled into the calculated ACL

Configuration via config file

Configuration via config file is as simple as passing a path to the config file to the init() function e.g.:

var TDPAHACL=require("tdp-ah-acl");
var tdpahacl=new TDPAHACL();
tdpahacl.init(api, "../config/TDPAHACL/TDPAHACLConfig.js", __dirname);

Configuration via JavaScript object

Configuration via object is a simple matter of passing a JavaScript object to the run() function e.g. (these rules are contrived so don't read too deeply into them specifically):

var ACLConfig=
{
    ruleProcessingOrder:"deny,allow", 
    allowReinitialisation:false,
    exitOnRoleProcessingError:true,
    rules: 
    {
        superUser:
        {
            allow:
            [
                "*"
            ]
        },
        admin:
        {
            inheritsFromRoles:["statsUser","authenticatedUser"],
            allow:
            [
                "admin/*:>=2.0"
            ],
            deny:
            [
                "admin/*:>=3.0"
            ]
        },
        statsUser:
        {
            allow:
            [
                "stats/appInfo:>=0.0",
                "stats/serverInfo:>=0.0",
                "stats/graphs/*"
            ],
            deny:
            [
                "stats/graphs/sensitiveInfo"
            ]
        },
        authenticatedUser:
        {
            inheritsFromRoles:["public"],
            allow:
            [
                "pageContent:>=2.0",
                "navigation:>=1.0"
            ],
            deny:
            [
                
            ]
        },
        public:
        {
            allow:
            [
                "login:>=3.0"
            ]
        }
    }
}

module.exports=ACLConfig;

tdpahacl.init(api, ACLConfig);

Usage

TDPAHACL has a number of functions and variables, most of these are private to the instance itself, you can see them by looking at the source code of the main module. The public functions are as follows:

TDPAHACL.init(api, configString, relativeToPath)

Overview

TDPAHACL.init() is the (synchronous) initialisation function which sets up the instance, importing config and creating the ACL map (a private object) which controls access to actions.

Parameters

  • api - (Object) the actionHero API main object
  • configString - (String or Object) either a Javascript configuration object literal or a path to a config file which contains a NodeJS module as configuration - both as per the above
  • relativeToPath - (String) if configString is a path to a config file, this is (optionally) a path to which the configString is relative

Example usage

See the example initialiser for an example use (or the example above).

TDPAHACL.roleHasPermissionsOnAction(role, actionName, actionVersion, callback)

Overview

TDPAHACL.init() is the (asynchronous) ACL rule check function which determines whether or not a role has permission to run an action/version.

Parameters

  • role - (String) the role of the user for whom permissions are being checked
  • actionName - (String) the name (action.name) of the actionHero action which is being checked
  • actionVersion - (String / Semantic version number or integer) the version (action.version) of the ationHero action being checked. Note: integer numbers will be automatically converted to semantic version numbers e.g. 1 will become 1.0
  • callback - (Function) a callback function which will be passed one boolean parameter, roleAllowed which is true if the role has permission to run the action/version being tested, false otherwise (including when no relevant rule exists)

Example usage

See the example initialiser for an example use (or the example above).

TDPAHACL.normaliseActionName(actionName)

Overview

This (synchronous) function is not expected to be used often but is a simple method which normalises action.name's passed in to this module to try to ensure consistency and reduce the likelihood of error. It currently simply trims whitespace from the beginning and end of the actionName.

Parameters

  • actionName - (String) the action.name being processed

Example usage

var normalisedActionName=TDPAHACL.normaliseActionName(actionName);

TDPAHACL.normaliseActionVersion(actionVersion)

Overview

This (synchronous) function is not expected to be used often but is a simple method which normalises action.version's passed in to this module to try to ensure consistency and reduce the likelihood of error. It converts integer action.versions into semantic version numbers e.g. 1 becomes '1.0'.

Parameters

  • actionVersion - (String) the action.version being processed

Example usage

var normalisedActionVersion=TDPAHACL.normaliseActionVersion(actionVersion);

Logging

TDPAHACL uses the built-in winston logging from actionHero (configuration information). The bundled tests however just use console.log (and not in a very clever way) for simplicity.

How it works

Since actionHero API apps are well-defined in terms of their available endpoints (publicly available API methods), TDPAHACL works a little differently to most other ACLs which do not have such strong definition - at least it's different to those I have previously used. The major setup is performed by the init() method, no real surprises there, what init() does is roughly:

  1. Read in config files and overload userland config onto the default config (for uniformity and default config inheritance)
  2. Runs through each API action (and version) and recursively calculates which roles are allowed to access them, creating an in-memory map/hash of this

This means we offload the vast majority of the processing work to the initialisation stage, meaning our runtime process is fast, really fast - all that's needed is a test on the ACL map hash for each action/version requested.

If you need to know more, you're probably best looking through the main module library and the example actionHero initialiser.

Why is TDPAHACL not param(eter) aware?

This is something I considered however, as a general rule, your API should be granular enough that you can appropriately restrict permissions based on the action name only. For example, a well-modularised API should have endpoints roughly as follows:

  • auth/login
  • auth/register
  • auth/delete
  • auth/logout

Rather than a single auth endpoint which might receive a parameter which determined the action (login/register/delete/logout) to perform.

incidentally, you can achieve the above in actionHero by creating a directory/folder inside the "actions" directory/folder called "auth", adding actions inside the "auth" directory/folder called login, register, delete and logout whose action.name is "auth/login", "auth/register", "auth/delete" and "auth/logout".

Parameter-awareness may appear at some later stage in TDPAHACL but don't count on it! Relying on the action name and version alone is faster and simpler yet still works for most use-cases. You're welcome to raise an issue if you really need parameter awareneness (or fork the project).

Security

Security is obviously (being an ACL) a pretty high priority for this module. A key part of the security is in the public/private methods of the module, achieved by using function-scope, only listing public methods where strictly necessary.
The security of the module will be subject to continuous evaluation and suggestions/pull requests which improve the situation are very much appreciated.

To do

  • SHOULD BE DONE: installer script using TDPGlobFileCopier
  • Test usage as NPM module - does defaultConfig.js work? (run tests to verify)
  • Finalise logging
  • Finalise tests
    • Need to add test(s) to verify:
      • ruleProcessingOrder
      • allowReinitialisation
      • exitOnRoleProcessingError
  • Move getConfigObject and overloadJSObject out to a module

Change log

Major changes are listed below:

  • v0.3 - getACLMapForActionVersion() is now more comprehensive and fully recursive (with role inheritance), consequently some parameters changed. Strict mode.
  • v0.2 - roleHasPermissionsOnAction() is now asynchronous
  • v0.1 - Initial version

License

TDPAHACL is issued under a Creative Commons attribution share-alike license. This means you can share and adapt the code provided you attribute the original author(s) and you share your resulting source code. If, for some specific reason you need to use this library under a different license then please contact me and i'll see what I can do - though I should mention that I am committed to all my code being open-source so closed licenses will almost certainly not be possible.