Rails - Handle User roles using pundit - ruby-on-rails

I have a table of users with enum user_type [Manager, Developer, QA]. Currently, I'm handling sign in using Devise and after login I'm using the following logic to display the appropriate webpage:
class HomeController < ApplicationController
before_action :authenticate_user!
def index
if current_user.manager?
redirect_to manager_path(current_user.id)
end
if current_user.developer?
redirect_to developer_path(current_user.id)
end
if current_user.quality_assurance?
redirect_to qa_path(current_user.id)
end
end
end
I want to use pundit gem to handle this. From the documentation, it transpired that this logic will be delegated to policies but I can't figure out how. Can somebody help me in implementing pundit in my project?
This is my users table:
I have created a user_policy but its mostly empty:
class UserPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
end
User model:

You want to use Pundit to authorize a user, as in check if that user should be allowed to visit a controller action. If the user is not authorized for a specific action it raises a Pundit::NotAuthorizedError
You can check if a user is allowed to perform an action in the pundit policy, in which you have access to record (the instance thats passed to authorize) and user. So assuming you have a Flat Model, where only the owner can edit the Flat you might do this:
# flats_policy.rb
def edit?
record.user == user
end
Now lets say you also want to allow admins to edit you might do this
# flats_policy.rb
def owner_or_admin?
record.user == user || user.admin # where admin is a boolean
end
def edit?
owner_or_admin?
end
and the controller:
# flats_controller.rb
def edit
#flat = Flat.find(params[:id])
authorize #flat
# other code here
end
Now the index action is the odd one out because you would essentially have to call authorize on each instance, so the way Pundit handles this is with the Scope:
# flats_policy.rb
class Scope < Scope
def resolve
scope.all
end
end
and a corresponding index action might look like:
def index
#flats = policy_scope(Flat) # note that we call the model here
end
So lets say a user can only see flats that he/she owns:
# flats_policy.rb
class Scope < Scope
def resolve
scope.where(user: user)
end
end
and if admins can see all flats:
# flats_policy.rb
class Scope < Scope
def resolve
if user.admin
scope.all
else
scope.where(user: user)
end
end
end
In any case if the user is not allowed to perform an action you can rescue from the error like so:
# application_controller
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_to(root_path)
end
I guess you could do some dirty redirecting here, as in send admins to an admins_root_path, users to a default_root_path and so on...
On a final note, since this post is already too long you can check a policy in the view like this:
<% if policy(restaurant).edit? %>
You can see me if you have edit rights
<% end %>

Related

Why does a Pundit policy for one controller is affected by another?

