Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design patterns and examples #38

Open
facultymatt opened this issue Sep 4, 2013 · 27 comments
Open

Design patterns and examples #38

facultymatt opened this issue Sep 4, 2013 · 27 comments

Comments

@facultymatt
Copy link

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 has view, list permission fro articles #1 - 100 but not articles 101-200. When matt queries GET api/articles how do I limit his access to articles 1-100? Would I access ACL to get his available resources, and set something like find().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!

@pwmckenna
Copy link

👍 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
Copy link

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

@icompuiz
Copy link

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.

@manast
Copy link
Member

manast commented Feb 27, 2014

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
Copy link

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

@chasevida
Copy link

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
Copy link
Author

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
Copy link

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
Copy link

icompuiz commented Mar 1, 2014

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.

@chasevida
Copy link

@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
Copy link
Author

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

@icompuiz
Copy link

icompuiz commented Mar 1, 2014

Whoops, sorry. Here is a link to the commit icompuiz/express-mongoose-acl@e27ef6c

@danwit
Copy link

danwit commented Apr 26, 2014

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

@chasevida
Copy link

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

@danwit
Copy link

danwit commented May 1, 2014

@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
Copy link

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 "blog_post_[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
Copy link
Member

manast commented Jun 3, 2014

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
Copy link

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

  1. "blog_post_[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'], ...)
  • 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?

@jithinag
Copy link

jithinag commented Aug 8, 2014

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

@jithinag
Copy link

jithinag commented Aug 8, 2014

/*


  • 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

@swordsreversed
Copy link

@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
Copy link

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
Copy link

cookie-ag commented Sep 5, 2016

@icompuiz I tried using the reference but the URL normalise middleware doesn't work. Here is a open issue (#205) pls help.

Also for some off reason i cannot access req.params.id when using the acl.middleware()

Update

@TungXuan
Copy link

TungXuan commented Sep 13, 2016

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

@chasevida
Copy link

@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
Copy link

pak11273 commented Aug 8, 2018

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
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests