0

In my app I have Permission table, which stores all the logic what User can do. With Pundit I want to allow User to create new Campaign if Permission table allows. User can access Campaigns and create new, if Permission table contains this info:

  • permitable_type: Sysmodule // another table where I store info on System sections, where Campaigns is one of
  • permitable_id: 2 // means Campaigns from Sysmodule
  • level: 3 // means User can edit something in Campaigns section

So far I keep getting error "Pundit::NotDefinedError", unable to find policy of nil policies/application_policy.rb is standart, no changes. Obviously I am doing sothing wrong. How do I do this authorization correctly? Many thanks for any help! I am on Rails 5 + Pundit.

models/permission.rb

class Permission < ApplicationRecord
  belongs_to :permitable, polymorphic: true
  belongs_to :user
  enum level: {owner: 1, view: 2, edit: 3}
end

models/user.rb

has_many :permissions
has_many :campaigns, through: :permissions, source: :permitable, source_type: 'Campaign' do
  def owner_of
  where('`permissions`.`level` & ? > 0', Permission::owner )
  end
end

has_many :sysmodules, through: :permissions, source: :permitable, source_type: 'Sysmodule' do
  def can_access
  where('`permissions`.`level` & ? > 1', Permission::can_access )
  end
end

controllers/campaigns_controller.rb

def new
  @campaign = Campaign.new
  authorize @campaign
end

policies/campaign_policy.rb

class CampaignPolicy < ApplicationPolicy
attr_reader :user, :campaign, :permission
  @user = user
  @permission = permission
end
def new?
  user.permission? ({level: 3, permitable_type: "Sysmodule", permitable_id: 2})
end

views/campaigns/index.html.erb

<% if policy(@campaign).new? %>
</li>
   <li><%= link_to "New campaign", new_campaign_path(@campaign) %></li>
</li>
<% end %>
matiss
  • 747
  • 15
  • 36
  • 1
    Joining everything though a polymorphic relation on the `permissions` table is going to be a huge performance problem. Also calling class methods though `::` (`Permission::owner`) is not good style in Ruby as it looks like you are accessing a module constant. – max Sep 25 '16 at 16:40
  • I think what you are trying to do is `Permission.levels[:owner]`. `Permission::owner` is actually equal to `Permission.where(level: :owner)` – max Sep 25 '16 at 16:47
  • @max What's your suggestion to be able to set permissions (View/Edit/Delete) for any object (e.g., Campaign) for any User? Let's assume, from 10 campaigns in database, User can View two, Edit two, is Owner of two, but cannot access four. – matiss Sep 25 '16 at 16:49
  • A basic role based access system might be a better alternative. https://github.com/RolifyCommunity/rolify – max Sep 25 '16 at 17:09
  • https://en.wikipedia.org/wiki/Role-based_access_control – max Sep 25 '16 at 17:10
  • @max I checked Rolify before, however I did not get, how I will be able to allow User to have access on single record level (e.g. Campaign). Do you have any suggestion what should I change in this code to make Campaign policy work, please? – matiss Sep 25 '16 at 17:23

1 Answers1

0

Instead of dealing directly with what permissions a user should have try thinking of it what roles users can have in a system.

This makes it much easier to create authorization rules that map to real world problems.

Lets imagine an example where we have users and groups. The rules are as follows:

  • groups can be created by any user
  • the user that creates the group automatically becomes an admin
  • groups are private
  • only admins or members can view a group
  • only an admin can modify a group

The models:

class User < ApplicationRecord
  rolify 
end

class Group < ApplicationRecord
  resourcify
end

The policy:

class GroupsPolicy < ApplicationPolicy

  class Scope < Scope
    def resolve
      scope.with_roles([:admin, :member], current_user)
    end
  end

  def show?
    user.has_role?([:member, :admin], record)
  end

  def index?
    true
  end

  def create?
    true # any user can create
  end

  def new? 
   create?
  end

  def update?
    user.has_role?(:admin, record)
  end

  def edit?
    update?
  end

  def destroy?
    update?
  end
end

The controller

class GroupsController < ApplicationController

  respond_to :html

  before_action :autenticate!
  before_action :set_group!, only: [:show, :edit, :update, :destroy]

  def show
    respond_with(@group)
  end

  def index
    @groups = policy_scope(Group.all)
    respond_with(@groups)
  end

  def new
    @group = authorize( Group.new )
  end

  def create
    @group = authorize( Group.new(group_attributes) )
    if @group.save
      current_user.add_role(:member, @group)
      current_user.add_role(:admin, @group)
    end
    respond_with(@group)
  end

  def edit
  end

  def update
    @group.update(group_params)
    respond_with(@group)
  end

  def destroy
    @group.destroy
    respond_with(@group)
  end

  private 
    def set_group!
       @group = authorize( Group.find(params[:id]) )
    end

    def group_params
       params.require(:group).permit(:name)
    end
end
max
  • 96,212
  • 14
  • 104
  • 165
  • Thank you for this example. I probably could use Rolify for few general roles I have in my app. However I need to allow to assign permissions for some objects on record level. Do you think it would be ok performance wise, if I leave Permissions table for that purpose and move everything else to roles? I mean, I could probably optimize Permissions table, e.g., permitable_type could be Enum. Probably I have to optimize some queries as well. – matiss Sep 26 '16 at 08:55