From fa623aa00b4cde4f326a20b2bb0f947b2a91346c Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Thu, 8 Aug 2024 17:03:11 -0400 Subject: [PATCH] Add docs --- guides/authorization/authorization.md | 9 ++++++++ lib/graphql/schema/enum.rb | 30 +++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/guides/authorization/authorization.md b/guides/authorization/authorization.md index 8d19307339..913b38b1ba 100644 --- a/guides/authorization/authorization.md +++ b/guides/authorization/authorization.md @@ -17,6 +17,7 @@ Schema members have `authorized?` methods which will be called during execution: - Fields have `#authorized?(object, args, context)` instance methods - Arguments have `#authorized?(object, arg_value, context)` instance methods - Mutations and Resolvers have `.authorized?(object, context)` class methods and `#authorized?(args)` instance methods +- Enum values have `#authorized?(context)` instance methods These methods are called with: @@ -90,6 +91,14 @@ For this to work, the base argument class must be {% internal_link "configured w See mutations/mutation_authorization.html#can-this-user-perform-this-action {% internal_link "Mutation Authorization", "/mutations/mutation_authorization.html#can-this-user-perform-this-action" %}) in the Mutation Guides. +## Enum Value Authorization + +{{ "GraphQL::Schema::EnumValue#authorized?" | api_doc }} is called when client input is received and when the schema returns values to the client. + +For authorizing input, if a value's `#authorized?` method returns false, then a {{ "GraphQL::UnauthorizedEnumValueError" | api_doc }} is raised. It passed to your schema's `.unauthorized_object` hook, where you can handle it another way if you want. + +For authorizing return values, if an outgoing value's `#authorized?` method returns false, then a {{ "GraphQL::Schema::Enum::UnresolvedValueError" | api_doc }} is raised, which crashes the query. In this case, you should modify your field or resolver to _not_ return this value to an unauthorized viewer. (In this case, the error isn't returned to the viewer because the viewer can't do anything about it -- it's a developer-facing issue instead.) + ## Handling Unauthorized Objects By default, GraphQL-Ruby silently replaces unauthorized objects with `nil`, as if they didn't exist. You can customize this behavior by implementing {{ "Schema.unauthorized_object" | api_doc }} in your schema class, for example: diff --git a/lib/graphql/schema/enum.rb b/lib/graphql/schema/enum.rb index 4e0d7cf90e..3a20c59300 100644 --- a/lib/graphql/schema/enum.rb +++ b/lib/graphql/schema/enum.rb @@ -22,6 +22,14 @@ class Schema class Enum < GraphQL::Schema::Member extend GraphQL::Schema::Member::ValidatesInput + # This is raised when either: + # + # - A resolver returns a value which doesn't match any of the enum's configured values; + # - Or, the resolver returns a value which matches a value, but that value's `authorized?` check returns false. + # + # In either case, the field should be modified so that the invalid value isn't returned. + # + # {GraphQL::Schema::Enum} subclasses get their own subclass of this error, so that bug trackers can better show where they came from. class UnresolvedValueError < GraphQL::Error def initialize(value:, enum:, context:, authorized:) fix_message = if authorized == false @@ -38,6 +46,8 @@ def initialize(value:, enum:, context:, authorized:) end end + # Raised when a {GraphQL::Schema::Enum} is defined to have no values. + # This can also happen when all values return false for `.visible?`. class MissingValuesError < GraphQL::Error def initialize(enum_type) @enum_type = enum_type @@ -47,10 +57,10 @@ def initialize(enum_type) class << self # Define a value for this enum - # @param graphql_name [String, Symbol] the GraphQL value for this, usually `SCREAMING_CASE` - # @param description [String], the GraphQL description for this value, present in documentation - # @param value [Object], the translated Ruby value for this object (defaults to `graphql_name`) - # @param deprecation_reason [String] if this object is deprecated, include a message here + # @option kwargs [String, Symbol] :graphql_name the GraphQL value for this, usually `SCREAMING_CASE` + # @option kwargs [String] :description, the GraphQL description for this value, present in documentation + # @option kwargs [::Object] :value the translated Ruby value for this object (defaults to `graphql_name`) + # @option kwargs [String] :deprecation_reason if this object is deprecated, include a message here # @return [void] # @see {Schema::EnumValue} which handles these inputs by default def value(*args, **kwargs, &block) @@ -144,6 +154,12 @@ def validate_non_null_input(value_name, ctx, max_errors: nil) end end + # Called by the runtime when a field returns a value to give back to the client. + # This method checks that the incoming {value} matches one of the enum's defined values. + # @param value [Object] Any value matching the values for this enum. + # @param ctx [GraphQL::Query::Context] + # @raise [GraphQL::Schema::Enum::UnresolvedValueError] if {value} doesn't match a configured value or if the matching value isn't authorized. + # @return [String] The GraphQL-ready string for {value} def coerce_result(value, ctx) types = ctx.types all_values = types ? types.enum_values(self) : values.each_value @@ -155,6 +171,12 @@ def coerce_result(value, ctx) end end + # Called by the runtime with incoming string representations from a query. + # It will match the string to a configured by name or by Ruby value. + # @param value_name [String, Object] A string from a GraphQL query, or a Ruby value matching a `value(..., value: ...)` configuration + # @param ctx [GraphQL::Query::Context] + # @raise [GraphQL::UnauthorizedEnumValueError] if an {EnumValue} matches but returns false for `.authorized?`. Goes to {Schema.unauthorized_object}. + # @return [Object] The Ruby value for the matched {GraphQL::Schema::EnumValue} def coerce_input(value_name, ctx) all_values = ctx.types ? ctx.types.enum_values(self) : values.each_value