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.
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.
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?
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/2520I'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?