Graphql-ruby: Apply pundit_role to an argument that is part of an InputObject

Created on 6 Sep 2019  路  10Comments  路  Source: rmosolgo/graphql-ruby

Is it possible to apply a pundit_role to an argument that is part of an InputObject?

class Inputs::PersonInput < Types::BaseInputObject

  argument :is_cool, Boolean, required: false, 
    pundit_role: :already_very_cool

end

I have other arguments working spectacularly with the pundit_role directive but not inside of InputObject. I am thinking that if it is, somehow the mutation would have to pass along information about the Type or the Policy in order for this to work ... if it's able to work at all.

Most helpful comment

Thanks for the details on that, @thornomad . It required a bigger shift, actually a breaking change to graphql-ruby. Previously, argument values weren't included at all in the authorization system. Now, they're passed down as arguments to .authorized? methods, which was a breaking change. I've put this change on the 1.10-dev branch: https://github.com/rmosolgo/graphql-ruby/pull/2520

I've just released 1.10.0.pre1, which includes those changes as well as others, and graphql-pro 1.11.0, which should take advantage of that new behavior.

Can you give those a try and let me know how it goes?

All 10 comments

It _should_ just work if you set up the argument_class(...) configuration as shown in the docs: https://graphql-ruby.org/authorization/pundit_integration.html#authorizing-arguments

Does that work for you? What happens when you try it?

I will include my setup below.

So I have a specific optional argument on my PersonInput that I want to limit to a particular role. That is, if a current_user without correct permissions tries to run updatePerson and includes that _particular argument_ for updating and they (current_user) do not have permission, that update should throw an error.

I know I could create two different update mutations (personUpdate and personUpdateSecretSauce) but I thought it would be easier just to have one with the restricted argument.

Anyway, here we go!

# app/graphql/types/base_argument.rb
class Types::BaseArgument < GraphQL::Schema::Argument
  include GraphQL::Pro::PunditIntegration::ArgumentIntegration
  pundit_role nil
end

# app/graphql/types/base_field.rb
class Types::BaseField < GraphQL::Schema::Field
  include GraphQL::Pro::PunditIntegration::FieldIntegration
  argument_class Types::BaseArgument
  pundit_role nil
end

# app/graphql/types/base_input_object.rb
class Types::BaseInputObject < GraphQL::Schema::InputObject
  argument_class Types::BaseArgument
end

# app/graphql/mutations/base_mutation.rb
class Mutations::BaseMutation < GraphQL::Schema::Mutation
  include GraphQL::Pro::PunditIntegration::MutationIntegration

  argument_class Types::BaseArgument
  field_class Types::BaseField
  object_class Types::BaseMutationPayload

  null false

end

# app/graphql/mutations/person_update.rb
class Mutations::PersonUpdate < Mutations::BaseMutation

  # anyone can attempt to call this mutation, but should get shut down 
  # if they cannot pass `update` check on the `id` argument
  # or the `already_cool` check on the `input#is_cool` argument
  pundit_role nil
  type Types::PersonType

  argument :id, ID,
           required: true,
           as: :person,              
           loads: Types::PersonType, 
           pundit_role: :update      # limit to people with update power!

  argument :input, Inputs::PersonInput, required: true

  def resolve(person:, input:)
    person.update!(input.to_kwargs)
    person
  end

  def self.policy_class
    ::PersonPolicy
  end

end

# app/graphql/inputs/person_input.rb
class Inputs::PersonInput < Types::BaseInputObject

  argument :first_name, String, required: false
  argument :last_name, String, required: false
  argument :email, String, required: false
  argument :is_cool, Boolean, required: false, pundit_role: :already_cool

end

The pundit_role call that I have on the argument(:id) method works as expected. However, the already_cool role in the PersonInput is never being called.

