Skip to content

Identification and access management library for all JS runtimes that support ES Modules.

License

Notifications You must be signed in to change notification settings

coreybutler/iam

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

IAM
Identification and Access Management

Share with your developer network using a quick tweet.

IAM is an access control framework that runs on all JavaScript runtimes (Browsers, Node.js, Deno, etc). It is lightweight, built on standards, and incredibly powerful.

The library manages roles and permissions, allowing developers to create simple or complex authorization patterns. The main benefit is the ridiculously lightweight query engine, which primarily answers one question:

"Does the user have the right to do something with the system resource?"

if (user.authorized('system resource', 'view')) {
  display()
} else {
  throw new Error('Access Denied')
}

Version 2.0.0-alpha update notice:

A new major version has been released, though it still functions almost identically to the 1.x.x branch. Here's why:

  1. Version 1.0.0 was created before ES Modules were widely supported. Now that ES Module support is common, the library no longer needs separate packages for distribution.
  2. This was originally released as a library for Butler Logic clients. Fortunately, it has become a more generic tool, available for a general audience. As such, we've moved the package from @butlerlogic/iam to @author.io/iam. The Author npm organization is more suitable for long term support.

This release also introduced 200+ unit tests.

How it works:

IAM keeps track of resources, rights, roles, and groups. By maintaining the permission structure within the library (internally), it is capable of automatically deriving user rights, even in complex schemas. It's like a permissions calculator.

The library is designed under the guiding principle that determining whether a user is authorized to view/use a specific feature of an application should always be a binary operation.

Shortcuts

Corey Butler (original author) gave a recorded introduction to IAM, available here. The companion slides are available at edgeatx.org/slides.

Abstracting complexity

Problems with authorization are typically caused by conditional logic that is too complicated.

Consider the following "authorization" question:

"Is the user allowed to use this feature, or are they part of a group that can access this feature, or have they been explicitly denied access to a feature, or are they part of a group that's part of another group that has permission, or are any permission overrides to account for?"

Just like proper sentences, code shouldn't have "run on" logic. Being a mental gymnast should not be a prerequisite to understand whether someone can access a system component or not. IAM abstracts this complexity.

Example Browser UI

The code for this is available in the basic example.

IAM Example UI

Example Node API

The code for this is available in the api example.

In this example, requireAuthorization is Express middleware that maps to IAM's user.authorize() method.

IAM Example API


Designing an Access Control System

The following guide breaks down the basic terminology of an access control system (as it pertains to IAM).

Resources The names associated with a system component, such as admin portal, user settings, or an any other part of a system where access should be controlled.
Rights Rights are defined for each resource.
For example, the admin portal resource may have view and manage rights associated with it. Users who are granted view rights should be able to see the admin portal, while users with manage rights can do something in the admin portal. Users without either of these rights shouldn't see the admin portal at all.
Users System users
Roles A collection of permissions for system features, typically based on how the festures are used together.
Groups A collection of users.

To grant/revoke access, developers create roles and assign them to users or groups of users. A role is assigned the rights of specific resources. Users and groups can then be assigned to these roles.

Groups can be assigned users, roles, and even other groups. Groups allow developers to define simple or complex permission hierarchies.

By using each of these major components (resources, rights, roles, users, groups), the permission structure of your applications become significantly easier to manage. In turn, authorizing users becomes a trivial task with the IAM.User object. See examples in the usage section below.


Installation

This is available as an importable ES Module (all runtimes).

A guide and high level API documentation are below. See the source code for additional inline documentation.

Installing for Node.js (8.x.x or higher) as an ES Module

// Browser/Deno Runtime
import IAM, { Resource, Role, Group, User, Right } from 'https://cdn.jsdelivr.net/npm/@author.io/iam/index.min.js'

// Node Runtime
import IAM, { Resource, Role, Group, User, Right } from '@author.io/iam'

npm install @author.io/iam -S

For Node versions prior to 13.x.x, Node must be run with the --experimental-modules flag:

node --experimental-modules index.js

For more information, read the ES Module Support Announcement.

See the api example for a working example.


API Usage

Resources & Rights

Resources can be thought of as components of a system. For example, in a UI, there may be several different pages/tools available to users. Each page/tool could be a unique resource. A basic web application may have a user page, admin section, and a few tools. All of these could be resources. It is up to the developer to identify and organize resources within the system.

Rights can be thought of as actions, permissions, features, etc. Rights often represent what a user can or can't see/do. Like resources, rights are just an arbitrary label, so it can be named any way you want. The naming is less important than understanding there is a relationship between resources and rights (resources have rights).

Creating a single resource

// IAM.createResource('resource', rights)
IAM.createResource('admin portal', ['view', 'manage'])

Creating bulk resources

IAM.createResource({
  'admin portal': ['view', 'manage'],
  'profile page': ['view'],
  'tool': ['view']
})

Modifying resources

There is no specific "modification" feature. Just create the resource again to overwrite any existing resources/rights.

Deleting one or more resources

IAM.removeResource('admin portal', 'tool', ...)

// To remove all:
IAM.removeResource()

Viewing available resources

