I have an api-only rails application using active_model_serializers 0.10. I have a current_user attribute in my ApplicationController and am trying to access it from my serializers in order to restrict the data shown. I can do it by passing it to scope manually like this ExerciseSerializer.new(#exercise, scope: current_user), but would like to have a general solution.
This is my ApplicationController:
class ApplicationController < ActionController::API
include Response
include ExceptionHandler
serialization_scope :view_context
# called before every action on controllers
before_action :authorize_request
attr_reader :current_user
def check_access_rights(id)
#current_user.id == id
end
def check_admin_rights
if !#current_user.admin
raise(ExceptionHandler::AuthenticationError, Message.unauthorized)
end
end
private
# Check for valid request token and return user
def authorize_request
#current_user = (AuthorizeApiRequest.new(request.headers).call)[:user]
end
end
This is one of my serializers:
class ExerciseSerializer < ActiveModel::Serializer
attributes :id, :name, :description, :image_url, :note
delegate :current_user, :to => :scope
has_many :exercise_details
end
And this is how I present the objects:
def json_response(object, status = :ok)
render json: object, status: status
end
When serializing I get the following error:
** Module::DelegationError Exception: ExerciseSerializer#current_user delegated to scope.current_user, but scope is nil:
When I try accessing the current_user from within the Serializer, I get the following error:
*** NameError Exception: undefined local variable or method `current_user' for #<ExerciseSerializer:0x007ff15cd2e9c0>
And obviously scope is nil.
Any ideas would be helpful. Thanks!
Found it randomly after countless times unsuccessfully reading the offical docs : https://www.driftingruby.com/episodes/rails-api-active-model-serializer
So here is the interesting part:
def current_user_is_owner
scope == object
end
So current_user is saved in the scope variable by default, you don't need to add code in the controller to retrieve it.
Works in 0.10 and available since 0.08:
https://github.com/rails-api/active_model_serializers/search?utf8=%E2%9C%93&q=scope&type=
Related
I'm building an API with the kollegorna's tutorial.
I used ActiveHashRelation for displays serialiazed arrays properly.
# app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < Api::V1::BaseController
include ActiveHashRelation
def index
posts = apply_filters(Post::Post.all, params)
render json: posts, each_serializer: Api::V1::PostSerializer
end
end
The problem is that I have a model Post::Post and the module Post. It's like it can not found the correct model.
# app/models/post/post.rb
class Post::Post < ActiveRecord::Base
validates_presence_of :body
end
# app/models/post.rb
module Post
def self.table_name_prefix
'post_'
end
end
I think it's because of ActiveHashrelation because when I write posts = Post::Post.all it's working. But I can't filter the array.
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 ?
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
I setup this UserSerializer
class UserSerializer < ActiveModel::Serializer
attributes :id, :first_name, :last_name, :email, :abilities
delegate :current_user, to: :scope
delegate :current_client, to: :scope
def abilities
object.client_roles.where(client_id: current_client.id)
end
end
and this from my ApplicationController
class ApplicationController < ActionController::Base
serialization_scope :view_context
protected
def current_client
....
I put in the "delegate" code based on this railscast around 7:45
He then goes on to say that the downside is that the tests now need a view_context and gives a solution for using Test Unit.
When I run my specs I get one of two errors
Failure/Error: get "show", :id => user.id, :format => :json
NoMethodError:
undefined method `current_client' for #<#:0x00000004f69f60>
or
Failure/Error: data.active_model_serializer.new(data, {:root =>
name}).to_json
RuntimeError:
UserSerializer#current_client delegated to scope.current_client, but scope is nil:
How do I provide the view_context in my controller specs?
Thanks.
OK. My fault. I was calling the UserSerializer manually in my code later to do assertions. That's what was throwing the error. Fixed it just by adding:
before(:each) do
UserSerializer.any_instance.stub(:scope => controller)
end
How do I serialize permissions with active_model_serializers? I don't have access to current_user or the can? method in models and serializers.
First, to get access to the current_user in the serializer context, use the new scope feature:
class ApplicationController < ActionController::Base
...
serialization_scope :current_user
end
In case you are instantiating serializers manually, be sure to pass the scope:
model.active_model_serializer.new(model, scope: serialization_scope)
Then inside the serializer, add custom methods to add your own authorization pseudo-attributes, using scope (the current user) to determine permissions.
If you are using CanCan, you can instantiate your Ability class to access the can? method:
attributes :can_update, :can_delete
def can_update
# `scope` is current_user
Ability.new(scope).can?(:update, object)
end
def can_delete
Ability.new(scope).can?(:delete, object)
end
We created a gem that provides this functionality: https://github.com/GroupTalent/active_model_serializers-cancan
I think you can pass anything you want to serialization_scope so I simply pass the Ability.
class ApplicationController < ActionController::Base
...
serialization_scope :current_ability
def current_ability
#current_ability ||= Ability.new(current_user)
end
end
class CommentSerializer < ActiveModel::Serializer
attributes :id, :content, :created_at, :can_update
def can_update
scope.can?(:update, object)
end
end
I can't do otherwise since my abilities are actually based on two variables (not in the example above).
If you still need access to current_user you can simply set an instance variable on Ability.