Aug 30 2006

Role Based Authentication from Rails Recipes. Part 1

dastels @ 1:58 am

First of all.. this is a great book! I’ve already used a variety of Chad’s recipes, either as is or (more often) as the basis of something customized.

That said, I was a bit disappointed when I read through Recipe 32 “Authorizing Users With Roles”. Mind you the solution was fine and worked nicely. My issue was with the code itself. Specifically, the unless condition in:

def check_authorization
  user = User.find(session[:user])
  unless user.roles.detect{|role|
    role.rights.detect{|right|
      right.action == action_name && right.controller == self.class.controller_path
      }
    }
    flash[:notice] = "You are not authorized..."
    request.env["HTTP_REFERER"] ? (redirect_to :back) : (redirect_to home_url)
    return false
  end
end

 

What’s wrong? Well, if we look at this code we start with a user, get the users roles, get the roles rights, get the rights action & controller. All from code in a controller. That’s a classic example of function objects & data objects. “But,” you say, “this is Rails.” So what. It’s still Object Oriented Programming. Thankfully, this is easy to clean up.

First, we can extract the condition out into a separate method:

def check_authorization
  user = User.find(session[:user])
  unless has_right?(user)
    flash[:notice] = "You are not authorized..."
    request.env["HTTP_REFERER"] ? (redirect_to :back) : (redirect_to home_url)
    return false
  end
end

def has_right?(user)
  user.roles.detect{|role|
    role.rights.detect{|right|
      right.action == action_name && right.controller == self.class.controller_path
    }
  }
end

 

Now we can push the code that deals with the user into the User class:

class User
  #...
  def has_right_for?(action_name, controller_name)
    roles.detect{|role|
      role.rights.detect{|right|
        right.action == action_name && right.controller == controller_name
      }
    }
  end
end

 

Leaving this behind:

def has_right?(user)
  user.has_right_for?(action_name, self.class.controller_path)
end

 

Most of User#has_right_for? can be pushed farther, into Role:

class User
  #...
  def has_right_for?(action_name, controller_name)
    roles.detect{ |role| role.has_right_for?(action_name, controller_name) }
  end
end

class Role
  #...
  def has_right_for?(action_name, controller_name)
    rights.detect{ |right| right.action == action_name && right.controller == controller_name }
  end
end

 

We can go one step farther:

class Role
  #...
  def has_right_for?(action_name, controller_name)
    rights.detect{ |right| right.has_right_for?(action_name, controller_name) }
  end
end

class Right
  #...
  def has_right_for?(action_name, controller_name)
    action == action_name && controller == controller_name
  end
end

 

Now.. the controller has a user in hand. It asks that user if it has the right to access the requested page. That user in turn, has some roles.. it asks each role if it has the right for the requested page, each role in turn asks it’s rights. Everyone in the chain talks only to what it has directly in hand. This is the way it should be. Now the controller doesn’t know or care what it means to have the right to access an action… only that the user knows, and how to ask that user.


Aug 20 2006

has_and_belongs_to_many

dastels @ 2:19 am

For anyone learning rails in my wake, here the “Doh!” moments I had as I tried to get many-to-many relationships working. Either it took some digging to find the required information or it wasn’t immediately obvious.

Here’s my list of things I found by trial/error/reading/ and lots of googling (and, yes, it is a verb now.. I’ve been using it as a verb for at least 3 years).

  1. Put has_and_belongs_to_many in each class, naming the other class (it’s table name really… a plural)
  2. Add instances of one class to the collection in the other, e.g. order.items << album. Do this in only one direction.
  3. Before adding to an object’s collection, save the object. E.g
    if order.save
      order.items << an_item
    

    It seems that this is only required in the case where order is a freshly created object. This makes sense as it will not have an id (for the join table) until it is saved. Furthermore, both objects will have to have been previously saved (i.e. have ids).

  4. The join table is made up of the names of the two related tables.. in alphabetic order. E.g. items_orders, not orders_items
  5. Don’t put an id column in the join tables. Mine had such a column because I sketched out my schema in a migration, not realizing this.
  6. In your migrations, use :id => false as a second argument to create_table, e.g.:
    create_table :items_orders, :id => false do |t|
      t.column :item_id, :integer
      t.column :o rder_id, :integer
    end
    

If I’ve missed anything on this list, please let me know and I’ll add it.


Aug 20 2006

Delving ActiveRecords secrets

dastels @ 1:50 am

My ramp up on Rails continues. Today I’ve been getting familiar with ActiveRecords table relationship facilities. Pretty nice. One-to-one and one-to-many were trivial… although I’m currently stuck on getting many-to-many relationship working. I keep making progress in little steps, though.


Aug 15 2006

Back on Track

dastels @ 6:47 am

After being distracted by other things, I’m back to getting more up to speed with Rails. I’ve been doing a fair bit with Ruby lately, especially rSpec. Now I have some external motivations to dive back into Rails. I’ve pulled my ReadingList project off the shelf, and started from scratch again (for the third time.. previously once in Rails and once in Seaside), this time using rSpec as I go. After a couple days work, I have a load of functionality… including some AJAX dazzle this time.

Once it’s in good shape, it will go live here.


« Previous Page