Using authorized_scope on input
lifeiscontent opened this issue · 7 comments
@palkan is it possible to use authorized_Scope on an input like you would with params
from rail's strong parameters?
I saw that you had this feature in Rails and it started to make me wonder why inputs
don't work like strong parameters in the context of GraphQL Ruby.
Yeah, I think it should be possible.
Let's imaging what it could be like using an example from graphql ruby docs.
class Types::PostAttributes < GraphQL::Schema::InputObject
description "Attributes for creating or updating a blog post"
argument :title, String, "Header for the post", required: true
argument :full_text, String, "Full body of the post", required: true
argument :categories, [Types::PostCategory], required: false
end
class Mutations::CreatePost < GraphQL::Schema::RelayClassicMutation
input_object_class Types::PostAttributes
def resolve(input:)
authorized_input = authorized_scope(input, with: PostPolicy)
# ...
end
end
class ApplicationPolicy < ActionPolicy::Base
# define scope type matcher based on the data type
scope_matcher :graphq_input, GraphQL::Schema::InputObject
end
class PostPolicy < ApplicationPolicy
graphql_input_scope do |input|
# do something with input here
end
end
Not sure how exactly we need to filter GraphQL inputs. I think, raising an error if some unauthorized field was set.
@palkan yeah, I think you'd raise an ActionPolicy::Unauthorized
do you know what that might look like here? e.g. if an input has a user_id
and its not the id of the viewer
if an input has a user_id and its not the id of the viewer
Then, why do you need a user_id
at all ?)
A straightforward way to achieve this is:
graphql_input_scope do |input|
raise ActionPolicy::Unauthorized if input.user_id != user.id
# return input if all is OK
input
end
@palkan because the viewer could be an admin creating on behalf of the user. and if a user (hacker) tries to pass a different user ID, it should throw an error. do you have a better way of handling this kind of scenario?
I see two ways of dealing with this.
- Using scopes to filter the input object. In this case, we delete the
user_id
from the input (that's similar toparams.permit
in Rails controllers):
graphql_input_scope do |input|
input.argument_values.delete(:user_id) unless user.admin?
input
end
Not sure about the code above, graphql-ruby looks a bit complicated here. I took a look a these files:
- https://github.com/rmosolgo/graphql-ruby/blob/e881fc30760f6724ddbb627d66f0ee2e2e98def5/lib/graphql/schema/input_object.rb#L12-L20
- https://github.com/rmosolgo/graphql-ruby/blob/e881fc30760f6724ddbb627d66f0ee2e2e98def5/lib/graphql/query/arguments.rb#L41-L53
- Using visibility filters and hide the argument from non-admins altogether.
That would likely require adding a custom argument class and a filter:
class ArgumentWithVisibility < GraphQL::Schema::Argument
attr_reader :admin_only
alias admin_only? admin_only
def initialize(name, type, desc = nil, admin_only: false, **kwargs)
@admin_only = admin_only
super(name, type, desc, **kwargs)
end
end
class BaseInputObject < GraphQL::Schema::InputObject
argument_class ArgumentWithVisibility
end
filter = ->(member, ctx) {
next true unless member.is_a?(ArgumentWithVisibility)
!member.admin_only? || ctx[:current_user]&.admin?
}
MySchema.execute(query_string, except: filter)
class Types::PostAttributes < BaseInputObject
# ...
# Finally, in your schema
argument :user_id, ID, required: false, admin_only: true
end
@palkan, sorry for commenting on stale issue, but can you please explain how to properly use raising of ActionPolicy::Unauthorized
from scopes, especially scopes for Strong Params.
The problem is that exception initializer requires two params to be passed: policy
and rule
. See:
https://github.com/palkan/action_policy/blob/e5337c01d1caa652828ecd804406bea821c46975/lib/action_policy/authorizer.rb#L24
And while scopes are always defined in a policy, there is no problem to pass, but some existing rule may not fit scope's logic.
And most importantly, it is not clear how to pass rejection reasons.
but some existing rule may not fit scope's logic
You can use any Symbol, it's just a name of the rule (or scope) that has triggered the exception.
And most importantly, it is not clear how to pass rejection reasons.
Hm, we don't have result
object initialized while applying scopes. We can do smth like that:
params_filter do |params|
if user.admin?
params.permit(:name, :email, :role)
else
reject!(:invalid_params) if params.key?(:role) || params.key?(:email)
params.permit(:name)
end
end
def reject!(reason = nil)
# populate result object
apply(:unauthorized_params)
result.reasons.add(self, reason) if reason
raise Unauthorized.new(self, result.rule)
end
# Always return false — a hack to reject scopes
def unauthorized_params
false
end
Looks hacky(
We can make such #reject!
(without apply hacks) a part of the API.