Ruby on Rails
LoginGeneratorACLSystem

This example is based on LoginGeneratorAccessControlList (which was based on ACLController).

It uses AccessControlListExample database model and assumes you used the LoginGenerator.

It adds Roles such that

Users <-> Roles <-> Permissions

A User’s permissions are added to their session when they authenticate. Permissions are of the form {controller}/{method}. For example:

posts/add
posts/edit
posts/index   #use this if you want to protect "url/posts" 

Using before_filter :require_login will require the user to have the correct permission string before the action is performed. If the user is unauthorized, they are returned to the requesting page. You may use the :except attribute to allow anyone to access a specific method.

Permissions are kept as part of their session. Any changes made to the User’s permissions will not take effect until they login again. The filters added to this controller will be run for all controllers in the application. Likewise all the methods added be available for all controllers.

To Install:

  1. Follow the directions in HowToQuicklyDoAuthenticationWithLoginGenerator
  2. Create the database schema described in AccessControlListExample
  3. Follow the instructions below:

Put this into lib/acl_system.rb:

# See <a href="http://wiki.rubyonrails.com/rails/show/LoginGeneratorACLSystem">http://wiki.rubyonrails.com/rails/show/LoginGeneratorACLSystem</a>

module ACLSystem
  include LoginSystem

  # This module wires itself into the LoginSystem authorize? method.  You
  # should use the normal:
  #
  #   before_filter :login_required
  #
  # or to leave some actions unprotected:
  #
  #   before_filter :login_required, :except => [ :list, :show ]
  #

  protected

  # Authorizes the user for an action.
  # This works in conjunction with the LoginController.
  # The LoginController loads the User object.
  def authorize?(user)
    required_perm = "%s/%s" % [ params['controller'], params['action'] ]

    if user.authorized? required_perm
      return true
    end

    return false
  end
end

Put this into app/models/permission.rb:

# See <a href="http://wiki.rubyonrails.com/rails/show/AccessControlListExample">http://wiki.rubyonrails.com/rails/show/AccessControlListExample</a>
# and <a href="http://wiki.rubyonrails.com/rails/show/LoginGeneratorAccessControlList">http://wiki.rubyonrails.com/rails/show/LoginGeneratorAccessControlList</a>

class Permission < ActiveRecord::Base
  has_and_belongs_to_many :roles
end

Put this into app/models/role.rb:

# See <a href="http://wiki.rubyonrails.com/rails/show/AccessControlListExample">http://wiki.rubyonrails.com/rails/show/AccessControlListExample</a>
# and <a href="http://wiki.rubyonrails.com/rails/show/LoginGeneratorAccessControlList">http://wiki.rubyonrails.com/rails/show/LoginGeneratorAccessControlList</a>

class Role < ActiveRecord::Base
  has_and_belongs_to_many :permissions
  has_and_belongs_to_many :users
end

Then edit your controllers/application.rb to resemble:

# The filters added to this controller will be run for all controllers in the application.
# Likewise will all the methods added be available for all controllers.

require_dependency "acl_system" 

class ApplicationController < ActionController::Base
  include ACLSystem
  model :user
end

Add the following to your user model:

  has_and_belongs_to_many :roles

  # Return true/false if User is authorized for resource.
  def authorized?(resource)
    return permission_strings.include?(resource)
  end

  # Load permission strings 
  def permission_strings
    a = []
    self.roles.each{|r| r.permissions.each{|p| a<< p.name }}
    a
  end

To use it, just call the normal LoginGenerator filter in your controllers:

before_filter :login_required

or to leave some actions unprotected:
before_filter :login_required, :except => [ :list, :show ]

-JamesHillyerd


QUESTION

How would you distinguish an admin from a regular user?

ANSWER

To distinguish an admin from a regular user you need to add entries to the roles, users_roles, permissions, permissions_roles tables. For two users, moe and curly, identfied with 1 and 2, respectively:

INSERT INTO roles (id, name) VALUES (1, 'admin')
INSERT INTO roles (id, name) VALUES (2, 'regular')

INSERT INTO roles_users (user_id, role_id) VALUES (1, 1)
INSERT INTO roles_users (user_id, role_id) VALUES (2, 2)

INSERT INTO permissions (id, name) VALUES (1, 'admin/index')

INSERT INTO permissions_roles (permission_id, role_id) VALUES (1, 1)

