Assume we have the following code (We are using pundit for authorization and Active model serializer to format json response)
class User has_many :projects end class Project end class ProjectPolicy < ApplicationPolicy attr_accessor :user, :project def initialize(user, project) @user = user @project = project end def show? user.member_of?(project) end def destroy? user.admin_of?(project) end end class ProjectsController def show authorize @project render json: @project end def destroy authorize @project @project.destroy head :no_content end end class ProjectSerializer attributes :name, :descroption endWhen we request a project resource, we will get:
# http://myapp/projects/1 { "project": { "id": 1, "name": "My project", "description": "My project description" } }When a user tries to do any action on a project, we first authorize this user to make sure he can do this action on this project, everything seems fine.
But what about client side validation? If the mobile app or javascript code in the browser consuming this api wants to do client side validation for better user experience and to save the backend server from useless requests, then we have to return the policy with the json response like this.
class ProjectSerializer attributes :name, :descroption, :policy def policy project_policy = ProjectPolicy.new(scope.current_user, self.object) { show: project_policy.show?, destroy: project_policy.destroy?, } end endAnd in case you don’t already do this, add this line to application controller, such that we can use
methods available in rails html views (like current_user, which will be available within scope)
serialization_scope :view_context # in application_controller.rb # http://myapp/projects/1 { "project": { "id": 1, "name": "My project", "description": "My project description", "policy": { "show": true, "destroy": false } }But then we have to remember that whenever we add a policy method to our policy class and use that method in our controller, we have then to add that method to the json response, so that the API consumer is aware of this policy.
The problem is that we may easily forget to do this, and new developers contributing to our code will most likely forget, as this is not an intuitive action to do, new developer must be told about it and they must remember it! A sign of a source of bugs.
A simple solution I used is to use ruby metaprogramming to add the whole dependency thing to the API response, here is the black magic code (places in an initializer):
ActiveModel::Serializer.class_eval do def self.add_policies attribute :policy def policy policy_instance = scope.policy(self.object) policy_instance.as_json end end endAnd override as_json in ApplicationPolicy so that it returns only public methods ending with question mark (which indicates they are policy methods)
ApplicationPolicy def as_json policies = {} public_methods(false).select{ |method| method.to_s =~ /\?$/ }.each do |policy_method| can_perform_policy_method = send(policy_method) policies["#{policy_method.to_s.sub(/\?$/, '')}"] = can_perform_policy_method end policies end endThen in ProjectSerializer, we just call add_policies
class ProjectSerializer add_policies attributes :name, :descroption endAnd we have all our policy methods automatically added to to the project json object .