Differing views based on permission levels

I am crossposting this here from the Rogue’s Parley, just in case I can capture more opinions or ideas.

I am trying to come up with all the ways that I can differ a view based on levels of permissions. I am coming up with a contrived example to help me through this. Let’s say I need to differ a menu based on levels of permission. What options are there to accomplish this? This menu could differ based on any number of factors and permission levels.

Solution 1, if statements in the view:

<% if user.admin? %>
  <%= render 'admin_menu' %>
<% else %>
    <%= render 'user_menu' %>
<% end %>

I am not a fan of if statements in the view. This adds duplication if menu items are shared between admins and users. The if statement can grow unwieldy as soon as we base this off of anything more than one thing. Maybe a third user type sees everything the user sees except for one menu item.

Solution 2, decorators on user:

<%= render current_user.menu %>

# user_decorator.rb
def menu
  user.admin? ? 'admin_menu' : 'user_menu'
end

Gets rid of the if statement in the view, but falls to the same fate as solution 1 as permissions get complex.

Solution 3, prepending to the view_path

<%= render 'menu' %>
# app/controllers/application_controller.rb
before_action :set_view_path

private
def set_view_path
  prepend_view_path('admin') if user.admin?
end

# app/views/application/_menu.html.erb
 User stuff here

# app/views/admin/application/_menu.html.erb
  Admin stuff here

Adds uncommon misdirection and still falls to the same fate as solution 1 as permissions get complex.

Solution 4, Pundit and menu item model

class MenuItem
  attr_reader :name
  def initialize(name)
    @name = name
  end
  
  def to_partial_path
    "menu_items/#{name}"
  end

  def self.all # might be able to dynamically create this based on files
    [ MenuItem.new("stuff"), MenuItem.new("things"), MenuItem.new("admin") ]
  end
end

# Add menu item partials:
# menu_items/_stuff.html.erb, menu_items/_things.html.erb, menu_items/_admin.html.erb

class MenuItemPolicy < Struct.new(:user, :menu_item)
  class Scope
    attr_reader :user, :menu_item

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

    def resolve
      items = MenuItem.all
      items.reject! { |x| x.name == 'admin' } unless user.admin?
      # Other rules that may reject menu items go here
      items
    end
  end
end

# app/controllers/application_controller.rb
def menu_items
  policy_scope(MenuItem)
end
helper_method :menu_items

# app/views/layouts/application.html.erb
<%= render menu_items %>

Removes duplication of view code, keeps all permission logic in one place. Never seen an approach like this which makes me think something is not quite right with it. May not be obvious what is going on? Maybe it’s horrible style?

I am facing this problem in a variety of places in our application where we have to show or hide things based on complex permissions and rules. It is not purely role based security because any number of things could affect why you don’t get access to that UI element. And of course all of this has to be backed up by actual authorization in the controllers as well, but that is solved with Pundit for us.

Does anyone have any better approaches or knowledge that they could lend to me in this arena? Presenters for example, that could solve the menu problem above.

Cheers,

Frank

(I have read this, which has some good content in it: http://hawkins.io/2014/01/delivery_mechanisms_with_sinatra-logic-less_views/)

1 Like

I’ve used something very similar to your approach #4 on project that had a form with complex permissions. Depending on your role, you might see only a subset of the fields and some of the fields might be read-only.

I used a value object that responded to to_partial_path to wrap each field as well as a Pundit policy object to authorize and filter them. This approach worked quite well for encapsulating and isolating all of the permission-based complexity around the form inputs.

Which approach to take depends on the complexity of your permission scheme. If there are only admins and non-admins and there are only a couple view elements that are admin-only then I’d probably go for conditionals in the view. As the complexity of the permission model and instances of branching logic rise, then the other approaches make much more sense.

Strongly agree with @joelq on the complexity of roles. Most of the apps I’ve worked on have only required two levels of permissions, in which case I just augment the User model with an admin boolean flag. Then you can run current_user.admin? to check for admin-level access. Once you get beyond that (even if it’s just one more role beyond basic user and admin), I think that approach should be scrapped for something more robust that is purpose-built for authorization.