console.log(IAM.resources)
{
  "admin portal": ["view", "manage"],
  "profile page": ["view"],
  "tool": ["view"]
}

Roles

Roles are used to map system resources/rights to users. A role consists of resources and which rights of the resource should be enforced.

Creating a role

The example below creates a simple administrative role called "admin". This role grants view and manage rights on the admin portal resource.

// IAM.createRole('role name', {
//   'resource': rights
// })

IAM.createRole('admin', {
  'admin portal': ['view', 'manage']
})

Granting all rights

For situations where all rights need to be granted on a specific resource, a shortcut * value can be used.

IAM.createRole('admin', {
  'admin portal': '*'
})

DENY rights

To explicitly deny a right, preface the right with the deny: prefix.

IAM.createRole('basic user', {
  'admin portal': ['deny:view', 'deny:manage']
})

// Alternatively
IAM.createRole('basic user', {
  'admin portal': 'deny:*'
})

FORCIBLY ALLOW rights

There are circumstances where a user may belong to more than one role or group, where one role denies a right and another allows it. For example, users may be denied access to an administration tool by default, but admins should be granted special access to the tool. In this case, the admin rights must override the denied rights. This is accomplished by prefixing a right with the allow: prefix.

IAM.createRole('basic user', {
  'admin portal': 'deny:*'
})

IAM.createRole('superuser', {
  'admin portal': 'allow:*'
})

If a user was assigned to both the basic user and superuser roles, the user would be granted all permissions to the admin portal because the allow:* right of the "superuser" role supercedes the deny:* right of the "basic user" role.

ALLOW RIGHTS ALWAYS SUPERCEDE DENIED RIGHTS. ALWAYS.

Applying rights to everyone

There is a private/hidden role produced by IAM, called everyone. This role is always assigned to all users. It is used to assign permissions which are applicable to every user of the system. A special everyone() method simplifies the process of assigning rights to everyone.

IAM.everyone({
  'resource': 'right',
  'admin portal': 'deny:*',
  'user portal': 'view', // A single string is valid
  'tool': ['view'] // Arrays are also valid.
})

Viewing roles/rights

The full list of roles and rights associated with them is available in the IAM.roles attribute.

console.log(IAM.roles)
{
  "admin portal": ["deny:view", "deny:manage"],
  "user portal": ["view", "manage"],
  "tool": ["view", "manage"]
}

Users

Users can be assigned to roles, granting or denying access to system resources.

Creating a user

let user = new IAM.User()
user.name = 'John Doe'

Please note that user "name" does not necessarily refer to a person's name. It is merely an optional label to help identify a particular user (useful when viewing reports, groups of users, etc).

Assigning users to a role

user.assign('roleA')
user.assign('roleB', 'roleC')

There is also a shortcut to assign roles to a user when the user is created:

let user = IAM.createUser('admin', 'basic user')

In the example above, the user would automatically be assigned to the "admin" and "basic user" roles.

Removing users from a role

user.revoke('admin')
user.clear() // Removes all role assignments.

Determining if a user is assigned to a role

user.of('role')

Determining if a user is authorized to use a resource

if (user.authorized('admin portal', 'manage')) {
  adminView.enable()
}

The code above states "if the user has the manage right on the admin portal, enable the admin view."

Group membership

See the group section below.

Explicit User Permissions

It is possible to explicitly assign a right(s) directly to a user, overriding any group/role assignments the user may be associated with. To do this, use the setRight method.

// Explicitly grant the user view rights on the portal resource.
IAM.currentUser.setRight('portal', 'view')

// Explicitly deny the user view rights on the administrator resource.
IAM.currentUser.setRight('administrator', 'deny:view')

// Do both at the same time, and also deny managing the portal.
IAM.currentUser.setRight({
  portal: 'deny:view',
  asministrator: ['deny:view', 'deny:managerusers']
})

This feature can be very powerful, but should be used sparingly/only as necessary. Explicit rights override all other permissions, but have no flexibility the way roles and groups do. Explicit rights are kind of the "blunt hammer" way of enforcing a specific access control.

Take note: Rights are not accrued by resource. If this method is run more than once for the same resource, the rights for that resource will only reflect the latest update.

Setting a right to null will remove the explicit right.

// Destroy the explicit portal right
IAM.currentUser.setRight('portal', null)

// Destroy all explicit user rights
IAM.currentUser.setRight(null)
IAM.currentUser.setRight()

Group Management

Groups are a simple but powerful organizational container. Groups have two types of members: users and other groups.

Roles are assigned to groups, applying the permissions to all members of the group. For example, a user who is a member of the "admin" group will receive all of the same roles/privileges assigned to the "admin" group.

Users inherit permissions from the groups they are a part of, but groups inherit permissions from the groups within them. For example, a group called "superadmin" contains a group called "admin". The "superadmin" group inherits all privileges from the "admin" group. This is a "reverse" cascade hierarchy, which allows privileges to be "rolled up" into higher order groups.

Creating groups

let group = IAM.createGroup('admin')
// or
let groups = IAM.createGroup('admin', 'profile', '...')

