Search code examples
ruby-on-railspermissionscancanuser-roles

dynamic cancan from database with complex conditions


I'm trying to define user created roles that select permissions from a list of permissions. I want a permission like this:

def initialize(user)
  user.projects_users.each do |project_user|
    project_user.role.privileges do |privilege|
      can :create, ProjectsUser, :project_id => project_user.project_id
    end
  end
end

But I'm trying to save privileges in a database in such a way that I can do this to get the outcome above

def initialize(user)
  user.projects_users.each do |project_user|
    project_user.role.privileges do |privilege|
      can privilege.action.to_sym, privilege.subject_class.constantize, privilege.conditions
    end
  end
end

The problem lies in the 'privilege.conditions' part. I cannot store a condition that must be executed only in the Ability.rb file. If I try to store:

{ :project_id => project_user.project_id }

It would say there is no variable named 'project_user'. I could save it as a string and in my Ability file do eval(privilege.condition), however I would need to do this only on the values. I tried something like this:

def initialize(user)
  user.projects_users.each do |project_user|
    project_user.role.privileges do |privilege|
      can privilege.action.to_sym, privilege.subject_class.constantize, privilege.conditions.each do |subject, id|
        subject => eval(id)
      end
    end
  end
end

The error I'm getting is 'syntax error, unexpected =>, expecting keyword_end' for the 'subject =>' piece.

Not sure how to do this exactly...

I'm using this line of commands to test it:

@user_id = 4
@role = Role.create(name: "Tester", project_id: 4)
@priv = Privilege.create(:action => :create, :subject_class => 'ProjectsUser', :conditions => { :project_id => 'project_user.project_id' })
@role.privileges << @priv
@project_user = ProjectsUser.create(:user_id => @user_id, :role_id => @role.id, :project_id => @role.project_id)
@a = Ability.new(User.find(@user_id))
@a.can?(:create, ProjectsUser.new(:user_id => @user_id + 1, :role_id => @role.id, :project_id => @role.project_id))

Any advice?


Solution

  • Ok So I found a really easy work around. Essentially the do block on the conditions was not being evaluated correctly. Here's the working code:

    user.projects_users.each do |project_user|
      project_user.role.privileges.each do |privilege|
        can privilege.action.to_sym, privilege.subject_class.constantize, Hash[privilege.conditions.map {|subject, condition| [subject, eval(condition)] }]
      end
    end
    

    Notice the Hash[privilege.conditions.map {|subject, condition| [subject, eval(condition)] }]

    What this is doing is taking a symbol as the key in conditions such as :subject_id and mapping it to the evaluated condition, which evaluated to a particular id.

    In my model I have

    class Privilege < ActiveRecord::Base
        has_and_belongs_to_many :roles
    
      serialize :conditions, Hash
    end
    

    And an example model is:

    Privilege.create(
      :action => :create, 
      :subject_class => 'ProjectsUser', 
      :conditions => { :project_id => 'project_user.project_id' }
    )
    

    Update

    This method only works for conditions that are one level deep. A condition like this would NOT work. You will get a: TypeError: no implicit conversion of Hash into String

    :conditions => { 
      :project => { 
        :location_id => 'project_user.project.location_id'
      }
    }
    

    This is not the best solution, but a work around this is

    :conditions => { 
      :project => "{ 
        :location_id => eval(\"project_user.project.location_id'\")
      }"
    }