Aug 30 2006

BDD-style JavaScript testing

dastels @ 6:53 pm

BDD-style JavaScript testing:

“Borrowing from Behaviour Driven Development techniques, especially the RSpec framework I’ve added some new features to script.aculo.us™ testing library.”

(Via mir.aculo.us.)

Check it out!


Aug 30 2006

Role Based Authentication from Rails Recipes. Part 2

dastels @ 2:19 am

Once I had the user roles code in place, working, and cleaned up, I decided to extend it by allowing regular expressions in the rights instead of literal strings (for controller & action names). Whether I stick with this going forward, who knows… but it’s convenient for development. Instead of separate rights for each action on a controller I can specify .* as the action to have the right apply to all actions for a controller.

Recall that I ended up with the core of the rights checking code actually in the Right class:

class Right < ActiveRecord::Base
  has_and_belongs_to_many :roles

  def has_right_for?(action_name, controller_name)
    action == action_name && controller == controller_name
  end
end

 

My first step was to wrap Rights attributes in a Regexp and do a match with the requested controller/action names:

class Right < ActiveRecord::Base
  has_and_belongs_to_many :roles

  def has_right_for?(action_name, controller_name)
    get_action_regex.match(action_name) && get_controller_regex.match(controller_name)
  end

  def get_action_regex
    Regexp.new(action)
  end

  def get_controller_regex
    Regexp.new(controller)
  end
end

 

It might be a case of premature optimization, but the Regexps can easily be cached since they are nicely encapsulated:

class Right < ActiveRecord::Base
  has_and_belongs_to_many :roles

  def has_right_for?(action_name, controller_name)
    get_action_regex.match(action_name) && get_controller_regex.match(controller_name)
  end

  def get_action_regex
    @action_regex || (@action_regex = Regexp.new(action))
  end

  def get_controller_regex
    @controller_regex || (@controller_regex = Regexp.new(controller))
  end

end

 

Part of the reason I blogged this was as an example of how much easier and obvious an enhancement can be when the code is cleanly (and extremely) refactored. More importantly, the details of rights can be changed without anything outside of the Right class being aware of it. If this ability is not one of the core benefits of OO, what is?


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.