Search code examples
ruby-on-railspermissionsrolescancan

Defining abilities in more complex environment with role and group models


in my rails app (I use devise and cancan), each (registered) user belongs to exactly one role ('Administrator' or 'Users') but to at least one group (something like 'Family', 'Friends', 'Co-workers'). At runtime, when a new folder (see below) is created, a habtm relation to one or many groups can be set, which defines who can access the folder. Selecting no group at all should result in a world-wide accessible folder (i.e. users do not have to be logged in to access these folders). But right now, I don't know yet, how to define such world-wide accessible folders in my ability.rb, because I do not know how to define "can read folders which have no groups associated to it".

The relevant snippet of my app/models/ability.rb looks like this:

user ||= User.new
if user.role? :Administrator
  can :manage, :all
elsif user.role? :Users
  # user should only be able to read folders, whose associated groups they are member of
  can :read, Folder, :groups => { :id => user.group_ids }
else
  # here goes the world-wide-accessible-folders part, I guess
  # but I don't know how to define it:
  ## can :read, Folder, :groups => { 0 } ???
end

The relevant snippet of my app/controllers/folders_controller.rb looks like this:

class FoldersController < ApplicationController
  before_filter :authenticate_user!
  load_and_authorize_resource

Can someone give me a hint?


Solution

  • I had the same problem just the other day. I figured out the solution after reading the CanCan readme, which you should do if you haven't yet.

    You can view my solution here: Context aware authorization using CanCan

    To give you an answer more specific to your use case, do the follow:

    In your application controller you need to define some logic which will pick your abilities.

    class ApplicationController < ActionController::Base
      check_authorization
    
      def current_ability
        if <no group selected logic> # Maybe: params[:controller] == 'groups'
          @current_ability = NoGroupSelectedAbility.new(current_user)
        else
          @current_ability = GroupSelectedAbility.new(current_user)
        end
      end
    
      # Application Controller Logic Below
    end
    

    You'll then need to create a new ability (or abilities) in your app/models/ folder. You can also do cool stuff like this:

    if request.path_parameters[:controller] == groups
      @current_ability = GroupsAbility.new(current_group_relation)
    end 
    

    Where current_group_relation is defined in app/controllers/groups_controller.rb. This will give you specific abilities for specific controllers. Remember that a parent classes can call methods in child classes in Ruby. You can define a method in your controller, and call it from ApplicationController, as long as you are certain what controller is currently being used to handle the request.

    Hope that helps.

    EDIT: I wanted to show you what a custom ability looks like.

    # File path: app/models/group_ability.rb
    class GroupsAbility
      include CanCan::Ability
    
      # This can take in whatever you want, you decide what to argument to 
      # use in your Application Controller
      def initialize(group_relation)
        group_relation ||= GroupRelation.new
    
        if group_relation.id.nil?
          # User does not have a relation to a group
          can :read, all
        elsif group_relation.moderator?
          # Allow a mod to manage all group relations associated with the mod's group.
          can :manage, :all, :id => group_relation.group.id
        end
      end
    end