Aug 30

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.



Steve Jobs Memorial

Stay Hungry, Stay Foolish!
Steve Jobs

In memory of Steve Jobs