Topic: Design Review: Business Object Permissions in my Models

Greetings forum! The design I describe below for custom object permissions is working well for me in development. I have not yet taken this to production. I am posting this design for public review and commentary. Is there a more efficient way to implement a dynamic roles/rights model? Please tear it apart. What am I doing wrong?

Kudos to Bruce Perens for his ModelSecurity gem.

Happy Friday!
- Taylor

-----------------------------------

A. Overview

My new collaboration application requires a roles/rights system that goes beyond what is described in "Rails Recipies", Fowler. Rather than having a global, fixed caste system of moderators or admins, my access rights are very 'per object' and subject to business rules.

I implemented my permissions business rules down at my Model layer as ModelSecurity User.current aware can_user_do_this? boolean instance methods. I then use these methods with my current working object (@project.can_user_do_this?) in the view rhtml to restrict visibility of widgets and in the controller layer to prevent posts of information.

I hope to, as a separate phase, implement full on ModelSecurity, but haven't though that out fully yet.


B. Requirements

My Business Rules / Permissions Requirements:
1. Project Owners can view and modify all parts of their own projects.
2. Project Team Members can view and modify some parts of their team projects.
3. Registered Visitor can see most parts of all projects.
4. Anonymous can see parts of all projects.
5. Nobody can modify a project if the Owner hasn't paid his subscription fee.
6. A Project Owner may also be a Project Team Member on other Projects.


C. Implementation Examples

Model Permissions implementation:

class Project < ActiveRecord::Base
  belongs_to :user # owner relationship, do not confuse with User.current.
  def user_can_read_core?
    # ModelSecurity convention - means user logged in. Must have an account to read core.
    true if User.current
    false
  end
  def user_can_post_core?
    # Only the owner can post core information, and only if he's paid.
    true if User.current && User.current.id == user_id && project_is_paid_for?
    false
  end
end

;

Controller Permissions usage:

class ProjectController < ApplicationController
  before_filter :require_login, :only => [ :post_core ]
  def post_core
    @project = Project.find_by_id(@params['id'])
    raise "Permissions Exception" if @project.user_can_post_core?
    # Do magic to store parameters to object.
    ...
  end
end

;

View permissions usage:

<% if @project.user_can_read_core? %>
  Project ID: <%= @project.id %>, Name: <%= @project.name %>
<% end %>

<% if @project.user_can_post_core? %>
  Modify the core information below: ...
<% end %>


;

Last edited by brockman (2006-08-25 11:10:36)

Re: Design Review: Business Object Permissions in my Models

Looks good. I have done something similar, but in my case I placed the permissions in the User model and used Single Table Inheritance for the different roles. What I like about this is it places all permissions in one spot and different roles can override permission through inheritance. Because in your case the role of the user changes based on its relation to an object, I doubt this will work well for you. But, you are welcome to gather ideas from it. Anyway, here's the details:

There's several models that need user permissions for the CRUD (and a couple other) operations. Right now there are 3 roles, but this is fairly easy to change by adding another User subclass: Admin, Member, and Guest.

Here's an example of the view:

<% for widget in @widgets %>
  <% if current_user.can_view_widget? widget %>
    <%= link_to widget.name, widget_url(widget) %>
    <%= link_to 'Edit', edit_widget_url(widget) if current_user.can_update_widget? widget %>
    <%= link_to 'Destroy', widget_url(widget), :method => 'delete' if current_user.can_destroy_widget? widget %>
  <% end %>
<% end %>
<%= link_to 'Create Widget', new_widget_url  if current_user.can_create_widget? %>

We could implement each method individually in the user model, but that would be a lot of work. So we do a little magic with method_missing and point to a generic method.

class User < ActiveRecord::Base
  def method_missing(call, *args)
    if call.to_s =~ /^can_(\w+)_(\w+)\?$/
      can? $1.to_sym, $2.to_sym, args.first
    else
      super
    end
  end

  def can?(action, item_type, item = nil)
    false
  end
end


