OptimalBits / node_acl

Access control lists for node applications
2.62k stars 369 forks source link

Design patterns and examples #38

Open facultymatt opened 11 years ago

facultymatt commented 11 years ago

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:

Thanks in advance for helping me understand this better!

pwmckenna commented 10 years ago

:+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?

chasevida commented 10 years ago

curious if anyone found some good examples of using this module?

icompuiz commented 10 years ago

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

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

The next part is your role definitions. I like to keep my role definitions simple at first and then build on them later.

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

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.

manast commented 10 years ago

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.

icompuiz commented 10 years ago

Sure, go for it. The example needs some cleaning up though, it was written pretty hastily.

chasevida commented 10 years ago

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.

facultymatt commented 10 years ago

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!

chasevida commented 10 years ago

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?

icompuiz commented 10 years ago

Check out my additions. https://github.com/icompuiz/express-mongoose-acl/compare/chasevida:master...patch-1?quick_pull=1

To summarize the additions

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.

chasevida commented 10 years ago

@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.

facultymatt commented 10 years ago

@icompuiz the link you provided shows "nothing to compare" :(

icompuiz commented 10 years ago

Whoops, sorry. Here is a link to the commit https://github.com/icompuiz/express-mongoose-acl/commit/e27ef6c2bd61623f44849fd676d38bb289bc07eb

danwit commented 10 years ago

I created some example for using node_acl with mongo and expressjs: https://gist.github.com/danwit/11307969

chasevida commented 10 years ago

@danwit this is by far the easiest gist for getting up to speed with this modules implementation. thanks!

danwit commented 10 years ago

@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

DesignByOnyx commented 10 years ago

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:

  1. 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.
  2. Each author will belong to the "authors" role as well as his own private role with his user ID: "user_[id]"
  3. Every time a user creates a blog_post, I will "allow" that user full CRUD operations for that resource using a naming convention like "blogpost[id]":

    ACL.allow('user_' + [id], 'blog_post_' + [id], ['create', 'read', 'update', 'delete'], ...)
  4. 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).

manast commented 10 years ago

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 userRolesto 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...

DesignByOnyx commented 10 years ago

@manast - Thanks for the response. I went down that route originally and here's why I switched directions:

  1. "blogpost[id]" - is a resource just by it's very name - so lets treat it like a resource
  2. 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], ...)
  3. 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'], ...)
  4. 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'], ...)
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?

jithinag commented 10 years ago

i used this github.com/chasevida/express-mongoose-acl.git but some error is occued why?

jithinag commented 10 years ago

/*


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

/*


in this commended portion included but some error is occuerd

swordsreversed commented 8 years ago

@DesignByOnyx I'm looking to set up a very similar acl, do you have a fuller example of the code you could share? Thanks.

ajmueller commented 8 years ago

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.

cookie-ag commented 7 years ago

@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
TungXuan commented 7 years ago

@chasevida So how we implement to view (ejs or jade). We have a lot of pages and buttons, tags,...??

chasevida commented 7 years ago

@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.

pak11273 commented 6 years ago

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?

longhaiyan commented 4 years ago

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.