When a single group is created, the new group is returned (i.e. group in the first example). When multiple groups are created at the same time, an array of groups is returned (i.e. groups in the second example)

Group descriptions

Sometimes (especially in reporting) it is useful to have a description of a group. A generic description is generated by default, but it's possible to supply a custom description using the description attribute.

let group = IAM.createGroup('admin')

group.description = 'An administrative group.'

Descriptions are optional.

Assigning roles/privileges to a group

group.assign('roleA', 'roleB')

let roleC = IAM.createRole('roleC', {...})

group.assign(roleC)

It is possible to assign one or more roles at the same time. A role must be the unique name (string) of an existing role or the actual IAM.Role object.

Removing roles/privileges from a group

group.revoke('roleA', roleC)

Similar to adding a role, supply one or more existing role name/IAM.Role objects to the revoke method.

Removing all roles from a group

group.clearRoles() clears all role assignments.

Adding & removing users to a group

let user = new IAM.User()

group.add(user)
group.remove(user)

// or
user.join(group)
user.leave(group)

Adding & removing subgroups to a group

let group = new IAM.Group('admin')
let supergroup = new IAM.Group('superadmin')

// Groups can be added by IAM.Group object or by name
supergroup.add(group)
// or
supergroup.add('admin')

// Groups can be removed by IAM.Group object or by name
supergroup.remove(group)
// or
supergroup.remove('admin')

Tracing Permission Lineage

There are situations when it is useful to know how/why a privilege was assigned to a user. For example, it's not uncommon to ask questions like "why does/n't John Doe have permission to the administration section?". The lineage system is designed to trace a permission back to the authorization source. In other words, it helps identify which group, role, or inheritance pattern ultimately granted/denied access to a resource.

The IAM.Group and IAM.User objects both contain a trace(<resource>, <right>) method for this. The resource needs to be a string/Resource) and the right is a string/Right.

In the following example, a system resource called admin portal exists, but everyone is denied access by default. A role called administrator is created, which grants access to the admin portal resource. Using this structure, only members of the administrator role should have access to the admin portal resource.

// Create a system resource and rights.
IAM.createResource({
  'admin portal': ['view', 'manage']
})

// Deny admin portal rights for everyone.
IAM.everyone({
  'admin portal': 'deny:*'
})

// Create an "administrator" role for users who SHOULD be able to access the admin portal.
IAM.createRole('administrator', {
  'admin portal': 'allow:*'
})

// Create some groups for organizing administrative users.
IAM.createGroup('partialadmin', 'admin', 'superadmin')

// Assign the administrator role to the partialadmin group.
IAM.group('partialadmin').assign('administrator')

// Add the partialadmin group to the admin group,
// and add the admin group to the superadmin group.
// This is the equivalent of saying "the partialadmin
// group belongs to the admin group, and the admin group
// belongs to the superadmin group".
IAM.group('admin').add('partialadmin')
IAM.group('superadmin').add('admin')

// Create a user
let user = new IAM.User()
user.name = 'John Doe' // Optional "nicety" for reporting purposes.

// Add the user to the superadmin group.
user.join('superadmin')

// The user should have access to view the admin portal.
console.log(user.authorized('admin portal', 'view')) // Outputs "true"

Perhaps the user "John Doe" shouldn't have access to the admin portal. Instead of being frustrated and wondering why that user has access when he shouldn't, use the trace method to quickly find out how permission was granted.

console.log(user.trace('admin portal', 'view'))

The console output would look like:

{
  "display": "superadmin (group) <-- admin (subgroup) <-- partialadmin (subgroup) <-- administrator (role) <-- * (right to view)",
  "description": ""The \"view\" right on the \"admin portal\" resource is granted by the \"admin\" role, which is assigned to the \"subadmin\" group, which is a member of the \"admin\" group, which the user is a member of.\"",
  "governedBy": {
    "group": Group {#oid: Symbol(superadmin group),},
    "right": Right {#oid: Symbol(allow:* right),},
    "role": Role {#oid: Symbol(admin role),}
  },
  "granted": true,
  "resource": Resource {#oid: Symbol(admin portal resource),},
  "right": "view",
  "stack": (5) [Group, Group, Group, Role, Right],
  "type": "role"
}

The display and description attributes are the most descriptive.

In this case, the user is just part of a group that he probably shouldn't be a member of... so fixing it is a matter of removing the user from the group. The important part of this trace feature is you didn't have to hunt through an entire application code base to find out which group.

Here is the actual output from the basic example:

IAM Example Lineage

The lineage/trace tool also supports explicitly denied rights (i.e. deny:xxx). It will return null if there is no lineage.

Lineage is parsed into several additional attributes, purely for ease of use.

The stack attribute provides references to the full lineage, including all elements that were responsible for assigning/revoking the specified privileges. This is the same as the display attribute, but provides a programmatic trace instead of a descriptive trace.

The governedBy attribute provides the highest level group, role, and right. The granted attribute determines whether the right was allowed or not. The resource attribute is a reference to the system resource, and the right attribute is the actual right.

The type attribute indicates whether a role the permission was granted/revoked by a "role" or "group" assigned to the user.