New plugin acl_system

Posted by ezmobius Mon, 20 Feb 2006 07:20:00 GMT


UPDATE! NEW REPO URL & FEATURES!


Welcome to the acl_system plugin for rails. This plugin is designed to give you a flexible declarative way of protecting your various controller actions using roles. It’s made to site on top of any authentication framework that follows a few conventions. You will need to have a current_user method that returns the currently logged in user. And you will need to make your User or Account model(or whatever you named it) have a has_and_belongs_to_many :roles. So you need a model called Role that has a title attribute. Once these two things are satisfied you can use this plugin.

So lets take a look at the sugar you get from using this plugin. Keep in mind that the !blacklist part isn’t really necessary here. I was just showing it as an example of how flexible the permissions string logic parser is.
class PostController < ApplicationController
  before_filter :login_required, :except => [:list, :index]
  access_control [:new, :create, :update, :edit] => '(admin | user | moderator)',
                 :delete => 'admin & (!moderator & !blacklist)' 
Of course you can define them all seperately if they differ at all.
class PostController < ApplicationController
  before_filter :login_required, :except => [:list, :index]
  access_control :new => '(admin | user | moderator) & !blacklist',
                 :create => 'admin & !blacklist',
                 :edit => '(admin | moderator) & !blacklist',
                 :update => '(admin | moderator) & !blacklist',
                 :delete => 'admin & (!moderator | !blacklist)' 
And you can also use :DEFAULT if you have a lot of actions that need the same permissions.
class PostController < ApplicationController
  before_filter :login_required, :except => [:list, :index]
  access_control :DEFAULT => '!guest' 
                [:new, :create, :update, :edit] => '(admin | user | moderator)',
                 :delete => 'admin & (!moderator & !blacklist)'
There are two callback methods you can use to define your own success and failure behaviors. If you define permission_granted and/or permission_denied as protected methods in your controller you can redirect or render and error page or whatever else you might want to do if access is allowed or denied.
class PostController < ApplicationController
  before_filter :login_required, :except => [:list, :index]
  access_control :DEFAULT => '!guest' 
                [:new, :create, :update, :edit] => '(admin | user | moderator)',
                 :delete => 'admin & (!moderator & !blacklist)'

  # the rest of your controller here

  protected

  def permission_denied
    flash[:notice] = "You don't have privileges to access this action" 
    return redirect_to :action => 'denied'
  end

  def permission_granted
    flash[:notice] = "Welcome to the secure area of foo.com!" 
  end

end
There is also a helper method that can be used in the view or controller. In the view its handy for conditional menus or stuff like that.
<% restrict_to "(admin | moderator) & !blacklist" do %>
  <%= link_to "Admin & Moderator only link", :action =>'foo' %>
<% end %>
So the gist of it is that in the access_control controller macro, you can assign permission logic strings to actions in your controller. You supply a hash of :action => ‘permissions string” pairs. Any action not in the list is left open to any user. Any action with a logic string gets evaluated on each request to see if the current user has the right role to access the action. The plugin has a small recursive descent parser that evaluates the permission logic strings against the current_user.roles.

The way this works is that you have your User model and a Role model. User <= habtm => Role. So when an action that is access_control’ed gets requested the permission logic string gets evaluated against the current_user.roles . So a prerequisite of using this plugin is that you add a Role model with a title attribute that has_and_belongs_to_many User models. And you need to have a current_user method defined somewhere in your controllers or user system. Luckily the acts_as_authenticated plugin has the current_user defined already.

So here is the schema of this application including the Post model and the User and Role model plus the habtm join table:

ActiveRecord::Schema.define(:version => 3) do
  create_table "posts", :force => true do |t|
    t.column "title", :string, :limit => 40
    t.column "body", :text
  end
  create_table "roles", :force => true do |t|
    t.column "title", :string
  end
  create_table "roles_users", :id => false, :force => true do |t|
    t.column "role_id", :integer
    t.column "user_id", :integer
  end
  create_table "users", :force => true do |t|
    t.column "login", :string, :limit => 40
    t.column "email", :string, :limit => 100
    t.column "crypted_password", :string, :limit => 40 
    t.column "salt", :string, :limit => 40
    t.column "created_at", :datetime
    t.column "updated_at", :datetime
  end
end

