Simple in-memory ACL for node.js apps. Supports arbitrary roles and resources, including role/resource detection using a simple interface. Additionally supports custom assertions for more complex rules.
Most ACLs for node.js were too complex for my taste, or required strict conventions that I didn't necessarily want/need to follow. Virgen-ACL is an attempt to provide a low-level ACL that can be used the way works best for you. It is based loosely on Zend_Acl from Zend Framework 1.x, which is one of the most flexible ACLs I've ever worked with.
All ACL rules are stored in memory, making Virgen-ACL extremely fast. Unless specified with custom assertions, there are no DB lookups when querying the ACL, allowing your app to respond as quickly as possible to ACL-gated content.
When querying the ACL, the rules are processed in LIFO order. The first rule to produce a non-empty result (i.e. not null) becomes the permission for the given role/resource/action tuple. This allows you to provide general rules first, followed by "overrides" for special cases.
npm install virgen-acl
// Load library
var Acl = require("virgen-acl").Acl
, acl = new Acl();
// Set up roles
acl.addRole("guest"); // guest user, inherits from no one
acl.addRole("member", "guest"); // member inherits permissions from guest
acl.addRole("admin"); // Admin inherits from no one
// Set up resources
acl.addResource("blog"); // blog resource, inherits no resources
// Set up access rules (LIFO)
acl.deny(); // deny all by default
acl.allow("admin"); // allow admin access to everything
acl.allow("member", "blog", "comment"); // allow members to comment on blogs
acl.allow(null, "blog", "view"); // allow everyone to view the blogs
acl.allow("guest", "blog", ["list", "search"]) // supports arrays of actions
// Query the ACL
acl.query("member", "blog", "comment", function(err, allowed) {
if (allowed) {
// commenting allowed!
} else {
// no commenting allowed!
}
});
// supports multiple roles in query
acl.query(["member", "admin"], "blog", "create", function(err, allowed) {
if (allowed) {
// creating allowed!
} else {
// no creating allowed!
}
});
If you are more of an object-oriented programmer and prefer to use objects
to represent your roles and resources, then you're in luck! Virgen-ACL can
discover roles and resources from your objects so long as your role objects
contain the property role_id
OR a function getRoleId()
and your resource
objects contain the property resource_id
OR a function getResourceId()
.
Valid value types for role_ids are string, an array of strings, or null
. Valid
value types for resource_ids are null
or strings.
Here's an example of how that might work:
// User class
var User = (function(){
User = function(attribs) {
this.id = attribs.id || null;
}
User.prototype.getRoleId = function() {
if (this.id) {
return "member"; // members have an ID
} else {
return "guest"; // all other users are guests
}
}
return User;
})();
// Blog class
var Blog = (function(){
Blog = function(attribs) {
this.resource_id = "blog";
this.status = attribs.status || "draft";
};
return Blog;
})();
var userA = new User();
userA.getRoleId(); // returns "guest"
var userB = new User({id: 123});
userB.getRoleId(); // return "member"
var blog = new Blog();
blog.resource_id; // set to "blog"
// Set up ACL
var acl = new Acl();
acl.addRole("guest"); // guest inherits from no one
acl.addRole("member", "guest"); // member inherits from guest
acl.allow("guest", "blog", "view"); // guests allowed to view blog
acl.allow("member", "blog", "comment"); // member allowed to comment on blog
acl.query(userA, blog, "view", function(err, allowed) {
// userA is a guest and can view blogs
assert(allowed == true);
});
acl.query(userA, blog, "comment", function(err, allowed) {
// userA is a guest and cannot comment on blogs
assert(allowed == false);
});
acl.query(userB, blog, "view", function(err, allowed) {
// userB is a member and inherits view permission from guest
assert(allowed == true);
});
acl.query(userB, blog, "comment", function(err, allowed) {
// userB is a member and has permission to comment on blogs
assert(allowed == false);
});
Sometimes you need more complex rules when determining access. Custom assertions can be provided to perform additional logic on each matching ACL query:
acl.allow("member", "blog", "edit", function(err, role, resource, action, result, next) {
// Use next() if unable to determine permission based on provided arguments
if (!(role instanceof User) || !(resource instanceof Blog))
return next();
if (role.id == resource.user_id) {
// resource belongs to this role, allow editing
result(null, true);
} else {
// resource does not belong to this role, do not allow editing
result(null, false);
}
});
var userA = new User({id: 123});
assert(userA.id == 123);
var userB = new User({id: 456});
assert(userB.id == 456);
var blog = new Blog({user_id: 123});
assert(blog.user_id == 123);
// userA can edit this blog because the blog's user ID matches the userA's ID
acl.query(userA, blog, 'edit', function(err, allowed) {
assert(allowed == true);
});
// However userB cannot edit this blog
acl.query(userB, blog, 'edit', function(err, allowed) {
assert(allowed == false);
});