Conditional links with active_model_serializers - ruby-on-rails

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

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 - Handle User roles using pundit

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 %>

Rails - Pundit with scopes

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

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

Custom validation using helpers method

I want to make custom validation for Comment model: unregistered users shouldn't use e-mails of registered users, when they submitting comments.
I put custom validator class app/validators/validate_comment_email.rb:
class ValidateCommentEmail < ActiveModel::Validator
def validate(record)
user_emails = User.pluck(:email)
if current_user.nil? && user_emails.include?(record.comment_author_email)
record.errors[:comment_author_email] << 'This e-mail is used by existing user.'
end
end
end
And in my model file app/models/comment.rb:
class Comment < ActiveRecord::Base
include ActiveModel::Validations
validates_with ValidateCommentEmail
...
end
The problem is that I use current_user method from my sessions_helper.rb:
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
Validator can't see this method. I can include sessions_helper in Validator class, but it gives me an error about cookies method. It's a road to nowhere.
So how to make this custom validation rails way?
If the comment knows if it was created by a registered user (belongs_to :user), you can simply check against that:
def validate(record)
if record.user_id.nil? && User.where(:email => record.comment_author_email).exists?
record.errors[:comment_author_email] << 'This e-mail is used by existing user.'
end
end
If not, I think this validation should not be performed using a standard validator. It won't be aware of enough of the context to determine if the model meets this criteria. Instead, you should manually check this by passing the current_user from the controller itself:
# in comments_controller.rb
def create
#comment = Comment.new(params[:comment])
if #comment.validate_email(current_user) && #comment.save
...
end
# in comment.rb
def validate_email(current_user)
if current_user.nil? && User.where(:email => record.comment_author_email).exists?
errors[:comment_author_email] << 'This e-mail is used by existing user.'
end
end

Resources