Search code examples
ruby-on-railsruby-on-rails-4authorizationhas-many-throughpundit

Rails 4 + Pundit : join model authorization in has_many :through association


In my Rails app, there are 3 models, defined by a has_many :through association:

class User < ActiveRecord::Base
  has_many :administrations
  has_many :calendars, through: :administrations
end

class Calendar < ActiveRecord::Base
  has_many :administrations
  has_many :users, through: :administrations
end

class Administration < ActiveRecord::Base
  belongs_to :user
  belongs_to :calendar
end

The join Administration model has a role attribute, that we use to define the role — Owner, Editor or Viewer — of a given user for a given calendar.

Indeed, in the app, a user can be Owner of a calendar, and Viewer of another calendar for instance.

I implemented authentication with Devise.

I have also started implementing authorization with Pundit: authorization is currently working for calendars, where users can perform different actions depending on their roles.

UPDATE: here is the current CalendarPolicy:

class CalendarPolicy < ApplicationPolicy

  attr_reader :user, :calendar

  def initialize(user, calendar)
    @user = user
    @calendar = calendar
  end

  def index?
    user.owner?(calendar) || user.editor?(calendar) || user.viewer?(calendar)
  end

  def create?
    true
  end

  def show?
    user.owner?(calendar) || user.editor?(calendar) || user.viewer?(calendar)
  end

  def update?
    user.owner?(calendar) || user.editor?(calendar)
  end

  def edit?
    user.owner?(calendar) || user.editor?(calendar)
  end

  def destroy?
    user.owner?(calendar)
  end

end

Now, I would like to implement a Pundit policy for the Administration model, as follows:

  • If a user is Owner of a calendar, then he can perform Index, Show, Create, New, Edit, Update and Destroy actions on the Administrations of this calendar.
  • But, if a user is Editor or viewer of a calendar, then he can only do two things: 1. perform Index action to see all the users of a calendar and 2. perform Destroy action on his own Administration to "leave the calendar".

My problem is the following:

  • An Administration instance only exist as the connection between a user and a calendar, as explained above.
  • So, to perform actions on an Administration instance, I need three pieces of context: administration_id, user_id and calendar_id.
  • However, Pundit only accepts two pieces of context in a policy, generally the user and the actual record (which would be administration here).

On the GitHub page of Pundit, in the Additional context section, we can read the following:

Additional context

Pundit strongly encourages you to model your application in such a way that the only context you need for authorization is a user object and a domain model that you want to check authorization for. If you find yourself needing more context than that, consider whether you are authorizing the right domain model, maybe another domain model (or a wrapper around multiple domain models) can provide the context you need.

Pundit does not allow you to pass additional arguments to policies for precisely this reason.

However, in very rare cases, you might need to authorize based on more context than just the currently authenticated user. Suppose for example that authorization is dependent on IP address in addition to the authenticated user. In that case, one option is to create a special class which wraps up both user and IP and passes it to the policy.

Does a has_many :through association constitute one of the "very rare cases" mentioned above or is there a simpler way to implement authorization for my Administration join model?


Solution

  • I don't think, that this is an exceptional case. What's the reason to break good practices, so explicitly stated in the link you provided?

    You can just add add_viewer, add_editor, remove_viewer, remove_editor actions in your CalendarController.

    First two can be authorized with your old CalendarPolicy.

    class CalendarPolicy
      # old staff here
    
      def add_viewer?
        user.is_owner?(calendar)
      end
    
      def add_editor?
        user.is_owner?(calendar)
      end
    end
    

    For remove operations you will need AdministrationPolicy though (I was wrong, saying Calendar policy is enogh):

    class AdministrationPolicy
      attr_reader :user, :authorization
    
      def remove_viwer?
        authorization.viewer? and authorization.user == user
      end
    
      def remove_editor?
        authorization.editor? and authorization.user == user
      end
    end