I am perhaps misunderstanding Pundit policies but I am facing an issue where the UserPolicy is clashing with the SongPolicy.
What happens if that a statement in UserPolicy is being asserted ignoring what's written in SongPolicy:
Pundit::NotAuthorizedError in SongsController#edit
not allowed to edit? this User
def authorization
authorize Current.user
end
The issue emerged after introducing a new role for users but I believe that I probably haven't configured it right and for some reason only UserPolicy is looked at for asserting authorization in the SongsController?
I have two controllers that check for the user to be signed in (require_user_logged_in) and another to check on Pundit's policies (authorization):
class UsersController < ApplicationController
before_action :require_user_logged_in!, :authorization, :turbo_frame_check
# Actions were removed for brevity.
end
class SongsController < ApplicationController
before_action :require_user_logged_in!, :authorization, except: [:index, :show]
# Actions were removed for brevity.
end
The authorization methods looks like this:
def authorization
authorize Current.user
end
There's an application-level policy class, ApplicationPolicy:
# frozen_string_literal: true
class ApplicationPolicy
attr_reader :user, :params, :record
# Allows params to be part of policies.
def initialize(context, record)
if context.is_a?(Hash)
#user = context[:user]
#params = context[:params]
else
#user = context
#params = {}
end
#record = record
end
def index?
false
end
def show?
false
end
def create?
false
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
class Scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
raise NotImplementedError, "You must define #resolve in #{self.class}"
end
private
attr_reader :user, :scope
end
end
The UserPolicy to protect user views:
class UserPolicy < ApplicationPolicy
class Scope < Scope
end
def index?
user.has_role?(:admin)
end
def show?
# Access if admin or the same user only.
user.has_role?(:admin) || is_same_user?
end
def create?
index?
end
def new?
create?
end
def update?
index? || is_same_user?
end
def edit?
update? # This is called when accessing a view for `SongsController`.
end
def destroy?
index? || is_same_user?
end
def delete?
destroy?
end
private
# Used to keep a user from editing another.
# Admins should be allowed to edit all users.
def is_same_user?
# Check if user being accessed is the one being logged in.
params[:id].to_s == Current.user.username.to_s
end
end
And the SongPolicy:
class SongPolicy < ApplicationPolicy
class Scope < Scope
end
def index?
end
def show?
end
def create?
user.has_role?(:admin) || user.has_role?(:collaborator) # This is ignored.
end
def new?
create?
end
def update?
create?
end
def edit?
create?
end
def destroy?
user.has_role?(:admin)
end
def delete?
destroy?
end
end
Not sure what else to try here, I'm sure I'm missing something, if someone with more knowledge of Pundit could let me know their thoughts on why a statement for one policy can leak into another, it would be really helpful.
You're calling authorize on the current user, which is a User, so Pundit is going to infer the UserPolicy policy. It won't automatically infer the SongPolicy policy unless you provide a Song record, even if you're in the SongController controller.
If you want to use a different policy, you'll need to provide it via authorize(policy_class:).
authorize Current.user, policy_class: SongPolicy
Implicit authorization like this is generally a code smell. Ideally, you should be explicitly authorizing the current Song record(s) against the current user context.
My opinion is that you have a misconception on Pundit's approach. Particularly you're twisting the subject of the authorization with the object of the authorization. Let's try to explain.
You have an actor who wants to apply an action on an object. The object may be authorized to receive the action.
By default Pundit's always consider the actor to be the current_user.
The action is a method on a Policy.
The object is the resource you're working on; in the most trivial scenario it could be an ActiveRecord object - but it doesn't have to.
Pundit's authorize methods is intended, in plain english, as "authorize the the resource bar to receive the action foo from the current user".
What you're trying to do is instead "authorize the current user to apply the action foo on the resource bar.
What's the difference? The subject and the object of the authorization are swapped. IMO, while doing the authorization process, you should respond to the question: "Is this object authorized to receive this action by the actor?"
object action
------------ ------
authorize Current.user, :edit?
NOTE: the actor implicitly is current_user
NOTE: if action is not declared, then it will implicitly be action_name
which resolves to the question "is this specific user authorized to receive :edit? from the current user?"
Following the reasoning, this is what I'd consider the right approach for your example scenario:
class SongsController < ApplicationController
before_action :require_user_logged_in!, :authorization, except: [:index, :show]
private
def authorization
authorize Song
end
end
I do not advise to rely on callbacks and I'd rather write more explicit code
def edit
#song = Song.find(params[:id])
authorize #song, :edit?
end
This code resolves to the question "is this specific song authorized to receive :edit? from the current user?"
A word of warning about using a custom policy_class
like in
authorize Current.user, policy_class: SongPolicy
With this code the authorization will be made by calling SongPolicy#edit? but the record will regularly be set to Current.user's value ; let's suppose to have
class SongPolicy
def edit?
record.in_my_playlist?
end
end
where in_my_playlist? is Song's instance method: you'll end having
undefined method `in_my_playlist?` for #<User>
Probably you're not doing the thing you intended to do there.
A word of warning about the use of Current.user into your logic
If Current.user is using http://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html and your entire application is relying on that singleton, then you probably want to redefine Pundit's default user as documented here
class ApplicationController < ActionController::Base
def pundit_user
Current.user
end
end
otherwise you'll end up having your business logic and your authorization logic relying on two - potentially - different sources of truth.

Pundit Gem Index Page Prevent Access

I'm using the pundit gem and trying to figure out how to use it to prevent access to an index page that belongs to a user other than the current_user.
The examples only talk about how to scope the results to the current_user but no how to actually prevent access to the page itself if the current_user is NOT the owner of the record.
Any help appreciated
Thanks
Maybe you want something like this? (For class ModelName)
# /policies/model_name_policy.rb
class ModelNamePolicy
attr_reader :current_user, :resource
def initialize(current_user, resource)
#current_user = current_user
#resource = resource
end
def index?
current_user.authorized_to_edit?(resource)
end
end
# /models/user.rb
class User < ActiveRecord::Base
def authorized_to_edit?(resource)
admin? | (id == resource.created_by) # Or whatever method you want to call on your model to determine ownership
end
end
EDIT: Note that you will also need to call authorize from your controller to invoke the policy.

Restrict view for various roles in pundit