So the abstract user class returns false for everything. We can override that in each subclass to handle the permission that we want:

class Guest < User
  def can?(action, item_type, item = nil)
    action == :view # Guest can view everything, but has no other permissions
  end
end

class Admin < User
  def can?(action, item_type, item = nil)
    true # Admin can do anything
  end
end

class Member < User
  def can?(action, item_type, item = nil)
    action != :destroy # Member can do anything but destroy items
  end

  def can_destroy_widget?(widget)
    true if widget.user == self # Oh, but he can destroy widgets that he owns
  end
end


As you can see, you can use the generic can? method, or the more specific "can_destroy_widget?" method which will override the generic one because method_missing won't be called.

Although I have not gone this far yet, I expect to be able to have a before_filter on the controller and forward the params[:action] to this can? method so we don't have to perform a check for permissions in every controller action.

Like I said, I doubt this will work for you because you need the roles to be dynamic, but you may be able to take a few ideas such as the method_missing magic.

Railscasts - Free Ruby on Rails Screencasts

Re: Design Review: Business Object Permissions in my Models

Hmm I really like that design ryanb.  I see its limitations, but when it's sufficient, its very elegant.

My RoR journey  -- thoughts on learning RoR and lessons learned in applying TDD and agile practices.

Re: Design Review: Business Object Permissions in my Models

ryanb: method_missing is a nice trick. That will reduce the complexity of my implementation. Thanks!

Re: Design Review: Business Object Permissions in my Models

ryanb wrote:

class User < ActiveRecord::Base
  def method_missing(call, *args)
    if call.to_s =~ /^can_(\w+)_(\w+)\?$/
      can? $1.to_sym, $2.to_sym, args.first
    else
      super
    end
  end

Holy crapola Batman!

I actually understand some of this stuff..
I like how you slapped an *args in there to leave the door open for any number of arguments.

This stuff is magical to me .. some I have never seen before

1. I know that super reaches for the parent class, but what purpose does it serve here?

$1.to_sym

2. Those dollar signs remind me of PHP variables.. are they some type of variable?
can? $1.to_sym, $2.to_sym, args.first

3. How does this tie into the can method? Why arg.first?
/^can_(\w+)_(\w+)\?$/

4. I dont even know where to start to piece this one together.
=~

5. Does that mean equals Espa

Last edited by pimpmaster (2007-03-16 16:15:35)

Re: Design Review: Business Object Permissions in my Models

pimpmaster wrote:

1. I know that super reaches for the parent class, but what purpose does it serve here?

I'm only interested in methods which start with "can_". If it doesn't, then pass it along as if I never interrupted the flow of things. This is so ActiveRecord::Base can have its own method_missing call and do whatever it wants to it.

pimpmaster wrote:

$1.to_sym

2. Those dollar signs remind me of PHP variables.. are they some type of variable?

A dollar sign is a global variable in Ruby. When doing a regular expression like I am here, Ruby fills the $n ($1, $2, $3, etc.) global variables with the content in the parenthesis in the regular expression.

In other words, if it catches the method "can_create_post?" it will fill $1 with "create" and $2 with "post".

pimpmaster wrote:

can? $1.to_sym, $2.to_sym, args.first

3. How does this tie into the can method? Why arg.first?

The "can?" method is defined early in the User model. It takes a few arguments: the action to perform, the name of the model to perform it on, and the model itself if there is one. "args.first" takes the first argument that was passed to the method and send it to the "can?" method.

For example, if you call this:

user.can_destroy_post? @post

It will be interpreted as this:

user.can? :destroy, :post, @post

pimpmaster wrote:

/^can_(\w+)_(\w+)\?$/

4. I dont even know where to start to piece this one together.

That's a regular expression. Think of it as a very powerful way to search and manipulate text. It is definitely worth taking some time to learn. It's not limited to Rails or even Ruby, it is adopted by many programming languages.

pimpmaster wrote:

=~

5. Does that mean equals Espa

Railscasts - Free Ruby on Rails Screencasts

