How would you authorize indvidual attributes when viewing - ruby-on-rails

Looking to be able to authorize certain users to have the ability to view fields not just have restrictions on the entire object

Trying to help you, as part of the documentation:
With Pundit you can control which attributes a user has access to update via your policies. You can set up a permitted_attributes method in your policy like this:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user.admin? || user.owner_of?(post)
[:title, :body, :tag_list]
else
[:tag_list]
end
end
end
There is also a helper which can control permissions per action
permitted_attributes(record, action = action_name) which can be used instead.
Or, most probaby, you want to use scopes which define access to certain attributes.
From the documentation about scopes:
Often, you will want to have some kind of view listing records which a particular user has access to. When using Pundit, you are expected to define a class called a policy scope. It can look something like this:
class PostPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.admin?
scope.all
else
scope.where(published: true)
end
end
end
def update?
user.admin? or not record.published?
end
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.

Rails - with Pundit Scopes in policy

I am trying to figure out how to use pundit policy scopes in my article policy.
I have written an article policy, that nests a scope and then has a resolve method in it. The resolve method has alternatives based on who the current_user is.
My article policy has:
class ArticlePolicy < ApplicationPolicy
class Scope < Scope
attr_reader :user, :scope
# I now think I don't need these actions because I have changed the action in the articles controller to look for policy scope.
# def index?
# article.state_machine.in_state?(:publish)
# end
def show?
article.state_machine.in_state?(:publish) ||
user == article.user ||
article.state_machine.in_state?(:review) && user.org_approver ||
false
end
end
def create?
article.user.has_role?(:author)
end
def update?
# user && user.article.exists?(article.id) #&& user.article.created_at < 15.minutes.ago
user.present? && user == article.user
# add current state is not published or approved
end
def destroy?
user.present? && user == article.user
# user.admin?
# user.present?
# user && user.article.exists?(article.id)
end
end
private
def article
record
end
def resolve
if user == article.user
scope.where(user_id: user_id)
elsif approval_required?
scope.where(article.state_machine.in_state?(:review)).(user.has_role?(:org_approver))
else
article.state_machine.in_state?(:publish)
end
end
def approval_required?
true if article.user.has_role?(:author)
# elsif article.user.profile.organisation.onboarding.article_approval == true
# if onboarding (currently in another branch) requires org approval
end
def org_approver
if article.user.has_role? :author
user.has_role? :editor
# if onboarding (currently in another branch) requires org approval, then the approval manager for that org
elsif article.user.has_role? :blogger
user.has_role? :editor if user.profile.organisation.id == article.user.profile.organisation.id
end
end
end
The example in the pundit docs shows how to use this for an index, but how do I use the resolve method for a show action? Can I write several resolve methods for the various other controller actions?
Pundit Scopes
I dont have much experience with pundit, however by looking at documentation and your code the code I can see 2 things.
1 - You shouldnt use methods like show? inside your scope class.
inside your scope class, you should use only methods that returns a scope. the methods that returns boolean should be in the Policy level. But in your code I can boolean methods inside the scope class.
Instances of this class respond to the method resolve, which should return some kind of result which can be iterated over. For ActiveRecord classes, this would usually be an ActiveRecord::Relation.
from the docs
2 - Given that Scope are POROs (Plain Old Ruby Object) you can have more than one resolve methods (of course with a different name :)), because resolve is just a method name.
May be you can do something like
#policy
class ArticlePolicy < ApplicationPolicy
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
class Scope < Scope
def resolve
# some scope
end
def resolve_show
#scope for show action
# E.g scope.all
end
end
def show?
article.state_machine.in_state?(:publish) ||
user == article.user ||
article.state_machine.in_state?(:review) && user.org_approver || false
end
end
in your controller
#Articles controller
class ArticlesController < ApplicationController
...
def show
authorize Article
ArticlePolicy::Scope.new(current_user, Article).resolve_show
end
...
end
This should first authorize your show method with ArticlePolicy#show? and the scope from ArticlePolicy::Scope#resolve_show
Disclaimer: Untested code, use at your own risk ;)

rails4 + Pundit model instance not defined in policy

