3

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?

Thibaud Clement
  • 6,607
  • 10
  • 50
  • 103
  • 1
    I don't think `Administration` is required here. I think you can get `administration_id` from `User` and `Calendar`. Isn't it allowed to make queries inside Pundit's policy? – dimakura Sep 11 '15 at 17:20
  • Thanks for your comment. Sorry if this question sounds dumb, I am not very familiar with Pundit: how can `Administration` be not required in its own `AdminstrationPolicy`? Can we initialize the `AdminstrationPolicy` with only `User` and `Calendar`? – Thibaud Clement Sep 11 '15 at 17:26
  • Should I implement these authorization rules in the `CalendarPolicy` instead of the `AdministrationPolicy`? – Thibaud Clement Sep 11 '15 at 17:30
  • 1
    In `CalendarPolicy` can't you get `Administration` as `Administration.where(calendar:calendar, user:user).first`? – dimakura Sep 11 '15 at 17:34
  • isn't `CalendarPolicy` already implemented? – dimakura Sep 11 '15 at 18:39
  • Yes, as mentioned in the question, `CalendarPolicy` is already implemented and working. Now, the challenge is to implement a different layer of rules for Administration. – Thibaud Clement Sep 11 '15 at 18:41
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/89419/discussion-between-dimakura-and-thibaud-clement). – dimakura Sep 11 '15 at 18:42
  • To answer your previous comment, I don't know — ie I am not sure — if we can get `Administration` as `Administration.where(calendar:calendar, user:user).first` since this would call for User and Calendar. And the entire problem here is whether or not we can call for more than two parameters in a Pundit policy. – Thibaud Clement Sep 11 '15 at 18:43
  • Actually we can. It's plain Ruby. – dimakura Sep 11 '15 at 18:44

2 Answers2

1

Yes this is one of those rare cases.

# calendar controller show
@calendar = something    
@administration = @calendar.administration_of_current_user
authorize CalendarAdministrationContext

# pundit CalendarAdministrationContext
def initialize(user, administration, calendar)
  @user = user
  @administration = administration
  @calendar = calendar
end
penner
  • 2,707
  • 1
  • 37
  • 48
  • Thanks for your answer. If this is one of those rare cases, doesn't it mean that I need to create something like `AdministrationContext`, just like the `UserContext` example on the Pundit page? – Thibaud Clement Sep 11 '15 at 17:37
  • 1
    It really depends on how you plan on using the policy. But my thought are that the administration policy will always be used in conjunction with an @administration object and a stand alone policy for administration without an administration object would be useless? – penner Sep 11 '15 at 17:40
  • Yes, this is a valid point. What I am trying to accomplish here is to implement authorization regarding how users manage roles: 1. the roles of the users of a their own calendars and 2. their own role in any calendar, disregarding their role. Should this be done in the Administration poilcy (since the role column is in the administration table) or should we do it in the Calendar policy instead? I am kind of lost here. – Thibaud Clement Sep 11 '15 at 17:44
  • 1
    Strongly disagree. I think that "exceptional" case is only when you cannot get required information from given model. In this case you can easily navigate from `Calendar` to `Administration` models (using `User`). Using `AdministrationPolicy` will make it hard to use pundit in controllers and views. – dimakura Sep 11 '15 at 17:52
  • 1
    Agree. On the other hand, you don't want to make additional queries in the policy if you already have that data in the controller though. You can do all the lookups in the policy at the cost of performance. – penner Sep 11 '15 at 17:57
  • I don't think `Administration` model is needed in controller. The sole purpose of it is just checking permissions on given calendar. So I think it will end in one query in both cases. – dimakura Sep 11 '15 at 18:02
1

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
dimakura
  • 7,575
  • 17
  • 36
  • Thanks for your answer. The thing is that I have already implemented a `CalendarPolicy` defining rules for how users can / cannot perform actions on calendars: for instance, the Owner of a calendar can change the `name` of the calendar, but the Editor(s) and the Viewer(s) cannot. Now, I have another layer of permissions for users regarding the `roles`, as explained in the question. Should this other layer be implemented in the `CalendarPolicy` too, or in a separate `AdministrationPolicy`? Sorry if this does not sound clear, I am really confused. – Thibaud Clement Sep 11 '15 at 18:15
  • 1
    It should be done in the same Policy. Otherwise you should use two policies for single calendar instance in your views and controllers. Right? – dimakura Sep 11 '15 at 18:17
  • 1
    Add new layer as a new method in `CalendarPolicy`. It's a plain Ruby class after all. – dimakura Sep 11 '15 at 18:18
  • 1
    And use testing to be sure your security is not broken. – dimakura Sep 11 '15 at 18:18
  • 1
    Hmm but what if you want to display data about your authorization level in the view? The data should flow from controller to policy and from controller to view. Im not sure which way is right, I agree the docs point to this as being the correct answer. – penner Sep 11 '15 at 18:19
  • 1
    Would it helped if I added my current `CalendarPolicy` to the question? This way we would have something concrete to discuss and think about whether we should include both policies in the same file or not. – Thibaud Clement Sep 11 '15 at 18:32
  • 1
    @ThibaudClement great, please provide it – dimakura Sep 11 '15 at 18:33
  • Done, I updated the question with the content of calendar_policy.rb. – Thibaud Clement Sep 11 '15 at 18:35