dynamic cancan from database with complex conditions

1.6k views Asked by At

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?

2

There are 2 answers

1
TheJKFever On BEST ANSWER

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'\")
  }"
}
5
max On

I would definitely say that you are going about it a bit backwards. Creating a bunch of auth rules dynamically from database records is going to be really tricky to test due to the amount of setup that is going to be required everywhere in your test suite and is rather over-complicated.

Instead use blocks to declare complex relations and roles to store privileges.

Example using the awesome Rolify gem with a role scoped to a resource:

can :moderate, Forum do |forum|
  user.has_role?(:moderator, forum)
end

You can also use roles to assign privileges to groups:

if user.has_role? :admin
  can :crud, User
  # ...
end 

I would say that if you really need a complex totally dynamic solution where admins create authorization rules, role definitions etc from a web interface then you have definitely outgrown CanCan. But do you REALLY need it?