And so thats pretty much it for now. You add the roles to the Role.title attribute like admin, moderator and blacklist like above. These can be anything you want them to be, roles, groups or whatever. Then you can use as many nested parens and logic with & | ! as you want to define your complex permissions for accessing your controller. Make sure that your access_control gets called after the login_required before_filter because we assume that you are already logged in if you made it this far and then we eval the permissions logic.

You will want to define these access_control in each controller that needs specific permissions. unless you want to protect the same actions in all controllers, then you can put it in application.rb but I dont recommend it.

Stay tuned as I will be adding another macro method to use in your models to define precisions permissions for accessing your models soon. All suggestions or critiques are welcome.

You can get it here from svn. Keep in mind this is alpha stuff and subject to change so check back for updates and please report any bugs.

http://opensvn.csie.org/ezra/rails/plugins/dev/acl_system2/

Tags , , ,  | 71 comments

Comments

  1. Josh H said about 9 hours later:
    Great idea Ezra! I like how you took the complex problem of authorization and broke it up into a useful plugin. When I start implementing logins on my sites, I should be able to just use this on top of my own user models.
  2. Xian said about 9 hours later:
    I like a lot of things about this, but there seems to be a lot of repetition in declaring the permissions, particularly if you want to be adding the same permissions to a number of actions. Maybe something could be done to gang actions together like this: [:edit, :update] => '(admin | moderator) & !blacklist' And it would also be nice (but not necessary) to be able to declare some default access rules for the entire controller and then only override the few that need changing. This would be particularly useful for admin specific controllers. I haven't had a chance to play with the code yet, this may be possible by defining my own filter methods that use access_control.
  3. Jeremy Hubert said about 12 hours later:
    Very nice. I think I'll be using this in my current project. I also like Xian suggestions about gang actions. Would clean the code up a bit more. :) Keep it up Ez! You rock!
  4. Ezra Zygmuntowicz said about 12 hours later:
    Xian- I totally agree with you. That will be coming very soon. That is the main thing that I want to do is make things a bit more DRY. I just wanted to release early, release often so I could get some feedback to make this even better.
    I also am trying to come up with the best way to use this to protect models records from access by users that arent allowed access. Still haven't come up with the best way to do that yet. Probably something similar to access_control but for your models, where it can do a run time check to make sure the user has access rights before letting a find return results.
    I will be working on this plugin more today and this week since I need it for a real project. Please leave any other suggestions you might have here as I am interested in making this as nice as it can be.
    I am especially interested in any ideas about how to protect model records with a declarative syntax like I have here ;)
  5. Phil said 1 day later:
    Wow, I was just about to implement this on my own today. Thanks, Ezra!
  6. izo said 4 days later:
    Hey, I keep my fingers crossed for acl_for_model plugin i'm facing same problem
  7. Danny said 5 days later:
    This is very useful, thanks! Couldn't you somehow make the blacklist stuff default behaviour? I mean, normally, you never want anyone blacklisted to have access.
  8. Ezra Zygmuntowicz said 6 days later:
    Danny- I should say this up in the article but the blacklist deal is only there for an example and doesn't actually need to be there :) It was just there as an example of the flexible syntax of the logicstrings.
  9. Danny Lagrouw said 6 days later:
    I see... But then, wouldn't a blacklist be a nice standard feature to add? So if you do want a blacklist, you won't have to add it to all the access_control definitions? Never mind though, it's fine as it is ;-)
  10. Sam said 20 days later:
    It seems a bit complex to me. Like reinventing language syntax with your own parser. For example: @<% if permit?("(admin | moderator) & !blacklist", current_user) %>@ Could also be: @<% if authorized?('admin', 'moderator') && !current_user.blacklist? %>@ The action permissions could be handled with blocks I suppose. Just my preference. Still a nice/clean project. :)
  11. Jeff Cole said 36 days later:
    It would be great if the access rules could work on patterns in the role titles. For example, access_control :DEFAULT => 'admin_*' would work on both roles "admin_users" and "admin_forum".
  12. Kaveh Ahmadian said 47 days later:
    Jeff: Have you seen the the authorization plugin that Bill Katz has proposed? I think it easily allows you to have an admin role relative to your different models. The only drawback seems to be that you need seperate tables for each role (i.e. admin, moderator, etc.) Anyway, check it out at http://www.billkatz.com/authorization Ezra: Any ideas on how one might incorporate this with the ModelSecurity authorization framework. I haven't really started looking into it yet, but I wonder if one can wrap access_control directives in such a way that the check can return a boolean value to the :if parameter of Bruce's let_* methods. I could be way off here...
  13. Ezra Zygmuntowicz said 49 days later:
    Folks- I haven't had any free time to work on this recently but I do have plans to add features to this plugin so that you can secure your models via roles as well. I have just moved and started a new job so I will be setled soon and will release an updated version of the plugin soon.
  14. Jeremy Apthorp said 68 days later:
    I find that access_control fails to come into effect unless I comment out this line in lib/caboose/access_control.rb (line 24): c.default_access_context = defaults With that line still in place, acl_system2 seems to completely ignore me when I say access_control [:new, :edit, :destroy] => 'admin': i.e, it allows all logged in users to access those actions. If I change it to access_control [:new, :edit, :destroy] => '!admin', it refuses to let anyone access those actions.

    With that line from the source commented, however, all my tests pass. :)

    I'm sure this isn't quite the right way to go about fixing this issue, so could someone with better knowledge of the codebase shed some light on this? :)

  15. Ezra Zygmuntowicz said 69 days later:
    Jeremy - I wasn't able to reproduce the bug you are getting. However, I did updae the code so it checks for the default block given before it calls the method that was causing your errors. Anyway would you please svn up and see if I fixed this bug for you? Thanks
  16. Jeremy Apthorp said 69 days later:
    This change seems to have fixed the bug. Thanks!

    An excellent plugin.

  17. Marshall Roch said 70 days later:

    The ability to write your own handler is a really powerful undocumented feature.

    In my application, users have many roles which have many permissions, and access to certain tasks are based on permissions, not roles. All I had to do to make this plugin work for permissions instead was write a PermissionHandler class and add a retrieve_access_handler method in my ApplicationController to return PermissionHandler.new.

    Ezra, if you document how to do this in the post, you can remove the Roles requirement, which will make this plugin much more flexible and therefore more appealing.

  18. Ezra Zygmuntowicz said 70 days later:
    Cool Marshall ;) I'm glad somebody noticed that we put that in. I need to get better about documentation. I have just been so busy lately. If you make a small writeup of what you did I will include it in the README and post it here if you like? Otherwise, it will have to wait until I get some more time. I also have model protection code that is almost ready to use. It is easy to extend this plugin though as Marshall shows. I woudl love it if you sent me the PermissionHandler class you wrote and a small writeup. I will inclujde it in the plugin as an example.
  19. Matt said 108 days later:
    I like your system, but I had a question about the functionality. I wanted to restrict access to editing certain objects to only their creators. Each object references the creator, I was just curious if you see an easy way to do that.
  20. matt said 108 days later:
    nm i already do. heh.
  21. John said 115 days later:
    It seems as if the title attributes must be lowercase. When I define an uppercase 'Admin', even if I call access_control :new => 'Admin', I get the denied render text. Any ideas/comments? Otherwise, great plugin!
  22. Ezra said 115 days later:
    John-

    Thanks for finding that. I just fixed it and commited to the svn repo. Go ahead and grab the latest version anf it should fix your problem.
  23. Ian said 115 days later:
    Ezra, Do you have any plans to release the model access controls that you mentioned in post #18?
  24. Mike S. said 117 days later:
    Ezra, great plugin. I do have a question however-- is there any provision to accomodate "can't edit users except if its yourself"? For example "account_edit" should only be an administrative feature unless its a member editing their own profile...?
  25. Daniel said 126 days later:
    This is great! I encountered one aspect that might be a bug. If you call access_control in application.rb, you can't override it in your controllers. Thanks!
  26. Jeff Cole said 136 days later:
    I have a weird problem where access to a page is cached based on the first user who hits that page. If the first user has access, everyone will have access. If the first user does not have access, no one will have access. This only happens in my production environment, not my develoment environment. Has anyone observed this or does anyone have any suggestions? Thanks!
  27. Ezra Zygmuntowicz said 138 days later:
    Jeff- I haven't seen that one before ever. I have this plugin unmodified running in production on a number of sites and it behaves as expecte. Anything lse that could be causing it? Let me know if you can't get it to work and I will try to help.
  28. Jeff Cole said 138 days later:
    Thanks for the follow up. I was able to repeat it consistently, perhaps it is my version of Rails (1.0).
    It only worked after I changed this line in AccessControl.default_access_context:


    # JC changed this line:
    # @default_access_context[:user] ||= send(:current_user) if respond_to?(:current_user)
    # to this 1 line:
    @default_access_context[:user] = send(:current_user) if respond_to?(:current_user)

    All that does is force getting the context every time. You can email me at cole dot jeff at gmail dot com. Thanks...
  29. Ezra Zygmuntowicz said 140 days later:
    Jeff- I updated the plugin and made the change you suggested. Thanks for finding that. I wonder why I have never run into it before? Anyway... its up to date now.
  30. Jeff Cole said 140 days later:
    Sure thing. I don't know why nobody has run into it either. I hope you were able to replicate it and it isn't just an artifact of my setup.
  31. Joern said 144 days later:
    It sure is easily integrated with acts_as_authenticated and the swapping of the access handler was a no brainer, too, but: what is the most sensible way of accessing the permission information's from e.g. the view in order to display conditionally wheater the current user may or may not access the controller / action pair? If I am not mistaken @access is only available after the secured controller passed the before_filter chain, so I cannot use it's allow? function to e.g. regulate access to a central navigation menu. Do you have a better idea than patching access_control.rb to write controller / logic pairs and accessing these later using permit?() ?
  32. Ezra said 145 days later:
    Joern-

    I'm not sure if I get exactly what you want to do. you mean show or don't show certain links in a view? If so you can use the restrict_to helper in the view like this:

    
    restrict_to "admin | moderator" do 
       # restricted links go in here. and will only be
      # shown if the current_user passes the requirements
      # of the permission string.
    end
    
    

    For some reason my erb tags are not showing up in this comment. So imagine the resrict_to and end lines wrapped in erb tags. Does that get you where you want to be?
  33. Joern said 147 days later:
    Hi Ezra - no, I meant something like: if the acl_system restricts access to a certain controller / action tuple, I do not want to display e.g. a link to this action. For this I'd like to ask the acl_system instead of duplicating the rules the system took to make that decision. This seemed not really possible with your current approach, so I took a shortcut by writing all access rules into a module-specific class variable. Now I can check if a link should be displayed by asking permit?() - all I wanted to know is if there is a better way to achieve that.
  34. Evan said 162 days later:
    Hi, This is a nice plugin, but I wonder if it can be extended for a multi-level role list such that a Role model can have child Roles, and when you grant access control to a child, you automatically also grant access control to the parent Role. i.e. parent: Admin child: Manager grandchild: Moderator child: Publisher grandchild: Editor great-grandchild: Poster If you give the Editor role access control to a page, the Publisher and Admin roles automatically also gets access to it, but the rest don't. Thanks for the help.
  35. Roberto said 164 days later:
    Hi, I'm trying to use that with the userstamp plugin and I get following error: protected method `current_user' called for # when trying to access the controller MyFiles I included the libs in the Application Controller. Anyone tried that before? Thanks, Roberto
  36. Evan said 167 days later:
    Hi Roberto. Check where you placed your current_user method. The bottom-most methods are under the protected clause, so if you placed you method there, it's also protected. Just to be sure, move your current_user method near the top of your code.
  37. Bruno said 184 days later:
    Thank you for the plugin, it is installed and working. What is the best way to populate the roles_users table when a new user signs up?
  38. Chris said 195 days later:
    I am having problems grabbing this code via svn. When I pop the url in as a repository location, it says the connection timed out. If I try to do a script/plugin install using that url, ruby throws an exception, and the stack-trace indicates it is timing out. Is the repository down, or am I doing something wrong? I really look forward to integrating this on top of acts_as_authenticated, so any help you can give me here would be fantastic.
  39. Stephen said 204 days later:
    Looks like quite a promising plugin. SVN keeps timing out for me, though, just like for Chris above.
  40. ryan said 212 days later:
    ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:21:in `require__': no such file to load -- install (MissingSourceFile) cant install the plugin from the svn
  41. Lvr said 259 days later:
    Is this project still alive? I would really like to use this system, but I wouldn't want to end up with a system that's not supported anymore.
  42. Ezra said 259 days later:
    Yup this plugin still works great. I am still using it in its current form. I haven't had to make any changes to it because it just works. SO you are safe using it.
  43. Lvr said 263 days later:
    Thanks for the reply. I'll try it as soon as possible. :)
  44. Steve Hydrocodone said 265 days later:
    Hello all.
  45. no fax payday loan said 265 days later:
    no fax payday loan said 211 days later: Personal Cash Advance is the fastest way to obtain secure, online cash advance and payday loans. Applying and qualifying for a payday loan is quick and easy, and in many cases there are no documents to fax. Once you're approved for the cash advance, we'll electronically deposit the payday loan amount directly into your checking or savings account. We offer flexible payment options and discrete service that gets you the cash you need right now. It's that easy, so why wait to get that cash advance? still have a question, mail to us: financeadmin@mailpaydayloan.com If someone still ill please come to our Online Pharmacy hydrocodone
  46. no fax payday loan said 265 days later:
    no fax payday loan said 211 days later: Personal Cash Advance is the fastest way to obtain secure, online cash advance and payday loans. Applying and qualifying for a payday loan is quick and easy, and in many cases there are no documents to fax. Once you're approved for the cash advance, we'll electronically deposit the payday loan amount directly into your checking or savings account. We offer flexible payment options and discrete service that gets you the cash you need right now. It's that easy, so why wait to get that cash advance? still have a question, mail to us: financeadmin@mailpaydayloan.com If someone still ill please come to our Online Pharmacy hydrocodone
  47. Lvr said 267 days later:
    Hi, I'm using the plugin now and it's nice. I have a feature request though. I'm in this situation: PostController has index and list actions, they don't need authentication nor authorization. Every other action needs authentication with admin authorization, so I have a "before_filter :login_required, :except => ['index', 'list']" and for authorization I have "access_control :DEFAULT => 'admin', [:index, :list] => 'true'" Too bad this isn't very DRY. I define two times that I don't want any auth. for index and list. At first I couldn't find a way to work with :default AND an exeption (index and list) => they need NO ROLES. So I tried 'true' as a role and it worked. Maybe this is not intended but it works for me. Could there be a more RoR-like way to do it? access_control :DEFAULT => 'something', :no_auth => [array_of_pages_with_NO_NEED_FOR_AUTHORIZATION] or even auto-detect :no_auth by checking the before_filter :login_required, :except => 'bla' line.
  48. Katalog said 271 days later:
    I also am trying to come up with the best way to use this to protect models records from access by users that arent allowed access. Still haven't come up with the best way to do that yet. Probably something similar to access_control but for your models, where it can do a run time check to make sure the user has access rights before letting a find return results. I will be working on this plugin more today and this week since I need it for a real project. Please leave any other suggestions you might have here as I am interested in making this as nice as it can be.
  49. ken said 330 days later:
    am quite new to ROR. I have liked this plugin so much but have no clue on how to install it and use it. please help :-(
  50. Octoberdan said 330 days later:
    Wouldn't this plugin be RBAC and not ACL?
  51. RailsRocks said 332 days later:
    Really stupid Question. How/where do I install this plugin?..I checked it out using svn.
  52. RailsRocks said 332 days later:
    Never mind...I figured it out :)
  53. mcm8 said 338 days later:

    Lvr wrote in post 47 about the DRY problems with the :DEFAULT ACLs in combination with acts_as_authenticated. The workaround with "=> 'true'" doesn't work in my case.

    Here's my situation: Since most of my actions need authentication and authorization I defined a login_required before_filter as well as a :DEFAULT access_control rule for all controllers/actions in application.rb. For public actions without auth I simply put a "skip_before_filter... :only =>..." into the according controller. Problem: The acl_system throws an exception when there's no current_user available. Defining a "=> 'true'" ACL for the public actions doesn't help because the :DEFAULT ACL from the base controller is still executed (the new ACL ist just added). Despite that, this wouldn't be very DRY.

    My solution: Instead of using the "=> 'true'" workaround I added a few statements which prevent the acl_system from checking the current_user when there's no login_required before_filter. For this to work I added the line "@login_was_required = true" to the top of the login_required filter method in authenticated_system.rb and the line "return true unless @subject.instance_eval { @login_was_required }" at the top of the allowed?(action) method in access_control.rb. That did it for me - maybe it helps somebody.

    @ezmobius: I think that's the way it should work generally. There's IMHO no point in checking the ACLs if there's no current_user/login_required. Would make the :DEFAULT feature much more usable. Thanks for the great work!

    PS: A better way than the @login_was_required hack might be to check the before_filter-chain for the login_required before_filter

  54. Bruno said 372 days later:
    Hi, I am using the acl_system2 plugin and it seems to be working very well for me. When I try to run the test(rake), I get the following error... 1) Error: test_index(Dashboard::AvailableProgramControllerTest): NoMethodError: undefined method `roles' for :false:Symbol I am not sure how to fix this, has anyone seen this error before? Thank you
  55. Bernie said 389 days later:
    Nice fix mcm8 in post 55, this was a feature I really needed to DRY up things.
  56. tom said 395 days later:
    before_filter :current_context before_filter :login_required access_control :DEFAULT => 'admin|' + context.role + ')' def current_context context=...retrieve context ... end I'm not able to make the roles dynamic. What I am missing Thanks
  57. Ezra said 395 days later:
    Tom it looks like you are adding a closing paren without an opening paren.
  58. tom said 395 days later:
    This was only my typo in this post The error I see is: ``The error occurred while evaluating nil.role '' It looks like the evaluation of before_filter doesn't take affect before access_control I don't know how to go around this
  59. campground joshua tree said 408 days later:
    am quite new to ROR. I have liked this plugin so much but have no clue on how to install it and use it. please help :-(
  60. joshua tree plant said 408 days later:
    I have a weird problem where access to a page is cached based on the first user who hits that page. If the first user has access, everyone will have access. If the first user does not have access, no one will have access. This only happens in my production environment, not my develoment environment. Has anyone observed this or does anyone have any suggestions? Thanks!
  61. joshua tree said 408 days later:
    Yup this plugin still works great. I am still using it in its current form. I haven't had to make any changes to it because it just works. SO you are safe using it.
  62. book guest joshua tree said 408 days later:
    I like your system, but I had a question about the functionality. I wanted to restrict access to editing certain objects to only their creators. Each object references the creator, I was just curious if you see an easy way to do that.
  63. Walter McGinnis said 409 days later:
    joshua tree plant:

    It sounds like caching is working as it is supposed to, but you haven't used it correctly for your context.

    Caching only goes into effect in the production environment by default, so that is probably why you only see the problem on production.

    I would guess you have also used page or action caching when you should have used fragment caching (or none at all).

    Check out these tutorials:

    http://www.railsenvy.com/2007/2/28/rails-caching-tutorial

    http://www.railsenvy.com/2007/3/20/ruby-on-rails-caching-tutorial-part-2

    Cheers,
    Walter McGinnis

  64. Jim said 415 days later:
    How do i install this plugin, it doesn't seem to work, have I got the right name? I have done: script/plugin source http://opensvn.csie.org/ezra/rails/plugins/dev/acl_system2/ But when I do this script/plugin install acl_system I get this error: Plugin not found: ["acl_system"]
  65. Jim said 415 days later:
    Did it, with: script/plugin install http://opensvn.csie.org/ezra/rails/plugins/dev/acl_system2/
  66. Jim said 415 days later:
    undefined method `restrict_to' for #<#:0x31ca630> from app/views/layouts/admin01.rhtml Where I'm using your: <% restrict_to "(admin | moderator) & !blacklist" do %> <%= link_to "Admin & Moderator only link", :action =>'foo' %> <% end %> Do i have to put the method restrict_to from (plugins/lib/caboose/access_control.rb) into a helper?
  67. craiglist said 423 days later:
    I like your system, but I had a question about the functionality. I wanted to restrict access to editing certain objects to only their creators. Each object references the creator, I was just curious if you see an easy way to do that...
  68. Strafverteidiger Freiburg said 430 days later:
    Thanks for the reply. I'll try it as soon as possible. :)
  69. filmiki said 439 days later:
    Very helpful article, thank you!
  70. kody do gier said 440 days later:
    Your home page is very friendly and rich with information! Thanks!
  71. we live togehte said 441 days later:
    Predicted values in template editing can be done through any PHP script?

(leave url/email »)

   Preview comment