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





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.
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.
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!
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 ;)
Wow, I was just about to implement this on my own today. Thanks, Ezra!
Hey, I keep my fingers crossed for acl_for_model plugin i'm facing same problem
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.
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.
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 ;-)
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. :)
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".
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...
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.
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 = defaultsWith that line still in place, acl_system2 seems to completely ignore me when I sayaccess_control [:new, :edit, :destroy] => 'admin': i.e, it allows all logged in users to access those actions. If I change it toaccess_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? :)
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
This change seems to have fixed the bug. Thanks!
An excellent plugin.
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.
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.
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.
nm i already do. heh.
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!
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.
Ezra, Do you have any plans to release the model access controls that you mentioned in post #18?
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...?
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!
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!
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.
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:
All that does is force getting the context every time. You can email me at cole dot jeff at gmail dot com. Thanks...# 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)
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.
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.
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?() ?
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:
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?
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.
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.
Hi, I'm trying to use that with the userstamp plugin and I get following error: protected method `current_user' called for #
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.
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?
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.
Looks like quite a promising plugin. SVN keeps timing out for me, though, just like for Chris above.
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
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.
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.
Thanks for the reply. I'll try it as soon as possible. :)
Hello all.
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
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
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.
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.
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 :-(
Wouldn't this plugin be RBAC and not ACL?
Really stupid Question. How/where do I install this plugin?..I checked it out using svn.
Never mind...I figured it out :)
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
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
Nice fix mcm8 in post 55, this was a feature I really needed to DRY up things.
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
Tom it looks like you are adding a closing paren without an opening paren.
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
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 :-(
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!
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.
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.
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
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"]
Did it, with: script/plugin install http://opensvn.csie.org/ezra/rails/plugins/dev/acl_system2/
undefined method `restrict_to' for #<#
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...
Thanks for the reply. I'll try it as soon as possible. :)
Very helpful article, thank you!
Your home page is very friendly and rich with information! Thanks!
Predicted values in template editing can be done through any PHP script?