Re: Design Review: Business Object Permissions in my Models

Trying to get this rocking and right from the jump I have problems in my view.

wrong argument type String (expected Module)

I will be the first to admit that my view code for this is sloppy at best, but I can't figure out why it wont take strings as an argument..

<%= f.select :type, [['Choose Role...', '']] + [['Admin','admin'],['Member','member']] %>

I am convinced that I am going about this all wrong. There must be a cleaner (and more dynamic) way for me to return an array of available User subclasses, but I have no clue how.

Re: Design Review: Business Object Permissions in my Models

The "type" column behaves a little strangely. If you try setting it directly it doesn't like it. Try creating a virtual attribute to handle this for you.

# view
<%= f.select :kind, [['Choose Role...', '']] + [['Admin','admin'],['Member','member']] %>

# User model
def kind
  read_attribute(:type)
end

def kind=(kind)
  write_attribute(:type, kind)
end


Might want to rename. Haven't tested this.

Railscasts - Free Ruby on Rails Screencasts

Re: Design Review: Business Object Permissions in my Models

That did the trick... Rails sure can be picky sometimes huh?

Now I am faced with another sticking point... how do I actually restrict access at the controller level?

I have 3 users right now and their type column is set to "admin" "member" and "guest" respectively.

At this stage I think I need a before filter, but I'm not sure how to implement.

Re: Design Review: Business Object Permissions in my Models

In my application, users are simply admins or they aren't so I use the following method.

In controllers/application.rb:

def admin_check
    user = User.find_by_id(session[:user_id])
    unless user.admin
      flash[:notice] = "You do not have permission to access that area."
      redirect_to(:controller => "login", :action => "index")
    end
  end

In controllers/login_controller.rb:
before_filter :admin_check, :only => [ :add_user, :delete_user ]

Then you can set which actions you want the filter to run on.

Last edited by HarryP103 (2007-06-21 18:55:30)

Re: Design Review: Business Object Permissions in my Models

Thanks Harry!

That works splendidly..my only question is what method you are using to hide admin stuff in your views?

Re: Design Review: Business Object Permissions in my Models

In the controller, pass the current user object like so:

@user = User.find_by_id(session[:user_id])

Then in the view, I just do this:
<% if @user.admin %>
  Do whatever here...
<% end %>

Re: Design Review: Business Object Permissions in my Models

Ryan, thanks for referring me to this post. I looks the business...  something that will suit my project.

Just a few questions to clarify my understanding:
1. how does rails know whether a user is a member, guest or an admin. Is there a column in your users table where this is recorded?
2. You say that there should be a before_filter in the controllers, what would this look like?


Thanks

Re: Design Review: Business Object Permissions in my Models

jamesc wrote:

1. how does rails know whether a user is a member, guest or an admin. Is there a column in your users table where this is recorded?

Yep, it's just a "type" column. Look into Single Table Inheritance for more info.

jamesc wrote:

2. You say that there should be a before_filter in the controllers, what would this look like?

Unfortunately it's too complicated to post here. Basically you'll have to take the name of the action (new/edit/destroy/etc.) and translate that into the permission call on the current user to see if they have permission to perform that action on the model.

Railscasts - Free Ruby on Rails Screencasts

Re: Design Review: Business Object Permissions in my Models

Perhaps for the benefit of others creating web sites with different user permissions/roles, I found this incredibly easy to use plug-in that does just the thing:
See http://brainspl.at/articles/2006/02/20/ … acl_system

Re: Design Review: Business Object Permissions in my Models

brockman wrote:

Model Permissions implementation:

class Project < ActiveRecord::Base
  belongs_to :user # owner relationship, do not confuse with User.current.
  def user_can_read_core?
    # ModelSecurity convention - means user logged in. Must have an account to read core.
    true if User.current
    false
  end
  def user_can_post_core?
    # Only the owner can post core information, and only if he's paid.
    true if User.current && User.current.id == user_id && project_is_paid_for?
    false
  end
