varvet / pundit

Minimal authorization through OO design and pure Ruby classes
MIT License
8.26k stars 627 forks source link

Use #permitted_attributes with JSON API #622

Closed belgoros closed 4 years ago

belgoros commented 4 years ago

I tried without success to apply permitted_attributes as explained in Strong Parameters section when using with active_model_serializers gem in a Rails 5 API.

As of AMS deserialization section, when parsing params hash:

{"data"=>{"type"=>"posts", "id"=>"1", "attributes"=>{"title"=>"To TDD or Not", "body"=>"TLDR; It's complicated... but check your test coverage regardless."}}, "controller"=>"posts", "action"=>"update", "id"=>"1"}

in a controller as follows:

def post_params
    ActiveModelSerializers::Deserialization.jsonapi_parse!(
      params,
      only: [:title, :body]
    )
end

we'll get the parsed document as:

#=>
# {
#   title: 'To TDD or Not',
#   body: 'TLDR; It's complicated... but check your test coverage regardless.'
# }

It seems like it is not implemented to be used in situations like that or I'm missing something?

belgoros commented 4 years ago

I added pundit_params_for method to the PostsController as follows:

...
private
  def pundit_params_for(_record)
      params.fetch(:data, {}).fetch(:attributes, {})
    end

and called it in the updateaction:

def update
    if @post.update(permitted_attributes(@post))
      render jsonapi: @post
    else
      render jsonapi: @post.errors, status: :unprocessable_entity
    end
  end

It seems to work but no error raised even if I defined the PostPolicy as follows:

class PostPolicy < ApplicationPolicy
  def permitted_attributes
    if user.admin? || user.national?
      [:title, :body]
    else
      [:body]
    end
  end
end

and use rescue in AppplicationController like that:

class ApplicationController < ActionController::API
  include ActionController::MimeResponds
  include Pundit

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

private

  # In some cases your controller might not have access to current_user,
  # or your current_user is not the method that should be invoked by Pundit
  def pundit_user
    User.first
  end

  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

I do have an error in the Terminal:

Unpermitted parameter: :title

but the status code is still 200. The title was not modified in case of a User with role local, only the body value. It seems like NotAuthorizedError is never raised in this case. Am I missing something?

belgoros commented 4 years ago

Figured out the reason. I had to add config.action_controller.action_on_unpermitted_parameters = :raise to the config/application.rb. Then add the rule to rescue_from in the ApplicationController:

rescue_from Pundit::NotAuthorizedError, ActionController::UnpermittedParameters, with: :user_not_authorized

An there was a typo in the status name:

render jsonapi: errors_response, status: :unathorized

should be

render jsonapi: errors_response, status: :unauthorized

Now the response sends the needed 401 status and the error messsage.