I feel like there must be more information I have to pass to the PersonInput in order for it to know what the policy is to check and what record/object it is looking at as well. (In the same way with the id argument I have to tell it the type.

Hope what I am trying to do is making sense! Thanks.

Thanks for sharing those details! Yes, it definitely makes sense. Maybe it's a bug, let me take a look and follow up here.

+1

I have the same problem. Exact same setup.

+1

Looking forward to the fix.

Hi, sorry for the slow turn-around on this, and thanks for the bumps here. I've just released graphql 1.9.13 and graphql-pro 1.10.8 which should fix this. Please update to those versions and give it another go, and let me know if you run into any other trouble!

Thanks for working on this!

I updated to the latest version and after adding def self.pundit_policy_class to my PersonInput (from the example in my comment above) I can confirm now that _the policy is now being called via the input_! 馃憤

However: I think I am still missing another piece of this puzzle (I went over the docs again but didn't see it).

While the pundit policy is being called ... the record that is being passed to PersonPolicy from the PersonInput is not the _actual_ person object鈥攊nstead, the record that PersonPolicy receives is the PersonUpdate mutation itself.

how to get the actual record to the argument inside the input?

Inside the PersonMutation, the id argument has the information about the record specified explicitly (and pundit gets it):

# app/graphql/mutations/person_update.rb

# this defines what the `record` will be explicitly
argument :id, ID, required: true,
  as: :person, loads: Types::PersonType, 
  pundit_role: :update # it passes the record loaded via the id to pundit

However, back inside my PersonInput class: how do I tell that argument to also use the same record that was loaded by the :id argument from the mutation?

# app/graphql/inputs/person_input.rb
class Inputs::PersonInput < Types::BaseInputObject
  # how can I get this argument to identify the correct `record` to pass to pundit?
  argument :is_cool, Boolean, required: false, pundit_role: :already_cool
end

  def self.pundit_policy_class
    ::PersonPolicy
  end
end

I took a look at the additions here and am not seeing how it would be able to discover the hidden record that is specified by the id argument: https://github.com/rmosolgo/graphql-ruby/blob/ca7e303385ef0d6ffb295e2b8fca772106b03352/lib/graphql/schema/argument.rb#L96-L108

Thanks for the details on that, @thornomad . It required a bigger shift, actually a breaking change to graphql-ruby. Previously, argument values weren't included at all in the authorization system. Now, they're passed down as arguments to .authorized? methods, which was a breaking change. I've put this change on the 1.10-dev branch: https://github.com/rmosolgo/graphql-ruby/pull/2520

I've just released 1.10.0.pre1, which includes those changes as well as others, and graphql-pro 1.11.0, which should take advantage of that new behavior.

Can you give those a try and let me know how it goes?

Hi @rmosolgo - I just updated to the 1.10-pre version and it seems to be working now as expected! Thanks for digging into this and finding a solution. Onward!

Dear @rmosolgo and @thornomad,

I've got the same problem today and updated to graphql 1.10.0.pre1 and graphql-pro 1.11.0, but trying to read the record from the input argument still resolves to the mutation and not the record itself.

# app/graphql/mutations/user/update.rb

module Mutations
  module User
    class Update < Inputs
      graphql_name 'UpdateUser'
      pundit_role nil

      argument :id, ID, required: true, loads: Types::UserType, pundit_role: :can_update
      field :user, Types::UserType, null: true

      def load_id(user_id)
        Helpers::GlobalId.find_record(model: ::User, id: user_id)
      end

      def resolve(inputs)
        inputs = Helpers::GlobalId.force_database_ids(fields: inputs)
        Functions::Mutate.new(model: ::User, record: inputs[:id], fields: inputs.except(:id)).update
      end
    end
  end
end
# app/graphql/mutations/user/inputs.rb

module Mutations
  module User
    class Inputs < Base
      argument :username, String, required: false
      argument :is_admin, Boolean, required: false, pundit_role: :can_set_admin
    end
  end
end
# app/policies/user_policy.rb

class UserPolicy < ApplicationPolicy
  def is_admin?
    user.is_admin?
  end

  def can_create?
    user.is_admin?
  end

  def can_update?
    user.is_admin? || record.id == user.id
  end

  def can_set_admin?
      # a user can't remove it's own admin rights
      user.is_admin? && record.id != user.id
  end

  def can_delete?
    user.is_admin? && record.id != user.id
  end
end

I've expected to get the real record in my can_set_admin? method, but I get the mutation object instead.

Anything that I've missed?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jtippett picture jtippett  路  3Comments

pareeohnos picture pareeohnos  路  3Comments

rmosolgo picture rmosolgo  路  4Comments

KevinColemanInc picture KevinColemanInc  路  3Comments

dmc2015 picture dmc2015  路  3Comments