Now, moe has a ‘superuser’ role, this role has permission to do the admin/index action.

-TerryLorber

QUESTION
How do you give the regular user or the admin more permissions?? You can give access to the index action, but it kicks you out for the list action, etc.

QUESTION
This says “A User’s permissions are added to their session when they authenticate.” How does this happen? It looks like the permissions are rebuilt every time authorized? is called.

ANSWER
I think you are correct: the permissions strings are rebuild on each call. I don’t think that ruby actually has to query the database each time, as user.roles should be lazy-loaded once and then cached.

It should be trivial to modify the user object to cache the strings. I’m not actively using this code right now, so if someone else can try it, test it and then modify this page, it would be appreciated!

-JamesHillyerd

A straight forward extension would be to keep permissions as regexp:s and do regexp matching. This would enable you to do compact patterns for an admin.

insert into permissions(name,info) values('.*/.*', 'All access'); 

def authorized?(resource)
    match=false
    permission_strings.each do |p|
      r = Regexp.new(p)
      match = match || ((r =~ resource) != nil)
    end
    return match
end

-FredrikAndersson

I have just extended this with the concept of access-levels.

The short story:

  1. A level-attribute field the permissions_roles table that becomes an extra attribute on the permission.
  2. A :creator-relationship on every object, pointing to the :user-class
  3. Passing an arguments-hash all the way down to the authorized? method in user.rb if !@params[‘id’].nil? (i.e. we’re working on an existing object):
    
    if session[:user] and authorize?(session[:user],:object => @object)
       return true
    end
    

The authorize?-method needs to take a second argument, “arguments”, but just passes it on to user.authorized?

  1. The magic in user.rb:
    Change authorized?-method to:
      def authorized?(resource,arguments)
        if arguments.nil?
          return permission_strings.include?(resource)
        end
        if !arguments[:object].nil?
          return permission_strings.include?(resource) &&
          (permission_levels[resource].to_i.eql?(2) || arguments[:object].creator.id.to_i.eql?(self.id.to_i))
        end
      end
    
      def permission_levels
        b = {}
        self.roles.each do |r|
           r.permissions.each do |p|
            if b.empty? || b[p.name].nil? || p.level.to_i > b[p.name].to_i
              b[p.name] = p.level.to_i
            end
           end
        end
        b
      end
    

Now, the last and least elegant thing in my solution is my current strategy for finding what type of object the ID refers to. I have the controller-name, but how can I use this to find the object with id @params[‘id’] ? For now, I have hard-coded it in login_required in login_system.rb:

    if !params['id'].nil?
      if params['controller'].eql?('news')
        @object = News.find(params['id'])
      end
      if session[:user] and authorize?(session[:user],:object => @object)
        return true
      end
    else
      if session[:user] and authorize?(session[:user],nil)
        return true
      end
    end

How can I find the class-name automagically so that I can use it instead of the hard-coded News.find ?

- VegardEngen

def model_class
  klass_name=Inflector.classify(controller_name)
  Inflector.constantize(klass_name)
end
  ...
  model_class.find(params['id'])

- Simo Addsw.it

QUESTION
Perhaps I’m missing it, very very newby, but is it possible to block access? as in:

Jane has access to the whole house (living room, kitchen, bathroom, bedroom)
John has access to all rooms BUT the bedroom

  Jane gets house/.*
  John gets house/.*  - and - not house/bedroom

thanks!

- Jay Wiggins

ANSWER

Using FredrikAndersson’s regex example above, you can create these permissions:

house/.*
house/[^(bedroom)]

grant the first one to a role of jane’s and the second to a role of john’s.


The above doesn’t work for me but

house/(?!(bedroom))

works.

- Calle Gustafsson

I am getting the following error :

protected method `authorized?' called for #\< User:0x3970708\>

c:/ruby/lib/ruby/gems/1.8/gems/activerecord-1.13.0/lib/active_record/base.rb:1494:in `method_missing'
#{RAILS_ROOT}/lib/acl_system.rb:22:in `authorize?'
#{RAILS_ROOT}/lib/login_system.rb:49:in `login_required'

what could be a solution?
Vinit

Rolled back to last good information.

Put the functions authorized? and permission_strings above the “protected” in user.rb
goodi

Updated from @session/@params to session/params
~ ba

Rails newbie here… For some reason after I moved from the simple login authentication to trying ACL I get redirected from my target page to the login page again after I login. Wh at gives??