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/)