end

Hi, brockman, sorry I'm a newbie from Java background. In the above code, you seem to store the login user in a class variable @@current, and get it from an accessor User.current, but does this cause problems when multiple users login at the same time? thanks a lot.

Re: Design Review: Business Object Permissions in my Models

Oh, I digged the ModelSecurity Gem source code, and found the current user is  a thread local variable which is set at the start of each request, this is a lot like acegi. I got it!

Re: Design Review: Business Object Permissions in my Models

Hi, with regards with this tutorial, there are certain areas of the codes that I do not understand and I hope to clarify them soon!

   1. <% for widget in @widgets %>
   2.   <% if current_user.can_view_widget? widget %>
   3.     <%= link_to widget.name, widget_url(widget) %>
   4.     <%= link_to 'Edit', edit_widget_url(widget) if current_user.can_update_widget? widget %>
   5.     <%= link_to 'Destroy', widget_url(widget), :method => 'delete' if current_user.can_destroy_widget? widget %>
   6.   <% end %>
   7. <% end %>
   8. <%= link_to 'Create Widget', new_widget_url  if current_user.can_create_widget? %>

(Is there are need for the widget or is it just an example? What is this widget for?

if call.to_s =~ /^can_(\w+)_(\w+)\?$/ -- I type exactly? What does this line mean?

Also, for my application, the access levels is based on user's input. E.g. I am a super admin and I want to add my users. Therefore, it will be as follows.

Name: Happy
Password: ....
Confirm Password: ....
User type: Director --- User selects this radio button

Now i need to do an if else to see what is the user's input before granting the access. How do I do it? Sorry, I am quite new to rails. Hence these questions...
Please reply soon. Thanks! smile

Re: Design Review: Business Object Permissions in my Models

 =~ /^can_(\w+)_(\w+)\?$/

means : find a string that begins with 'can' followed by _ followed by at least one word followed by _ followed by at least one word followed by ? followed by  a global variable.

Thus

if call.to_s =~ /^can_(\w+)_(\w+)\?$/
      can? $1.to_sym, $2.to_sym, args.first

if call.to_s matches a string as described above it executes:
can? $1.to_sym, $2.to_sym, args.first

# which will result in :
user.can? :destroy, :post, @post
# in case call.to_s is:
user.can_destroy_post? @post

Look at:' user.can_destroy_post? @post' - you will see that it has the pattern that it described in the regex: can followed by _ followed by a word ......
The regex stores the \w+ in these 'containers': $1, $2, .. and you call them with:
can? $1.to_sym, $2.to_sym, args.first

which gives you:
can? followed by your first word to_sym followed by a komma followed by the second word to_sym ....

resulting in:
user.can? :destroy, :post, @post

in the concrete example the ryanb has given.

Your question is actually entirely answered in ryanb's posts above, so read that again. I am just repeating it here, because regex is extremely helpful. Just read through this thread again.

Add on: the whole thing works because ruby will look for the method that you specified; if it does not find it, it will call method_missing. Usually method missing calls the well know error; but now you have defined, that method_missing will
match the call.to_s with a regex and if there is a match, the call.to_s will be reformatted into ' user.can? :destroy, :post, @post ' , where the can? method has been defined in your model.

Last edited by ediestel (2008-01-09 17:27:19)

Re: Design Review: Business Object Permissions in my Models

I'm going to resurrect this thread because I think the method is great and I'm implementing it in my current project. But I have one awkward detail to work out.

In my view, I can show/hide a link based on admin status with:

<% if current_user.can_view_item? %>
  #link to item
<% end %>

but what to do when there is no user logged in (That is, when current_user returns false? - I'm using restful_authentication as well) Then I get a real no method error.  The obvious solution is to do this:

<% if current_user == true && current_user.can_view_item? %>

but it seems clunky. What's a cleaner way to prevent can_view_item? from generating a no method error when called on false (or nil, I suppose)?

Posts [ 1 to 20 of 21 ]

Pages 1 2 Next