I am following up from a problem that I had before. I was able to get the code to work for three roles, but I need to include 4 roles in the mix.
The problem: I have 4 roles (user, business user, super user, and admin). Admins have access to everything (user index). Super users can only see both users and business users (user index).
The error: I have a functioning app that allows admins to have access to everything, but my super users can only see users (and not business users). I tried switching in the User Policy resolve method, for the super user to role: 'business_user' to see if that even worked. Well, it does not work and it only shows me users (not business_users). It's probably a simple ruby issue that I'm overlooking.
User Policy
class UserPolicy
attr_reader :current_user, :model
def initialize(current_user, model)
#current_user = current_user
#user = model
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.admin?
scope.all
else user.super_user?
scope.where(role: 'user')
end
end
end
def index?
#current_user.admin? or #current_user.super_user?
end
end
User Controller
class UsersController < ApplicationController
before_filter :authenticate_user!
after_action :verify_authorized
def index
#users = policy_scope(User)
authorize #users
end
[rest of the controller]
User Model
class User < ActiveRecord::Base
enum role: [:user, :business_user, :super_user, :admin]
[rest of model]
end
Can you try to change this method :
def resolve
if user.admin?
scope.all
else user.super_user?
scope.where(role: 'user')
end
end
By this :
def resolve
if user.admin?
scope.all
else user.super_user?
scope.where('role == "user" or role == "business_user"').all
end
end
You have to change your query to have both roles.
I figured out what I had to do. It was a two step process. First, I had to change the role to the numerical value that pundit stores it as instead of the string, so the role would be 0 & 1. Second, I used an array to feed them into the param so it would accept multiple options.
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.admin?
scope.all
elsif user.super_user?
scope.where(role: [1,0])
else
scope.none
end
end
end

Pundit: undefined method `authorize'

I am trying to use Pundit to authenticate access to some static views that require no database interaction:
class StaticController < ApplicationController
include Pundit
authorize :splash, :home?
def home end
end
Below is my static policy. The home? policy always returns true, so I should be able to access the home view.
class StaticPolicy < Struct.new(:user, :static)
def initialize(user, resource)
#user = user
#resource = resource
end
def home?
true
end
end
Instead I get this:
undefined method `authorize' for StaticController:Class
Pundit works perfectly if I'm authorizing a model:
def forums_index
#forums = Forum.all
authorize #forums
end
However, if I try to use the authorize method outside of an action that doesn't make use of a model I get:
undefined method `authorize' for StaticController:Class
Well, AFAIK you'll always have to authorize against either an object or a class, while CanCan already "load_and_authorize_resource", when using Pundit you already know that you have to load and authorize something yourself (sorry if I'm being too obvious here).
That said and considering that your view doesn't have DB interation, it seems to me that the best solution for your case is make some custom authorization against your user, something like
class StaticPolicy < Struct.new(:user, :static)
def initialize(user, resource)
#user = user
#resource = resource
end
def home?
authorize #user, :admin # or suppress the second parameter and let the Policy use the 'home?' method
true
end
end
and in your UserPolicy something like
class UserPolicy < ApplicationPolicy
def admin # or def home?, it's up to you
user.admin?
end
end
I didn't test it, but that's the main idea, does it make any sense? Is it clear?
Please give it a try and post any impressions, hope it helps :)

Wrapping within filter

Using Rails 3.2. I am trying to secure my app by checking user permission on all crud actions. Here is one of the examples:
class StatesController < ApplicationController
def create
#country = Country.find(params[:country_id])
if can_edit(#country)
#state.save
else
redirect_to country_path
end
end
def destroy
#country = Country.find(params[:country_id])
if can_edit(#country)
#state = State.find(params[:id])
#state.destroy
else
redirect_to country_path
end
end
end
class ApplicationController < ActionController::Base
def is_owner?(object)
current_user == object.user
end
def can_edit?(object)
if logged_in?
is_owner?(object) || current_user.admin?
end
end
end
I notice that I have been wrapping can_edit? in many controllers. Is there a DRYer way to do this?
Note: All must include an object to check if the logged in user is the owner.
You are looking for an authorization library. You should look at CanCan which allows you to set certain rules about which objects can be accessed by particular users or groups of users.
It has a method, load_and_authorize_resource, which you can call in a given controller. So your statues controller would look like:
class StatesController < ApplicationController
load_and_authorize_resource :country
def create
#state.save
end
def destroy
#state = State.find(params[:id])
#state.destroy
end
end
CanCan will first load the country and determine whether or not the user has the right to view this resource. If not, it will raise an error (which you can rescue in that controller or ApplicationController to return an appropriate response".) You can then access #country in any controller action knowing that the user has the right to do so.

Resources