You Shall Pass is a ACL module for Javascript applications
MIT License
You Shall Pass is yet another ACL module for Javascript applications designed to be used on ES6 or Typescript projects.
With its companion modules express-you-shall-pass
, and koa-you-shall-pass
, it aims to be able to replace both passport
and node-acl
on NodeJS APIs, but can also be used client side.
It was written while designing an API to manage a restaurant chain, which is consumed by clients with different and overlapping permissions: administration, digital signage, public smartphone apps, cash registers, ...
Like many others, it allows to check if a user has permissions to access resources and perform actions.
Unlike others:
As this module is security related:
package.json
.With you-shall-pass
, there are no roles and resources, all of your permissions are defined in a single "Permission Graph".
For instance for a blog API, the graph should look like this:
+-----------+
| is_anyone |
+-----+-----+
|
If token |
If author flag is valid | If moderator flag
is set in the database v is set in the database
+-----------+ +--------+---------+ +--------------+
| is_author +<--------------+ is_authenticated +---------->+ is_moderator |
+-----+-----+ +--------+---------+ +-------+------+
| | |
| If author | |
Always | of the | Always |
| article | +-------------------------+
v v v v
+----------+---------+ +--------+---+-----+ +----------+---------+
| can_create_article | | can_edit_article | | can_delete_article |
+--------------------+ +------------------+ +--------------------+
Todo: write documentation
const acl = new Acl(
// Default permission
'is_anyone',
// Permission graph edges
[
{
explain: "User carries a basic authentication token",
from: 'is_anyone',
to: 'is_authenticated',
check: async ctx => {
// Decode basic auth token.
const [username, password] =
Buffer.from(ctx.token.split(' '), 'base64').toString().split(/:/);
// Load user **into context**.
// It will be available for all other check functions.
ctx.user = await getUser(username);
// true if allowed, false otherwise
return ctx.user && bcrypt.verify(password, ctx.user.password);
}
},
{
explain: "User is a moderator",
from: 'is_authenticated',
to: 'is_moderator',
check: async ctx => ctx.user.isModerator
},
{
explain: 'Moderators can edit and delete all articles'
from: 'is_moderator',
to: ['can_edit_article', 'can_delete_article'],
},
{
explain: "Authors can edit their own articles",
from: 'is_authenticated',
to: 'can_edit_article',
check: async ctx => ctx.article.createdBy == ctx.user.email
},
[...]
]
);
Todo: write documentation
// Can we reach the 'can_edit_article' permission **for this article and authentication token**
const result = await acl.check('can_edit_article', {
token: 'Basic ZWxsaW90QGUtY29ycC5jb206YW5hcmNoeV9mdHc=',
article: {id: 1, text: "I ❤ Javascript", createdBy: '[email protected]'}
});
if (result !== null) {
// User is allowed to edit the article (result contains the current user because it was
// loaded during the checks).
assert.isNotNull(result.user);
}
else {
// User does not have the 'can_edit_article' permission in this context.
}
A user can be allowed to perform an action because of many reasons. In the previous example, the permission to edit a given article can be reached either by the article author or by any moderator.
The same happens when a user is denied a permission: if a user is not allowed to edit a particular article, it is both because he is not a moderator, and because he is not the article's author.
By providing all nodes in the permission graph that were checked for a particular query, and the result of each check, the .explain()
method provides insight about why a permission was granted or denied.
// Ask `you-shall-pass` details about last example.
const explanation = await acl.explain('can_edit_article', {
token: 'Basic ZWxsaW90QGUtY29ycC5jb206YW5hcmNoeV9mdHc=',
article: {id: 1, text: "I ❤ anarchy", createdBy: '[email protected]'}
});
// We can check why we were allowed to edit the article in the previous example.
explanation
.filter(e => e.to == 'can_edit_article' && e.checkPassed)
.map(e => e.explain);
> ['Authenticated users can edit their own article']
// We can also check paths that the acl checker tried, but which failed to reach the requested permission.
explanation
.filter(e => !e.checkPassed)
.map(e => e.explain);
> ['User is a moderator']
Todo: write documentation
When dealing with complex permission graphs, if can quickly become inconvenient to handle the list of edges in a single file.
Todo: write documentation