I defines a Pundit policy "CompanyPolicy" as stated in the documentation , the scopez gives the expected results ( on :index ) but I get an exception trying to use the company model instance :
*** NameError Exception: undefined local variable or method `company' for #<CompanyPolicy:
here is the CompanyPolicy.rb
class CompanyPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.system_admin?
scope.all
else
Company.none
end
end
end
def new?
user.system_admin? ? true : false
end
def edit?
user.system_admin? ? true : false
end
def show?
user.system_admin? ? true : false
end
def destroy?
internal_name = Rails.application.secrets.internal_company_short_name
# do not destroy the internal company record
user.system_admin? && (company[:short_name] != internal_name ) ? true : false
end
end
and I check it from the Company controller
def destroy
authorize #company
##company.destroy
....
end
why (company[:short_name] is wrong ?
If I look into the Pundit doc , the example with the PostPolicy , scope and post.published is similar ...
class PostPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.admin?
scope.all
else
scope.where(:published => true)
end
end
end
def update?
user.admin? or not post.published?
end
end
Take a look into documentation:
Pundit makes the following assumptions about this class:
The class has the same name as some kind of model class, only suffixed with the word "Policy".
The first argument is a user. In your controller, Pundit will call the current_user method to retrieve what to send into this
argument
The second argument is some kind of model object, whose authorization you want to check. This does not need to be an
ActiveRecord or even an ActiveModel object, it can be anything
really.
The class implements some kind of query method, in this case update?. Usually, this will map to the name of a particular
controller action.
That's it really.
Usually you'll want to inherit from the application policy created by
the generator, or set up your own base class to inherit from:
class PostPolicy < ApplicationPolicy
def update?
user.admin? or not record.published?
end
end
In the generated ApplicationPolicy, the model object is called record.
just discovered that one should use #record rather than company
( read the a question related to scopes : Implementing scopes in Pundit )
but I don't understand why the Pundit doc does not mention it , and still use a model instance like 'post' for PostPolicy ...
can someone enlighten us ?

Multitenant scoping using Pundit

I'm using Pundit for authorization and I want to make use of its scoping mechanisms for multi-tenancy (driven by hostname).
I've been doing this manually to date by virtue of:
class ApplicationController < ActionController::Base
# Returns a single Client record
def current_client
#current_client ||= Client.by_host(request.host)
end
end
And then in my controllers doing things like:
class PostsController < ApplicationController
def index
#posts = current_client.posts
end
end
Pretty standard fare, really.
I like the simplicity of Pundit's verify_policy_scoped filter for ensuring absolutely every action has been scoped to the correct Client. To me, it really is worthy of a 500 error if scoping has not been officially performed.
Given a Pundit policy scope:
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
# have access to #scope => Post class
# have access to #user => User object or nil
end
end
end
Now, Pundit seems to want me to filter Posts by user, e.g.:
def resolve
scope.where(user_id: user.id)
end
However, in this scenario I actually want to filter by current_client.posts as the default case. I'm not sure how to use Pundit scopes in this situation but my feeling is it needs to look something like:
def resolve
current_client.posts
end
But current_client is naturally not going to be available in the Pundit scope.
One solution could be to pass current_client.posts to policy_scope:
def index
#posts = policy_scope(current_client.posts)
end
But I feel this decentralizes my tenancy scoping destroys the purpose of using Pundit for this task.
Any ideas? Or am I driving Pundit beyond what it was designed for?
The most "Pundit-complient" way to deal with this problem would be to create a scope in your Post model:
Class Post < ActiveRecord::Base
scope :from_user, -> (user) do
user.posts
end
end
Then, you will be able to use it in your policy, where user is filled with the current_user from your controller:
class PostPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope.from_user(user)
end
end
end
If you are returning an ActiveRecord::Relation from the scope, you can stop reading from here.
If your scope returns an array
The default ApplicationPolicy implement the method show using a where:
source.
So if your scope does not return an AR::Relation but an array, one work-around could be to override this show method:
class PostPolicy < ApplicationPolicy
class Scope
# same content than above
end
def show?
post = scope.find do |post_in_scope|
post_in_scope.id == post.id
end
post.present?
end
end
Whatever your implementation is, you just need to use the PostPolicy from your controller the "Pundit-way":
class PostsController < ApplicationController
def index
#posts = policy_scope(Post)
end
def show
#post = Post.find(params[:id])
authorize #post
end
end

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

Resources