måndag 29 juni 2009

Simple role management with nifty authentication

I recently had to implement role based authentication on top of nifty authentication. Messing about with roles is never much fun
and can easily grow into a complex mess of tangled if statements and what not.
The requirement stated that the three roles had to be implemented, Admin, Privileged User and Regular User, and a user can
only be assigned one role.
Looking at the simplest thing that could possibly work, I started of by adding a field to the users table containing an integer
value:

> ./script/generate migration add_role_type_to_users role_type:integer
> rake db:migrate

However, throwing integers around in the code everywhere isn't much fun and this is where a AppConfig file comes in handy
(see my previous post for more information):

development:
admin: 1
privileged_user: 2
regular_user: 3
...

Now instead of asking whether a user has a role_type of 3, we can ask it if it has a role_type of AppConfig.reqular_user.
But that's still not quite good enough though, since we still have to ask wether current_user.role_type ==
AppConfig.regular_user, a nicer api would be to ask the current user object directly, as in
current_user.regular_user?.
Well, that's easy enough, we just have to add the following methods to app/models/user.rb:

def admin?
role_type == AppConfig.admin
end

def privileged_user?
role_type == AppConfig.privileged_user
end

def regular_user?
role_type = AppConfig.regular_user
end

Now since we don't want a bunch of if statements in our views, it's a good idea to add the following methods to our
app/helpers/application_helper.rb

def admin_area(&block)
if current_user
if current_user.admin?
concat content_tag(:span, capture(&block), :class => "admin_area")
end
end
end

def privileged_user_area(&block)
if current_user
if current_user.admin? || current_user.privileged_user?
concat content_tag(:span, capture(&block),
:class => "privileged_user_area")
end
end
end

def regular_user_area(&block)
if current_user
concat content_tag(:span, capture(&block),
:class => "regular_user_area")
end
end

Now you can restrict your views like so:

<html>
<body>
<% admin_area do %>
<p>Content which only an admin can see</p>
<% end %>

<% privileged_user_area do %>
<p>Content which only an admin and a privileged user can see</p>
<% end %>

<% regular_user_area do %>
<p>Content which all logged in users can see</p>
<% end %>
</body>
</html>

But only restricting access to certain parts of your views is hardly enough, more likely you want to restrict access to certain
controller actions.
So lets start by creating the file config/initializers/authorizer.rb and paste the following snippet in there:

module Authorizer
def self.included(controller)
controller.extend ClassMethods
end

def authorized?(*roles)
if current_user
unless valid_user?(*roles)
session[:user_id] = nil
login_required
end
else
login_required
end
end

def valid_user?(*roles)
statement = returning [] do |s|
roles.each { |role| s << current_user.send("#{role}?") }
end.join(" || ")

return eval(statement)
end

module ClassMethods
def ensure_role(*args)
actions, roles = args.extract_options!, args
before_filter(actions) { |c| c.authorized? *roles }
end
end
end

ActionController::Base.send(:include, Authorizer)


This creates a before filter available to all controllers:

class FirstController < ApplicationController
ensure_role :admin, :privileged_user, :regular_user, :except => [
:index, :show
]
...
end

class SecondController < ApplicationController
ensure_role :admin, :privileged_user, :regular_user, :only => [
:create, :update
]
...
end


That's it for now, I might revisit it later and clean it up a bit and maybe turn it into a plugin.

Prettyfied niftyfied app config

In case you did'nt know already, Ryan Bates (of RailsCasts fame) has released a great collection of generators called
nifty-generators. Among a few other generators, there's one for generating an app config file for storing site wide configuration
options which you access like so:

APP_CONFIG[:config_option]

It's great, but not very pretty, I would much rather prefer something like:

AppConfig.config_option

Not that big of a difference but it fells a little bit nicer though, so here's how you go about accomplishing that goal.

Change the contents of the file config/initializers/load_app_config.rb from this:

raw_config = File.read(RAILS_ROOT + "/config/app_config.yml")
APP_CONFIG = YAML.load(raw_config)[RAILS_ENV].symbolize_keys

to this:

require 'ostruct'
require 'yaml'

raw_config = File.read(RAILS_ROOT+"/config/app_config.yml")
config = OpenStruct.new(YAML.load(raw_config))
::AppConfig = OpenStruct.new(config.send(RAILS_ENV))