How to specify nested CanCan permissions with non-standard relationship names?

I am using Devise for authentication and CanCan for authorization.

target

I have two models, User and Sponsorship, where sponsorship provides has_many: through the relationship between the sponsor user and the client user.

I want to set up CanCan so that a privileged user sponsor?

can manage their own sponsors, i.e. only sponsorships for which Sponsorship#client_id == user.id

. The user can also have privileges admin?

, in which case he can manage any Sponship.

model

class User < ActiveRecord::Base
  has_many :sponsor_links, :class_name => 'Sponsorship', :foreign_key => 'client_id'
  has_many :sponsors, :through => :sponsor_links, :class_name => 'User'

  has_many :client_links, :class_name => 'Sponsorship', :foreign_key => 'sponsor_id'
  has_many :clients, :through => :client_links, :class_name => 'User'

  def has_role?(role)
    ... return true if this user has <role> privileges
  end
end

class Sponsorship
  belongs_to :sponsor, :class_name => 'User'
  belongs_to :client, :class_name => 'User'
end

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new  # handle guest user (not logged in)
    if user.has_role?(:admin)
      can :manage, :all
    elsif user.has_role?(:sponsor)
      # not sure if the third argument is correct...
      can :manage, Sponsorship, :sponsor => {:user_id => user.id}
    end
  end
end

      

routes

I've set up nested routes to reflect the fact that the sponsoring user owns their clients:

resource :users, :only => [:index]
  resource :sponsorships
end

      

question

What's the correct way to upload and authorize user and sponsorship resources in my sponsored controller?

what i tried

It's like a normal nested resource that CanCan handles easily. But the relationships have non-standard names (ex: sponsor_links, not: sponsorship) and I didn't figure out how to set up ads load_and_authorize_resource

in my sponsor controller.

Among the many things I've tried that don't work;), here's one of the simpler versions. (Note also that my abilities may not be configured correctly - see above):

class SponsorshipsController < ApplicationController
  load_and_authorize_resource :sponsor_links, :class_name => "User"
  load_and_authorize_resource :sponsorships, :through => :sponsor_links
  respond_to :json

  # GET /users/:user_id/sponsorships.json
  def index
    respond_to @sponsorships
  end

  # GET /users/:user_id/sponsorships/:id.json
  def show
    respond_to @sponsorship
  end
end

      

By running CanCan :: AccessDenied errors, I know that:

  • The index

    user :sponsor

    authentication is performed for the user.
  • The index

    user :admin

    authentication is performed for the sponsorship.
  • In show

    , regardless of role, no authentication is performed for sponsorship.
+3


source to share


1 answer


partial answer

The first issue was the Abilities specification, which read:

...if user.has_role?(:sponsor)
  can :manage, Sponsorship, :sponsor => {:user_id => user.id}
end

      

but there should have been only

...if user.has_role?(:sponsor)
  can :manage, Sponsorship, :user_id => user.id
end

      

(Remember kids, unit tests are your friend! I forgot this lesson somehow.)

In the controller, I also changed:

  load_and_authorize_resource :sponsor_links, :class_name => "User"
  load_and_authorize_resource :sponsorships, :through => :sponsor_links

      



just

  load_and_authorize_resource :user
  load_and_authorize_resource :sponsorship

      

It basically works: it installs @user and @sponsorship and allows access to them. But the index function loads all sponsorships available for current_user, not just those owned by user_id. My fix - perhaps not optimal - was to rewrite the index function from

  def index
    respond_with(@user, @sponsorships)
  end

      

to

  def index
    @sponsorships = @sponsorships.where(:sponsor_id => @user.id)
    respond_with(@user, @sponsorships)
  end

      

With these changes, everything works.

If anyone has a more correct way to express this, I would like to know.

+1


source







All Articles