Rails - Pundit with scopes - ruby-on-rails

I am trying to figure out how to write pundit permissions in my Rails 4 app.
I have an article model, with an article policy. The article policy has:
class ArticlePolicy < ApplicationPolicy
attr_reader :user, :scope
def initialize(user, record, scope)
#scope = scope
super(user, record)
end
class Scope < Scope
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
end
# TO DO - check if this is correct - I'm not sure.
# I now think I don't need the index 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 article
record
end
The articles controller has:
def index
#articles = policy_scope(Article)
# query = params[:query].presence || "*"
# #articles = Article.search(query)
end
I am following the pundit documents relating to scopes and trying to figure out why the index action shown in the policy documents isn't working for me. I have tried the following (as shown in the docs):
<% policy_scope(#user.articles).sort_by(&:created_at).in_groups_of(2) do |group| %>
but I get this error:
undefined local variable or method `article' for #<ArticlePolicy::Scope:0x007fff08ae9f48>
Can anyone see where I've gone wrong?
I'm not sure that #user.articles is right. In my construct, articles belong to users, but in my index action, I want to show every user the articles that my scopes allow them to see.

You can try this in your action in controller.
#articles = policy_scope(Article).all
It will get all the articles. If you want to get the articles based on search params, you can try this.
#q = policy_scope(Article).search(params[:query])
#articles = #q.result

I think you may need to explicitly set article as an accessor in the Scope class as the error indicates that it doesn't recognise 'article'. Try something like
attr_accessor :article
set it in an initialize method and you can probably do away with the article method.
def initialize(record)
#article = record
end

Related

To test pundit, its necessary to log in at the app first?

I am using pundit gem to authorize in my system app. before to implement pundit I had my endpoint index like this:
def index
#cars = Car.all
render json: #cars
end
worked ok, but now with pundit, i made a change like this
def index
#cars = Car.all
authorize #cars
end
Now I am getting this error:
current_user must be defined before the impersonates method
Pundit works a little differently for the index route. typically you want to do exactly what you where trying authorize #post but we only pass one record.
So for you:
# posts_controller.rb
def index
#posts = policy_scope(Post)
end
this hits the following in pundit:
# app/policies/post_policy.rb
# [...]
class Scope < Scope
def resolve
scope.all # If users can see all posts
# scope.where(user: user) # If users can only see their posts
# scope.where("name LIKE 't%'") # If users can only see posts starting with `t`
# ...
end
end
As for your error related to the current_user, is that coming from pundit? In pundit itself your current_user (from devise for example) will be just user, such that you can check record.user == user.

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.

Conditional links with active_model_serializers

I'm trying to create a hypermedia api in rails. I'd like to serialize my payloads with active_model_serializers using the json_api adapter. But it doesn't seem trivial to serialize links conditionaly.
It's kind of a blog application where users can follow other users. So when I serialize a User resource, say for UserA, I want to have a link with rel :follow if current_user is not following UserA and a link with rel :unfollow if current_user is already following UserA.
This seems like an extremely trivial use case when creating a hypermedia api. Does anyone know if there's any good way of doing this with active_model_serializers?
I currently wrote something like this (and include it in all serializers):
def self.link(rel, &block)
serializer = self
super do
user = scope
next unless serializer.can?(user, rel, #object)
instance_eval(&block)
end
end
# And in serializer (just as usual):
link :self do
api_user_path(object.id)
end
It does work. But it just don't feel right. And I wouldn't be surprised if future changes to active_model_serializers screw things up for me.
If someone else is looking for a solution to this here is what I did. I added the gem Pundit and made the Policy classes in charge of link serialization (as well as the usual authorization) by adding methods called "link_#{rel}". I created a base serializer like this:
module Api
class BaseSerializer < ActiveModel::Serializer
include Pundit
def self.link(rel, &block)
unless block_given?
Rails.logger.warn "Link without block (rel '#{rel}'), no authorization check"
return super
end
method = "link_#{rel}"
# We need to let the super class handle the evaluation since
# we don't have the object here in the class method. This block
# will be evalutated with instance_eval in the adapter (which has
# the object to be serialized)
super do
policy_class = PolicyFinder.new(object).policy
unless policy_class
Rails.logger.warn "Could not find policy class for #{object.class}."
next
end
user = scope
policy = policy_class.new(user, object)
unless policy.respond_to?(method)
Rails.logger.warn "Serialization of #{object.class} infers link with rel '#{rel}'. " \
"But no method '#{method}' in #{policy.class}."
next
end
next unless policy.public_send(method)
instance_eval(&block)
end
end
end
end
Then other serializers inherit from BaseSerializer, like:
module Api
class UserSerializer < BaseSerializer
type 'user'
attributes :name,
:email,
:followers_count,
:following_count,
:created_at,
:updated_at
link :self do
api_user_url(object)
end
link :edit do
api_user_url(object)
end
link :follow do
follow_api_user_url(object)
end
link :unfollow do
unfollow_api_user_url(object)
end
end
end
So the Policies are just like normal Pundit Policies with some added methods for each link that should be serialized (or not).
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def link_self
true
end
end
module Api
class UserPolicy < ApplicationPolicy
alias current_user user
alias user record
def link_edit
current_user && current_user.id == user.id
end
# show follow link if user is not current_user and
# current_user is not already following user
def link_follow
current_user && current_user.id != user.id && !current_user.following?(user)
end
# show follow link if user is not current_user and
# current_user is following user
def link_unfollow
current_user && current_user.id != user.id && current_user.following?(user)
end
end
end

Pundit authorization in index

I have been recently reading through the pundit gem's README and noticed that they never authorize the index view within a controller. (Instead they use scope).
They give good reasoning for this, as an index page generally contains a list of elements, by controlling the list that is generated you effectively control the data on the page. However, occasionally it may be desired to block access to even the index page itself. (Rather than allowing access to a blank index page.) My question is what would be the proper way to perform this?
I have so far come up with several possibilities, and have the following classes:
A model MyModel
A controller MyModelsController
A policy MyModelPolicy
In my index method of my controller, the recommended method to solve this would be as follows:
def index
#my_models = policy_Scope(MyModel)
end
This will then allow access to the index page, but will filter the results to only what that use can see. (E.G. no results for no access.)
However to block access to the index page itself I have arrived at two different possibilities:
def index
#my_models = policy_Scope(MyModel)
authorize #my_models
end
or
def index
#my_models = policy_Scope(MyModel)
authorize MyModel
end
Which of these would be the correct path, or is there a different alternative that would be preferred?
class MyModelPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
raise Pundit::NotAuthorizedError, 'not allowed to view this action'
end
end
end
end
Policy,
class MyModelPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(user: user)
end
end
end
def index?
user.admin?
end
end
Controller,
def index
#my_models = policy_scope(MyModel)
authorize MyModel
end

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

Resources