This section of Pundit section says that we could control which attributes are authorized to be updated. But it fails in case of the use of active_model_seriallizers gem:
def post_params
# originally geneated by scaffold
#params.require(:post).permit(:title, :body, :user_id)
#To deserialize with active_model_serializers
ActiveModelSerializers::Deserialization.jsonapi_parse!(
params,
only: [:title, :body, :user]
)
end
If I modify the PostsController update action as Pundit suggested:
def update
if #post.update(permitted_attributes(#post))
render jsonapi: #post
else
render jsonapi: #post.errors, status: :unprocessable_entity
end
end
it fails with error:
ActionController::ParameterMissing (param is missing or the value is empty: post):
app/controllers/posts_controller.rb:29:in `update'
I also create the PostPolicy as follows:
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user.admin? || user.national?
[:title, :body]
else
[:body]
end
end
end
but it has no impact on the above error.
Any idea on how can we do that?
The solution I came to (thanks to #max for some tips and tricks) is as follows:
Add the following line to config/application.rb:
config.action_controller.action_on_unpermitted_parameters = :raise
Add the rescue_from either to the AplicationController or the one you are precisely interested:
class ApplicationController < ActionController::API
include ActionController::MimeResponds
include Pundit
rescue_from Pundit::NotAuthorizedError, ActionController::UnpermittedParameters, with: :user_not_authorized
...
private
def user_not_authorized
render jsonapi: errors_response, status: :unathorized
end
def errors_response
{
errors:
[
{ message: 'You are not authorized to perform this action.' }
]
}
end
end
Then add pundit_params_for method to the PostsController and change the update action (in my case I'd like to restrict some attributes in update action only:)
class PostsController < ApplicationController
...
def update
if #post.update(permitted_attributes(#post))
render jsonapi: #post
else
render jsonapi: #post.errors, status: :unprocessable_entity
end
end
private
def post_params
ActiveModelSerializers::Deserialization.jsonapi_parse!(
params,
only: [:title, :body, :user]
)
end
def pundit_params_for(_record)
params.fetch(:data, {}).fetch(:attributes, {})
end
end
VoilĂ . Now if an unpermitted attribute will be submitted for the update action, the response will have 500 status and contain the error as specified in ApplicationController#errors_response method.
ATTENTION: It still fails if you have some relations posted with the request (for example, you can have an Author as belongs_to relation with Post). Using pundit_params_for as before will fail to extract the corresponding author_id value. To see the way, here my another post where I explained how to use it.
Hope this helps.
Related
I am working on rails and trying to make a simple blog site and its working the way i want to on my local machine but when pushed to production its being blocked by the callback functions.
My before_action :authorized_user? callback is being called and it prompts for logging if not logged in for performing any method on the blog , and if logged in all methods create, update and destroy methods are working perfectly in my development environment but in production even after the user is logged in also and when the create method is being called it asks for to log in . I am unable to understand from where or what code is causing this to happen because the same is working perfectly fine on local machine.
Any help will he highly appreciated.
My blog_controller.rb file is
class BlogsController < ApplicationController
before_action :set_blog, only: [:show, :update, :destroy, :lock_blog, :pin_blog]
before_action :authorized_user?, except: [:index, :show]
def index
#blogs = Blog.all
render json: { blogs: #blogs },status: :ok
end
def show
comments = #blog.comments.select("comments.*, users.username").joins(:user).by_created_at
render status: :ok, json: { blog: #blog, blog_creator: #blog.user, comments: comments }
end
def create
#blog = Blog.new(blog_params.merge(user_id: #current_user.id))
if authorized?
if #blog.save
render status: :ok,
json: {blog: #blog , notice: "Blog Successfully created"}
else
errors = #blog.errors.full_messages.to_sentence
render status: :unprocessable_entity, json: {error:errors}
end
end
end
def update
if authorized?
if #blog.update(blog_params)
render status: :ok,
json: {blog: #blog, notice:"Blog successfully updated"}
else
render status: :unprocessable_entity,
json: {errors: #blog.errors.full_messages.to_sentence}
end
else
handle_unauthorized
end
end
def destroy
if authorized?
if #blog.destroy
render status: :ok,
json: {notice:'Blog deleted'}
else
render status: :unprocessable_entity,
json: {errors: #blog.errors.full_messages.to_sentence}
end
else
handle_unauthorized
end
end
private
def set_blog
#blog = Blog.find(params[:id])
end
def blog_params
params.require(:blog).permit(:title,:body,:image,:is_pinned, :is_locked)
end
def authorized?
#blog.user_id == #current_user.id || #current_user.admin_level >= 1
end
def handle_unauthorized
unless authorized?
render json:{notice:"Not authorized to perform this task"}, status:401
end
end
end
and application_controller.rb file is
class ApplicationController < ActionController::Base
skip_before_action :verify_authenticity_token
include CurrentUserConcern
include ExceptionHandlerConcern
include TokenGenerator
def authorized_user?
render json: { notice: 'Please log in to continue' }, status: :unauthorized unless #current_user
end
def authorized_admin?
authorized_user?
render json: {errors: 'Insufficient Administrative Rights'}, status: 401
end
private
end
current_user_concern.rb file
module CurrentUserConcern
extend ActiveSupport::Concern
included do
before_action :set_current_user
end
def set_current_user
if session[:token]
#current_user = User.find_by(token: session[:token])
end
end
end
Its generally recommended to use libraries for authentication and authorization instead of reinventing the wheel unless its for learning purposes. They have many eyes looking for bugs and insecurites and are battle hardened by tons of users. Home-rolled authentication systems are a very common source of security breaches which could lead to very expensive consequences.
If you're going to roll your own authorization and authentication solution I would suggest you take a page from the libraries like Devise, Pundit and CanCanCan and raise an error when a user is not authorized or authenticated so that you immediately halt whatever the controller is doing and stop the callback chain from executing further.
# app/errors/authentication_error.rb
class AuthenticationError < StandardError; end
# app/errors/authorization_error.rb
class AuthorizationError < StandardError; end
# app/controllers/concerns/
module Authenticable
extend ActiveSupport::Concern
included do
helper_method :current_user, :user_signed_in?
before_action :authenticate_user
rescue_from AuthenticationError, with: :handle_unauthorized
end
def current_user
#current_user ||= find_user_from_token if session[:token].present?
end
def find_user_from_token
User.find_by(token: session[:token])
end
def user_signed_in?
current_user.present?
end
def authenticate_user
raise AuthenticationError.new('Please log in to continue') unless user_signed_in?
end
def handle_unauthenticated(error)
render json: {
notice: error.message
},
status: :unauthorized
end
end
end
# app/controllers/concerns/authorizable.rb
module Authorizable
extend ActiveSupport::Concern
included do
rescue_from AuthenticationError, with: :handle_unauthorized
end
def authorize_admin
raise UserAuthenticationError.new('Insufficient Administrative Rights') unless current_user.admin?
end
def handle_unauthorized(error)
render json:{
notice: error.message
}, status: :unauthorized
end
end
class ApplicationController < ActionController::Base
skip_before_action :verify_authenticity_token
include Authenticable
include Authorizable
# Should you really be mixing this into the controller? Seperate the responsibilites!
include TokenGenerator
end
It also makes debugging much easier as you can disable rescue_from in testing so that you get an exception instead of just a cryptic failure message.
You should also setup your authorization system so that it always authenticates (and authorizes) unless you explicitly opt out. This is a best practice that reduces the possible of security breaches simply due to programmer omission. You opt out by calling skip_before_action :authorize_user.
Instead of your set_current_user use a memoized getter method (current_user) to remove issues caused by the ordering of callbacks. ||= is conditional assignment and will prevent it from querying the database again if you have already fetched the user. This should be the ONLY method in the system that knows how the user is stored. Do not access #current_user directly to avoid leaking the implementation details into the rest of the application.
Methods ending with ? are by convention predicate methods in Ruby and should be expected to return a boolean. Name your modules by what their responsibility is and not what code they contain - avoid the postfix Concern as it tells you nothing about what it does.
I am working on a project where I want ensure that if request does not have user params it should send error, so I used following method in controller
class UsersController < ApplicationController
before_action :check_params
private
def user_params
params.require(:user).permit(:email, :password)
end
def check_params
render json: { error: 'No user params provided' }, status: 401 unless params[:user]
end
end
It is working fine but I have to put on each individual controller, is there way I can add it in Application for all controllers! as only params[:user] changing, so if I have params[:company] I have to add another method in CompaniesController with params[:company] which is not really dry. I am surprise when we use params.require(:user) why it gives errors instead return validation error.
I look like you want to rescue from ActionController::ParameterMissing with a custom error message. The Rails way to do that is the rescue_from method.
Add the following to your controller:
rescue_from 'ActionController::ParameterMissing' do |exception|
render json: { error: 'No user params provided' }, status: 401
end
Well instead of creating this whole method you can do just check before where your action is checking params[:user] and reject on single line like following
#user = User.find_by_email(params[:user][:email]) if params[:user]
Now it won't search for your params[:user]
You can move common logic into a method in the base class, then derived classes can pass own "changeable" variables to it or ignore a method
class ApplicationController
def check_params(name, error_message)
render json: { error: error_message }, status: 400 if params[name].nil?
end
end
Usage
class UsersController < ApplicationController
before_action -> { check_params(:user, "User is missing") }
end
I had a model that should be rendered as JSON, for that I used a serializer
class UserSerializer
def initialize(user)
#user=user
end
def to_serialized_json
options ={
only: [:username, :id]
}
#user.to_json(options)
end
end
when I render json: I want though to add a JWT token and an :errors. Unfortunately I am having an hard time to understand how to add attributes to the serializer above. The following code doesn't work:
def create
#user = User.create(params.permit(:username, :password))
#token = encode_token(user_id: #user.id) if #user
render json: UserSerializer.new(#user).to_serialized_json, token: #token, errors: #user.errors.messages
end
this code only renders => "{\"id\":null,\"username\":\"\"}", how can I add the attributes token: and errors: so to render something like this but still using the serializer:
{\"id\":\"1\",\"username\":\"name\", \"token\":\"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.7NrXg388OF4nBKLWgg2tdQHsr3HaIeZoXYPisTTk-48\", \"errors\":{}}
I coudl use
render json: {username: #user.username, id: #user.id, token: #token, errors: #user.errors.messages}
but how to obtain teh same by using the serializer?
class UserSerializer
def initialize(user)
#user=user
end
def to_serialized_json(*additional_fields)
options ={
only: [:username, :id, *additional_fields]
}
#user.to_json(options)
end
end
each time you want to add new more fields to be serialized, you can do something like UserSerializer.new(#user).to_serialized_json(:token, :errors)
if left empty, it will use the default field :id, :username
if you want the json added to be customizable
class UserSerializer
def initialize(user)
#user=user
end
def to_serialized_json(**additional_hash)
options ={
only: [:username, :id]
}
#user.as_json(options).merge(additional_hash)
end
end
UserSerializer.new(#user).to_serialized_json(token: #token, errors: #user.error.messages)
if left empty, it will still behaves like the original class you posted
Change to_json to as_json, and merge new key-value.
class UserSerializer
def initialize(user, token)
#user=user
#token=token
end
def to_serialized_json
options ={
only: [:username, :id]
}
#user.as_json(options).merge(token: #token, error: #user.errors.messages)
end
end
i prefer to use some serialization gem to handle the serialize process like
jsonapi-serializer
https://github.com/jsonapi-serializer/jsonapi-serializer
or etc
So I am working on creating a playercard, which is basically a profile page for a user. The issue I am having on the backend is my private method playercard_params is only returning user_id, and not all the information inputted into the form...although regular params shows all the data needed to create the playercard. I thought the issue might be on the frontend, but working my way backwards came to the conclusion the issue is here on the backend.
Here is my controller:
class Api::V1::PlayercardController < ApplicationController
before_action :set_user
def index
if params[:user_id]
#playercard = #user.playercard
else
#playercard = Playercard.all
end
render json: #playercard
end
def show
#playercard = Playercard.find(params[:id])
render json: #playercard
end
def create
#playercard = Playercard.new(playercard_params)
binding.pry
if #playercard.save
render json: #user
else
render json: {
error: #playercard.errors.full_messages.to_sentence
}
end
end
def update
#playercard = Playercard.find(params[:id])
if #playercard.update(playercard_params)
render json: #playercard
else
render json: {
error: #playercard.errors.full_messages.to_sentence
}
end
end
private
def playercard_params
params.require(:playercard).permit(:player_nickname, :player_height_in_feet, :player_height_in_inches, :player_weight, :player_age, :player_fav_player, :user_id)
end
def set_user
#user = User.find(params[:user_id])
end
end
My playercard model:
class Playercard < ApplicationRecord
belongs_to :user
validates :player_nickname, :player_height_in_feet, :player_height_in_inches, :player_weight, :player_age, :player_fav_player, presence: true
end
and the serializer if that helps:
class PlayercardSerializer < ActiveModel::Serializer
attributes :id, :player_nickname, :player_height_in_feet, :player_height_in_inches, :player_weight, :player_age, :player_fav_player
belongs_to :user
end
Here are my params:
<ActionController::Parameters {"playerNickname"=>"white mamba", "playerHeightFeet"=>"6", "playerHeightInches"=>"3", "playerAge"=>"30", "playerWeight"=>"170", "playerFavPlayer"=>"Kobe", "user_id"=>"1", "controller"=>"api/v1/playercard", "action"=>"create", "playercard"=><ActionController::Parameters {"user_id"=>1} permitted: false>} permitted: false>
When I submit the form on the front end, I get errors saying each field is empty...in the pry, if I type playercard_params, only user_id shows up (with the correct id)
I solved the issue by lining up the naming convention for the attributes with the front-end and back-end. And it worked!
Thank you #jvillian for the insight!!
I have used the Pundit Gem before, but I've never tried doing what I'm trying to do now, and for some reason Pundit is not happy.
What I'm aiming to do, is to have a modal with the 'create' (Foo) form on my 'index'(Foos) page. Thus I need to instantiate an empty Foo object for the modal form to work.
The issue that I'm experiencing, is that Pundit throws an error when I submit the form remotely. The error is:
Pundit::NotDefinedError - unable to find policy of nil
I have tried to understand why this is happening but I've not been able to solve it yet.
Here is my foos_controller.rb#index:
...
def index
#foo = Foo.new
authorize #foo, :new?
#foos = policy_scope(Foo)
end
...
I then have the following 'before_action' filter that runs for my other actions i.e. 'create'
...
before_action :run_authorisation_check, except: [:index]
def run_authorisation_check
authorize #foo
end
...
The policies that I'm using in foo_policy.rb:
....
def index?
user.has_any_role? :super_admin
end
def create?
user.has_any_role? :super_admin
end
def new?
create?
end
def scope
Pundit.policy_scope!(user, record.class)
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.has_any_role? :super_admin
scope.all
end
end
end
....
The error does not present itself until I submit the form. Could anybody familiar with Pundit please help guide me to understand what I'm doing incorrectly?
UPDATE
Full foos_controller.rb
class FoosController < ApplicationController
def index
#foo = Foo.new
authorize #foo, :create?
#foos = policy_scope(Foo)
end
def new
#foo = Foo.new
end
def create
#foo = Foo.new(foo_params)
respond_to do |format|
if #foo.save
flash[:notice] = I18n.t("foo.flash.created")
format.json { render json: #foo, status: :ok }
else
format.json { render json: #foo.errors, status: :unprocessable_entity }
end
end
end
private
before_action :run_authorisation_check, except: [:index]
def foo_params
params.fetch(:foo, {}).permit(:bar)
end
def run_authorisation_check
authorize #foo
end
end
Yeah, you're not setting the value of #foo, that's why you're getting the error unable to find policy of nil.
Most times, you would have something like this in your foos_controller.rb:
before_action :set_foo, only: [:show, :edit, :update, :destroy]
before_action :run_authorisation_check, except: [:index]
...
private
def set_foo
#foo = Foo.find(params[:id])
end
Let me know if that works
I had this issue when working on a Rails 6 API only application with the Pundit gem.
I was running into the error below when I test my Pundit authorization for my controller actions:
Pundit::NotDefinedError - unable to find policy of nil
Here's how I solved:
Say I have a policy called SchoolPolicy:
class SchoolPolicy < ApplicationPolicy
attr_reader :user, :school
def initialize(user, school)
#user = user
#school = school
end
def index?
user.admin?
end
def show?
user.admin?
end
def new
create?
end
def edit
update?
end
def create
user.admin?
end
def update?
user.admin?
end
def destroy?
user.admin?
end
end
Then in my SchoolsController, I will have the following:
class Api::V1::SchoolsController < ApplicationController
before_action :set_school, only: [:show, :update, :destroy]
after_action :verify_authorized, except: :show
# GET /schools
def index
#schools = School.all
authorize #schools
render json: SchoolSerializer.new(#schools).serializable_hash.to_json
end
# GET /schools/1
def show
render json: SchoolSerializer.new(#school).serializable_hash.to_json
end
# POST /schools
def create
#school = School.new(school_params)
authorize #school
if #school.save
render json: SchoolSerializer.new(#school).serializable_hash.to_json, status: :created, location: api_v1_school_url(#school)
else
render json: #school.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /schools/1
def update
authorize #school
if #school.update(school_params)
render json: SchoolSerializer.new(#school).serializable_hash.to_json
else
render json: #school.errors, status: :unprocessable_entity
end
end
# DELETE /schools/1
def destroy
authorize #school
#school.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_school
#school = School.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def school_params
params.require(:school).permit(:name, :alias, :code)
end
end
Note:
I used an after_action callback to call the verify_authorized method to enforce authorization for the controller actions
I did not call the authorize method on the show action because it was skipped for authorization by me out of choice based on my design.
The instance variables called by the authorize method corresponds to the instance variable of the controller actions being called. So for the index action it is #schools and for the create action it is #school and so on.
That's all.
I hope this helps