node_acl
node_acl copied to clipboard
Design patterns and examples
Can you suggest any good examples of sites, apps, etc. using this module? I'm still having a bit of trouble wrapping my head around some of the concepts of ACL, which is preventing me from fully integrating this module.
Specifically, I'm stuck on the following:
- How to limit access when listing content. For example, user
matt
hasview, list
permission froarticles #1 - 100
but notarticles 101-200
. When matt queriesGET api/articles
how do I limit his access to articles 1-100? Would I access ACL to get his available resources, and set something likefind().where('_id').in(mattsArticles)
. Would I even use ACL for this? - How to grant "guest" permissions, where there is no user or userId available for ACL to check against.
- Where to do the updating or roles: is it better to do this in the controllers, or in the model schema, in
pre.('save')
middleware?
Thanks in advance for helping me understand this better!
:+1: I'd be curious how to use this for a basic blog type site. How would you allow users to see their own drafts, but not other user's?
curious if anyone found some good examples of using this module?
First of it is important to note that is is just a library that you build your application around. When designing an application where resource access will be controlled using ACL, you will first want to organize your resources in a logical manner. My application has clear separations of view logic, domain logic, and model logic where all routes into the domain logic are RESTful
For example, Consider a model Books The routes for this model are
- /books
- /books/:bookId
- /books/:bookId/pages
- /books/:bookId/pages/:pageId These are your resources
Next, I begin to thing about the actions that will be executed on these routes. My app is relatively simple, so these actions will be the standard HTTP CRUD actions
- get
- post
- put
- delete These are your permissions
The next part is your role definitions. I like to keep my role definitions simple at first and then build on them later.
- admin - usually has unlimited access to controlled resources
- user - has limited access to controlled resources
- public - has very limited access to controlled resources
- disabled - has no access to controlled resources
The final part is actual user definitions. This part is agnostic to how you are managing user accounts, but the idea is that a user definition is mapped to a role definition. When it comes time to actually check permissions, you will be checking if a user has permission on a particular resource.
Now we map our resources to our permissions and roles.
To bootstrap my access control table, I organize this information into two data structures.
The first data structure will map roles -> resources -> permissions The second data structure will map users -> roles
Here is what my first structure looks like:
var publicRole = {
name: 'public',
resources: [
],
permissions: []
};
var adminRole = {
name: 'admin',
resources: [
'/books',
'/books/:param1',
'/books/:param1/pages',
'/books/:param1/pages/:pageId'
],
permissions: '*'
};
var userRole = {
name: 'user',
resources: [
'/books',
],
permissions: ['get', 'post']
};
var allRoles = [
publicRole,
adminRole,
userRole
];
And the second data structure for the user definitions
var users = [
{
username: 'public',
roles: ['public'],
password: 'public'
},
{
username: 'admin',
roles: ['admin'],
password: 'admin_password'
},
{
username: 'foobar',
roles: ['user'],
password: 'barfoo'
}
];
I have defined the roles I mentioned earlier, associated them with the relevant resources and permissions, and defined users and associated them with their roles.
I admit, I am skipping a few steps, I apologize if this is still a little confusing.
If you look at the resource definitions, you will see :paramx, these are placeholder that I have put in. They are not defined by node_acl. Node acl only does string matching, so in order to check paths with parameters, I needed to find a way to generalize paramaterized paths. I will come back to this.
After you have defined your ACL and User lists, you will need to add them to the ACL table. I will not go into the code, but the basic algorithm is.
for each role in allRoles
for each resource in role.resources
node_acl.allow(role.name, resource, role.permissions)
for each user in users
create a new User(user.username, user.password) as new user
on new user created
node_acl.addUserRoles(new user.id, user.roles)
This is sufficient for this basic example. A more advanced example would allow you to have resource specific permissions.
Now that your ACL data has been persisted, we will define the access control logic.
The idea is regardless of your webserver, you want to intercept every request on a controlled route and check if the current user is permitted to perform the request. For our purposes a request can be defined as a route and an action to be performed on that route.
Because that is too abstract, I will assume we are using Express.
Our basic express route configuration would be
app.get('/books', booksCtrl.listBooks)
app.post('/books', booksCtrl.createBook)
app.get('/books/:bookId', booksCtrl.getBook)
app.put('/books/:bookId', booksCtrl.editBook)
app.delete('/books/:bookId', booksCtrl.deleteBook)
app.get('/books/:bookId/pages', booksCtrl.listPages)
app.post('/books/:bookId/pages', booksCtrl.addPage)
app.get('/books/:bookId/pages/:pageId', booksCtrl.getPage)
app.put('/books/:bookId/pages/:pageId', booksCtrl.editPage)
app.delete('/books/:bookId/pages/:pageId', booksCtrl.deletePage)
To integrate node_acl into this, we could go very simple and use the middleware method.
app.get('/books/:bookId', node_acl.middleware(), booksCtrl.getBook)
app.put('/books/:bookId', node_acl.middleware(), booksCtrl.editBook)
app.delete('/books/:bookId', node_acl.middleware(), booksCtrl.deleteBook)
app.get('/books/:bookId/pages', node_acl.middleware(), booksCtrl.listPages)
app.post('/books/:bookId/pages', node_acl.middleware(), booksCtrl.addPage)
app.get('/books/:bookId/pages/:pageId', node_acl.middleware(), booksCtrl.getPage)
app.put('/books/:bookId/pages/:pageId', node_acl.middleware(), booksCtrl.editPage)
app.delete('/books/:bookId/pages/:pageId', node_acl.middleware(), booksCtrl.deletePage)
By default, this will perform the following on every request
func (reqest, response, next) ->
node_acl.isAllowed(request.userId, url, httpMethod, func(error, isAllowed) ->
if (isAllowed) ->
next()
else ->
response.notAllowed()
)
I had two problems with using the middleware function at all
- My application didn't set the request.userId property, instead it uses req.user.id
- Many of my paramaterized paths were not explicitly defined for users
So I wrote my own middleware function, it looks a little like this
function myMiddleware(req, res, next) ->
if (req.user is undefined) ->
req.user = { id: 'public' }
id = req.user.id
// here i need to normalize the route in order to ignore routes with parameters
routeParts = req.path.split('/')
for each part in routeParths
if (part matches an id format) ->
replace it with (':param' + counter)
routeParts.join('/')
// this will convert a route /books/abc123 to /books/:param1
// now actually do the acl check
node_acl.isAllowed(id, routeParts, request.method, func(err, isAllowed) ->
if(isAllowed) ->
next();
else
response.notAllowed
I hope this is helpful.
I wanted to submit my advice fairly quickly and I couldn't share my exact working code examples, sorry if I wasn't very clear.
That was a great introductory tutorial. Do you mind if I put it on the wiki? Something to consider btw, in the middleware shipped with node_acl you can define userId as a function that takes (req, res) as input parameters and returns the userId.
Sure, go for it. The example needs some cleaning up though, it was written pretty hastily.
cheers @icompuiz that was really helpful. I appreciate the in depth response and the time taken for it. I've used the ZF2 acl module before so I am fairly familiar with the approach taken here but your response has clarified a few implementation points for me, so thanks.
I think the one thing I will still need to play around with (hopefully today when I get a moment) is implementing this with something like mongoose and persisting the users roles in their own schema similar to the ZF2 implementation.
Wow @icompuiz this is really detailed and informative! Thanks so much! I must say It's been some time since I worked on the project where this was implemented - so I forget what we wound up doing. Anyhow I'll keep this approach in mind for future projects. Thanks again!
So I was trying to implement this today and tried several approaches but I seem to just be missing it. If I had a very simple example project with routes for books and users with a few mongoose user accounts with a roles attribute how might I go about implementing this?
Check out my additions. https://github.com/icompuiz/express-mongoose-acl/compare/chasevida:master...patch-1?quick_pull=1
To summarize the additions
- Added permissions definitions
- Added a barebones Book model
- Added Book and User schema .post middleware
- The Authorization check
- Some skeleton controllers.
Note, I don't think it will run, but the added sections speak to what you will need. My additions were made based on the assumptions made in the example above.
Also, my example has two dependencies: node-async and lodash/underscore.js. As @manast mentioned earlier, you may be able to handle the asynchronousness (not a word) a little better by using the promises some of the functions return.
@icompuiz WOW! ok, seriously I thought it might just be one or two additional lines of implementation I was missing. This is a little more full on than I first appreciated. Really appreciate the time you took to expand and flesh this out. I will go through this thoroughly and get my head around it all now. Again, a huge thanks for the time you've taken to help explain this all.
@icompuiz the link you provided shows "nothing to compare" :(
Whoops, sorry. Here is a link to the commit https://github.com/icompuiz/express-mongoose-acl/commit/e27ef6c2bd61623f44849fd676d38bb289bc07eb
I created some example for using node_acl with mongo and expressjs: https://gist.github.com/danwit/11307969
@danwit this is by far the easiest gist for getting up to speed with this modules implementation. thanks!
@chasevida thanks! Good to hear somebody found use in it.
I dug into passportjs (authentication) the last couple of days and tried to combine it with node_acl to get a full auth process working. Maybe this adds to this conversation too: https://gist.github.com/danwit/e0a7c5ad57c9ce5659d2
I am building my first node-acl project, and I am needing to control access on the resource-level. Let's say I have a single blog, 5 authors, 1 admin. The admin should be able to do anything... no problem there. Each author should only be able to see, update, and delete their own blog posts. Here is my methodology (not yet implemented - wanted some advice first and then I will post back here with more details). The final process will be a little more refined than this, but here goes:
-
All authors will be assigned to the high-level "authors" role where they have full CRUD capabilities on blog_post resources. This is a high-level role which allows the ACL middleware to work as described above.
-
Each author will belong to the "authors" role as well as his own private role with his user ID: "user_[id]"
-
Every time a user creates a blog_post, I will "allow" that user full CRUD operations for that resource using a naming convention like "blog_post_[id]":
ACL.allow('user_' + [id], 'blog_post_' + [id], ['create', 'read', 'update', 'delete'], ...)
-
In order to show the user only HIS blog posts, I will need to do something like the following:
function getUserPosts() {
var promise = new Promise();
ACL.whatResources('user_' + req.user_id, function(err, resources) {
if(err) return promise.reject(err);
// filter the post IDs
var postIds = [];
for(var resourceName in resources) {
if( resourceName.indexOf('blog_post_') === 0 ) {
postIds.push( resourceName.split('blog_post_')[1] );
}
}
promise.resolve(null, postIds);
});
return promise;
}
getUserPosts().then(function(ids) {
db.blog_posts.find({_id: {$in: ids}}, function(err, posts) {
// Show the user his posts
});
}, function(err) {
// handle error
});
The getUserPosts
method can be easily rewritten to load ANY resource holding to the naming convention "[name_id]". If this is the way to go, and if others like this methodology, then I will likely implement a new whatResources
method which accepts an optional 2nd parameter for 'prefix'. This way the filtering can be offloaded to the backend driver (much faster).
Just an idea that may improve your example. Instead of creating a user_id role for the owner of the blog post create a blog_post_id role. Then use addUserRoles
to add roles to the user user_id.
Before calling whatResources you can call userRoles
to get the roles of the particular user.
Doing it like this you could in the future allow other users to have the same permissions over a blog post, and it will fell more natural...
@manast - Thanks for the response. I went down that route originally and here's why I switched directions:
-
"blog_post_[id]" - is a resource just by it's very name - so lets treat it like a resource
-
If I create a role for every resource as you suggest, I would have to "allow" a single resource to that role with the same ID ... which is just kinda redundant:
ACL.allow('blog_post_[id]', [id], ...)
-
The only way to give UserA permission to READ and UserB permission to UPDATE is to create multiple roles for each permission: blog_post_id_read, blog_post_id_update. Now the code looks like this... which is really redundant IMO:
ACL.allow('blog_post_[id]_read', [id], ['read'], ...) ACL.allow('blog_post_[id]_update', [id], ['update'], ...)
-
The big teller for me was that I originally started writing code the way you suggest and I began to see the pitfalls. When I rearranged the code to the way I am suggesting, the code got much shorter and easier to read. Using my methodology, the user role is tightly coupled to the user. Now I can document my app like such:
- After a blog post is created, give the user full CRUD permission to that resource:
ACL.allow('user_' + [user_id], 'blog_post_' + [id], ['create', 'read', 'update', 'delete'], ...)
- vs. After a blog post is created, create four roles for the resource, add the resource to each role with the matching permissions, then assign the user to each of those roles:
ACL.allow('blog_post_' + [id] + '_create', [id], 'create', ...);
ACL.allow('blog_post_' + [id] + '_read', [id], 'read', ...);
ACL.allow('blog_post_' + [id] + '_update', [id], 'update', ...);
ACL.allow('blog_post_' + [id] + '_delete', [id], 'delete', ...);
ACL.addUserRoles([user_id], ['blog_post_' + [id] + '_create', 'blog_post_' + [id] + '_read', 'blog_post_' + [id] + '_update', 'blog_post_' + [id] + '_delete'], ...)
Its funny that you, me, and our other developer all had the same initial idea. An argument was made against my methodology about redundancy in the sense that multiple users are going to have the same permissions to the same resource. My counter to that is "such is the nature of entity-level permissions - a lot of users are going to have the same access to many of the same resources. But eventually UserA is only going to have READ permissions where everybody else has full CRUD". Both methods can be used to achieve the same result, but my way actually feels a little more natural (as you put it) once I started writing code. Thoughts?
i used this github.com/chasevida/express-mongoose-acl.git but some error is occued why?
/*
- ACL */
var nodeAcl = new acl(new acl.mongodbBackend(mongoose.connection.db));
app.use( nodeAcl.middleware );
//nodeAcl.allow('guest', ['books'], ['get', 'post']); // throws error //nodeAcl.allow('admin', ['books', 'users'], '*'); // throws error
/*
- Create Server */
in this commended portion included but some error is occuerd
@DesignByOnyx I'm looking to set up a very similar acl, do you have a fuller example of the code you could share? Thanks.
Hi all,
I needed to build in authentication and authorization to an Express app recently and came across this issue. This discussion was very helpful, especially the examples by @icompuiz. As a result of my research and seeing that there is a need for a solid example of usage of the ACL, I created one. It uses Passport for authentication, the ACL for authorization, MongoDB and Mongoose for data, and SendGrid for email verification and password reset. Feedback and assistance in bug and security fixes would be appreciated.
@icompuiz I tried using the reference but the URL normalise middleware doesn't work. Here is a open issue (https://github.com/OptimalBits/node_acl/issues/205) pls help.
Also for some off reason i cannot access req.params.id
when using the acl.middleware()
Update
- Closed https://github.com/OptimalBits/node_acl/issues/205
- Used
req.route.path
for URL with params
@chasevida So how we implement to view (ejs or jade). We have a lot of pages and buttons, tags,...??
@TungXuan sorry, it's been a long time since I've been on this thread (over 2 years) and I have since moved in other directions. I think you may have to check in with others or open a new specific issue to discuss having this working within views.
There are several good examples of node_acl implemented in one file. But I have yet to find one that separates concerns across several files. I don't think it's a good practice to have on big bloated server.js file. Does anyone know where there are some working examples?
in koa, we can use router.match(url, method).pathAndMethod[0].path
to get ture routeParts in myMiddleware function
First of it is important to note that is is just a library that you build your application around. When designing an application where resource access will be controlled using ACL, you will first want to organize your resources in a logical manner. My application has clear separations of view logic, domain logic, and model logic where all routes into the domain logic are RESTful
For example, Consider a model Books The routes for this model are
- /books
- /books/:bookId
- /books/:bookId/pages
- /books/:bookId/pages/:pageId These are your resources
Next, I begin to thing about the actions that will be executed on these routes. My app is relatively simple, so these actions will be the standard HTTP CRUD actions
- get
- post
- put
- delete These are your permissions
The next part is your role definitions. I like to keep my role definitions simple at first and then build on them later.
- admin - usually has unlimited access to controlled resources
- user - has limited access to controlled resources
- public - has very limited access to controlled resources
- disabled - has no access to controlled resources
The final part is actual user definitions. This part is agnostic to how you are managing user accounts, but the idea is that a user definition is mapped to a role definition. When it comes time to actually check permissions, you will be checking if a user has permission on a particular resource.
Now we map our resources to our permissions and roles.
To bootstrap my access control table, I organize this information into two data structures.
The first data structure will map roles -> resources -> permissions The second data structure will map users -> roles
Here is what my first structure looks like:
var publicRole = { name: 'public', resources: [ ], permissions: [] }; var adminRole = { name: 'admin', resources: [ '/books', '/books/:param1', '/books/:param1/pages', '/books/:param1/pages/:pageId' ], permissions: '*' }; var userRole = { name: 'user', resources: [ '/books', ], permissions: ['get', 'post'] }; var allRoles = [ publicRole, adminRole, userRole ];
And the second data structure for the user definitions
var users = [ { username: 'public', roles: ['public'], password: 'public' }, { username: 'admin', roles: ['admin'], password: 'admin_password' }, { username: 'foobar', roles: ['user'], password: 'barfoo' } ];
I have defined the roles I mentioned earlier, associated them with the relevant resources and permissions, and defined users and associated them with their roles.
I admit, I am skipping a few steps, I apologize if this is still a little confusing.
If you look at the resource definitions, you will see :paramx, these are placeholder that I have put in. They are not defined by node_acl. Node acl only does string matching, so in order to check paths with parameters, I needed to find a way to generalize paramaterized paths. I will come back to this.
After you have defined your ACL and User lists, you will need to add them to the ACL table. I will not go into the code, but the basic algorithm is.
for each role in allRoles for each resource in role.resources node_acl.allow(role.name, resource, role.permissions) for each user in users create a new User(user.username, user.password) as new user on new user created node_acl.addUserRoles(new user.id, user.roles)
This is sufficient for this basic example. A more advanced example would allow you to have resource specific permissions.
Now that your ACL data has been persisted, we will define the access control logic.
The idea is regardless of your webserver, you want to intercept every request on a controlled route and check if the current user is permitted to perform the request. For our purposes a request can be defined as a route and an action to be performed on that route.
Because that is too abstract, I will assume we are using Express.
Our basic express route configuration would be
app.get('/books', booksCtrl.listBooks) app.post('/books', booksCtrl.createBook) app.get('/books/:bookId', booksCtrl.getBook) app.put('/books/:bookId', booksCtrl.editBook) app.delete('/books/:bookId', booksCtrl.deleteBook) app.get('/books/:bookId/pages', booksCtrl.listPages) app.post('/books/:bookId/pages', booksCtrl.addPage) app.get('/books/:bookId/pages/:pageId', booksCtrl.getPage) app.put('/books/:bookId/pages/:pageId', booksCtrl.editPage) app.delete('/books/:bookId/pages/:pageId', booksCtrl.deletePage)
To integrate node_acl into this, we could go very simple and use the middleware method.
app.get('/books/:bookId', node_acl.middleware(), booksCtrl.getBook) app.put('/books/:bookId', node_acl.middleware(), booksCtrl.editBook) app.delete('/books/:bookId', node_acl.middleware(), booksCtrl.deleteBook) app.get('/books/:bookId/pages', node_acl.middleware(), booksCtrl.listPages) app.post('/books/:bookId/pages', node_acl.middleware(), booksCtrl.addPage) app.get('/books/:bookId/pages/:pageId', node_acl.middleware(), booksCtrl.getPage) app.put('/books/:bookId/pages/:pageId', node_acl.middleware(), booksCtrl.editPage) app.delete('/books/:bookId/pages/:pageId', node_acl.middleware(), booksCtrl.deletePage)
By default, this will perform the following on every request
func (reqest, response, next) -> node_acl.isAllowed(request.userId, url, httpMethod, func(error, isAllowed) -> if (isAllowed) -> next() else -> response.notAllowed() )
I had two problems with using the middleware function at all
- My application didn't set the request.userId property, instead it uses req.user.id
- Many of my paramaterized paths were not explicitly defined for users
So I wrote my own middleware function, it looks a little like this
function myMiddleware(req, res, next) -> if (req.user is undefined) -> req.user = { id: 'public' } id = req.user.id // here i need to normalize the route in order to ignore routes with parameters routeParts = req.path.split('/') for each part in routeParths if (part matches an id format) -> replace it with (':param' + counter) routeParts.join('/') // this will convert a route /books/abc123 to /books/:param1 // now actually do the acl check node_acl.isAllowed(id, routeParts, request.method, func(err, isAllowed) -> if(isAllowed) -> next(); else response.notAllowed
I hope this is helpful.
I wanted to submit my advice fairly quickly and I couldn't share my exact working code examples, sorry if I wasn't very clear.