class GraphQL::Schema::Warden

Restrict access to a {GraphQL::Schema} with a user-defined filter.

When validating and executing a query, all access to schema members should go through a warden. If you access the schema directly, you may show a client something that it shouldn’t be allowed to see.

@example Hiding private fields

private_members = -> (member, ctx) { member.metadata[:private] }
result = Schema.execute(query_string, except: private_members)

@example Custom filter implementation

# It must respond to `#call(member)`.
class MissingRequiredFlags
  def initialize(user)
    @user = user
  end

  # Return `false` if any required flags are missing
  def call(member, ctx)
    member.metadata[:required_flags].any? do |flag|
      !@user.has_flag?(flag)
    end
  end
end

# Then, use the custom filter in query:
missing_required_flags = MissingRequiredFlags.new(current_user)

# This query can only access members which match the user's flags
result = Schema.execute(query_string, except: missing_required_flags)

@api private

Constants

NO_REFERENCES

Public Class Methods

from_context(context) click to toggle source
# File lib/graphql/schema/warden.rb, line 40
def self.from_context(context)
  (context.respond_to?(:warden) && context.warden) || PassThruWarden
end
new(filter, context:, schema:) click to toggle source

@param filter [<#call(member)>] Objects are hidden when ‘.call(member, ctx)` returns true @param context [GraphQL::Query::Context] @param schema [GraphQL::Schema]

# File lib/graphql/schema/warden.rb, line 89
def initialize(filter, context:, schema:)
  @schema = schema
  # Cache these to avoid repeated hits to the inheritance chain when one isn't present
  @query = @schema.query
  @mutation = @schema.mutation
  @subscription = @schema.subscription
  @context = context
  @visibility_cache = read_through { |m| filter.call(m, context) }
end
visible_entry?(visibility_method, entry, context, warden = Warden.from_context(context)) click to toggle source

@param visibility_method [Symbol] a Warden method to call for this entry @param entry [Object, Array<Object>] One or more definitions for a given name in a GraphQL Schema @param context [GraphQL::Query::Context] @param warden [Warden] @return [Object] ‘entry` or one of `entry`’s items if exactly one of them is visible for this context @return [nil] If neither ‘entry` nor any of `entry`’s items are visible for this context

# File lib/graphql/schema/warden.rb, line 50
def self.visible_entry?(visibility_method, entry, context, warden = Warden.from_context(context))
  if entry.is_a?(Array)
    visible_item = nil
    entry.each do |item|
      if warden.public_send(visibility_method, item, context)
        if visible_item.nil?
          visible_item = item
        else
          raise DuplicateNamesError.new(
            duplicated_name: item.path, duplicated_definition_1: visible_item.inspect, duplicated_definition_2: item.inspect
          )
        end
      end
    end
    visible_item
  elsif warden.public_send(visibility_method, entry, context)
    entry
  else
    nil
  end
end

Public Instance Methods

arguments(argument_owner) click to toggle source

@param argument_owner [GraphQL::Field, GraphQL::InputObjectType] @return [Array<GraphQL::Argument>] Visible arguments on ‘argument_owner`

# File lib/graphql/schema/warden.rb, line 177
def arguments(argument_owner)
  @visible_arguments ||= read_through { |o| o.arguments(@context).each_value.select { |a| visible_argument?(a) } }
  @visible_arguments[argument_owner]
end
directives() click to toggle source
# File lib/graphql/schema/warden.rb, line 199
def directives
  @schema.directives.each_value.select { |d| visible?(d) }
end
enum_values(enum_defn) click to toggle source

@return [Array<GraphQL::EnumType::EnumValue>] Visible members of ‘enum_defn`

# File lib/graphql/schema/warden.rb, line 183
def enum_values(enum_defn)
  @visible_enum_arrays ||= read_through { |e| e.enum_values(@context) }
  @visible_enum_arrays[enum_defn]
end
fields(type_defn) click to toggle source

@param type_defn [GraphQL::ObjectType, GraphQL::InterfaceType] @return [Array<GraphQL::Field>] Fields on ‘type_defn`

# File lib/graphql/schema/warden.rb, line 170
def fields(type_defn)
  @visible_fields ||= read_through { |t| @schema.get_fields(t, @context).values }
  @visible_fields[type_defn]
end
get_argument(parent_type, argument_name) click to toggle source

@return [GraphQL::Argument, nil] The argument named ‘argument_name` on `parent_type`, if it exists and is visible

# File lib/graphql/schema/warden.rb, line 154
def get_argument(parent_type, argument_name)
  argument = parent_type.get_argument(argument_name, @context)
  return argument if argument && visible_argument?(argument, @context)
end
get_field(parent_type, field_name) click to toggle source

@return [GraphQL::Field, nil] The field named ‘field_name` on `parent_type`, if it exists

# File lib/graphql/schema/warden.rb, line 138
def get_field(parent_type, field_name)
  @visible_parent_fields ||= read_through do |type|
    read_through do |f_name|
      field_defn = @schema.get_field(type, f_name, @context)
      if field_defn && visible_field?(field_defn, nil, type)
        field_defn
      else
        nil
      end
    end
  end

  @visible_parent_fields[parent_type][field_name]
end
get_type(type_name) click to toggle source

@return [GraphQL::BaseType, nil] The type named ‘type_name`, if it exists (else `nil`)

# File lib/graphql/schema/warden.rb, line 113
def get_type(type_name)
  @visible_types ||= read_through do |name|
    type_defn = @schema.get_type(name, @context)
    if type_defn && visible_and_reachable_type?(type_defn)
      type_defn
    else
      nil
    end
  end

  @visible_types[type_name]
end
interfaces(obj_type) click to toggle source

@return [Array<GraphQL::InterfaceType>] Visible interfaces implemented by ‘obj_type`

# File lib/graphql/schema/warden.rb, line 194
def interfaces(obj_type)
  @visible_interfaces ||= read_through { |t| t.interfaces(@context).select { |i| visible_type?(i) } }
  @visible_interfaces[obj_type]
end
possible_types(type_defn) click to toggle source

@return [Array<GraphQL::BaseType>] The types which may be member of ‘type_defn`

# File lib/graphql/schema/warden.rb, line 160
def possible_types(type_defn)
  @visible_possible_types ||= read_through { |type_defn|
    pt = @schema.possible_types(type_defn, @context)
    pt.select { |t| visible_and_reachable_type?(t) }
  }
  @visible_possible_types[type_defn]
end
reachable_type?(type_name) click to toggle source

@return Boolean True if the type is visible and reachable in the schema

# File lib/graphql/schema/warden.rb, line 132
def reachable_type?(type_name)
  type = get_type(type_name) # rubocop:disable Development/ContextIsPassedCop -- `self` is query-aware
  type && reachable_type_set.include?(type)
end
reachable_types() click to toggle source

@return [Array<GraphQL::BaseType>] Visible and reachable types in the schema

# File lib/graphql/schema/warden.rb, line 127
def reachable_types
  @reachable_types ||= reachable_type_set.to_a
end
root_type_for_operation(op_name) click to toggle source
# File lib/graphql/schema/warden.rb, line 203
def root_type_for_operation(op_name)
  root_type = @schema.root_type_for_operation(op_name)
  if root_type && visible?(root_type)
    root_type
  else
    nil
  end
end
types() click to toggle source

@return [Hash<String, GraphQL::BaseType>] Visible types in the schema

# File lib/graphql/schema/warden.rb, line 100
def types
  @types ||= begin
    vis_types = {}
    @schema.types(@context).each do |n, t|
      if visible_and_reachable_type?(t)
        vis_types[n] = t
      end
    end
    vis_types
  end
end
visible_argument?(arg_defn, _ctx = nil) click to toggle source
# File lib/graphql/schema/warden.rb, line 223
def visible_argument?(arg_defn, _ctx = nil)
  visible?(arg_defn) && visible_and_reachable_type?(arg_defn.type.unwrap)
end
visible_enum_value?(enum_value, _ctx = nil) click to toggle source
# File lib/graphql/schema/warden.rb, line 188
def visible_enum_value?(enum_value, _ctx = nil)
  @visible_enum_values ||= read_through { |ev| visible?(ev) }
  @visible_enum_values[enum_value]
end
visible_field?(field_defn, _ctx = nil, owner = field_defn.owner) click to toggle source

@param owner [Class, Module] If provided, confirm that field has the given owner.

# File lib/graphql/schema/warden.rb, line 213
def visible_field?(field_defn, _ctx = nil, owner = field_defn.owner)
  # This field is visible in its own right
  visible?(field_defn) &&
    # This field's return type is visible
    visible_and_reachable_type?(field_defn.type.unwrap) &&
    # This field is either defined on this object type,
    # or the interface it's inherited from is also visible
    ((field_defn.respond_to?(:owner) && field_defn.owner == owner) || field_on_visible_interface?(field_defn, owner))
end
visible_type?(type_defn, _ctx = nil) click to toggle source
# File lib/graphql/schema/warden.rb, line 227
def visible_type?(type_defn, _ctx = nil)
  @type_visibility ||= read_through { |type_defn| visible?(type_defn) }
  @type_visibility[type_defn]
end
visible_type_membership?(type_membership, _ctx = nil) click to toggle source
# File lib/graphql/schema/warden.rb, line 232
def visible_type_membership?(type_membership, _ctx = nil)
  visible?(type_membership)
end

Private Instance Methods

field_on_visible_interface?(field_defn, type_defn) click to toggle source

If this field was inherited from an interface, and the field on that interface is hidden, then treat this inherited field as hidden. (If it _wasn’t_ inherited, then don’t hide it for this reason.)

# File lib/graphql/schema/warden.rb, line 270
def field_on_visible_interface?(field_defn, type_defn)
  if type_defn.kind.object?
    any_interface_has_field = false
    any_interface_has_visible_field = false
    ints = unfiltered_interfaces(type_defn)
    ints.each do |interface_type|
      if (iface_field_defn = interface_type.get_field(field_defn.graphql_name, @context))
        any_interface_has_field = true

        if interfaces(type_defn).include?(interface_type) && visible_field?(iface_field_defn, nil, interface_type)
          any_interface_has_visible_field = true
        end
      end
    end

    if any_interface_has_field
      any_interface_has_visible_field
    else
      # it's the object's own field
      true
    end
  else
    true
  end
end
orphan_type?(type_defn) click to toggle source
# File lib/graphql/schema/warden.rb, line 311
def orphan_type?(type_defn)
  @schema.orphan_types.include?(type_defn)
end
reachable_type_set() click to toggle source
# File lib/graphql/schema/warden.rb, line 334
def reachable_type_set
  return @reachable_type_set if defined?(@reachable_type_set)

  @reachable_type_set = Set.new
  rt_hash = {}

  unvisited_types = []
  ['query', 'mutation', 'subscription'].each do |op_name|
    root_type = root_type_for_operation(op_name)
    unvisited_types << root_type if root_type
  end
  unvisited_types.concat(@schema.introspection_system.types.values)

  directives.each do |dir_class|
    arguments(dir_class).each do |arg_defn|
      arg_t = arg_defn.type.unwrap
      if get_type(arg_t.graphql_name) # rubocop:disable Development/ContextIsPassedCop -- `self` is query-aware
        unvisited_types << arg_t
      end
    end
  end

  @schema.orphan_types.each do |orphan_type|
    if get_type(orphan_type.graphql_name) == orphan_type # rubocop:disable Development/ContextIsPassedCop -- `self` is query-aware
      unvisited_types << orphan_type
    end
  end

  until unvisited_types.empty?
    type = unvisited_types.pop
    if @reachable_type_set.add?(type)
      type_by_name = rt_hash[type.graphql_name] ||= type
      if type_by_name != type
        raise DuplicateNamesError.new(
          duplicated_name: type.graphql_name, duplicated_definition_1: type.inspect, duplicated_definition_2: type_by_name.inspect
        )
      end
      if type.kind.input_object?
        # recurse into visible arguments
        arguments(type).each do |argument|
          argument_type = argument.type.unwrap
          unvisited_types << argument_type
        end
      elsif type.kind.union?
        # recurse into visible possible types
        possible_types(type).each do |possible_type|
          unvisited_types << possible_type
        end
      elsif type.kind.fields?
        if type.kind.interface?
          # recurse into visible possible types
          possible_types(type).each do |possible_type|
            unvisited_types << possible_type
          end
        elsif type.kind.object?
          # recurse into visible implemented interfaces
          interfaces(type).each do |interface|
            unvisited_types << interface
          end
        end

        # recurse into visible fields
        fields(type).each do |field|
          field_type = field.type.unwrap
          unvisited_types << field_type
          # recurse into visible arguments
          arguments(field).each do |argument|
            argument_type = argument.type.unwrap
            unvisited_types << argument_type
          end
        end
      end
    end
  end

  @reachable_type_set
end
read_through() { |k| ... } click to toggle source
# File lib/graphql/schema/warden.rb, line 330
def read_through
  Hash.new { |h, k| h[k] = yield(k) }
end
referenced?(type_defn) click to toggle source
# File lib/graphql/schema/warden.rb, line 302
def referenced?(type_defn)
  @references_to ||= @schema.references_to
  graphql_name = type_defn.unwrap.graphql_name
  members = @references_to[graphql_name] || NO_REFERENCES
  members.any? { |m| visible?(m) }
end
root_type?(type_defn) click to toggle source
# File lib/graphql/schema/warden.rb, line 296
def root_type?(type_defn)
  @query == type_defn ||
    @mutation == type_defn ||
    @subscription == type_defn
end
unfiltered_interfaces(type_defn) click to toggle source

We need this to tell whether a field was inherited by an interface even when that interface is hidden from ‘#interfaces`

# File lib/graphql/schema/warden.rb, line 262
def unfiltered_interfaces(type_defn)
  @unfiltered_interfaces ||= read_through(&:interfaces)
  @unfiltered_interfaces[type_defn]
end
union_memberships(obj_type) click to toggle source
# File lib/graphql/schema/warden.rb, line 255
def union_memberships(obj_type)
  @unions ||= read_through { |obj_type| @schema.union_memberships(obj_type).select { |u| visible?(u) } }
  @unions[obj_type]
end
visible?(member) click to toggle source
# File lib/graphql/schema/warden.rb, line 326
def visible?(member)
  @visibility_cache[member]
end
visible_abstract_type?(type_defn) click to toggle source
# File lib/graphql/schema/warden.rb, line 315
def visible_abstract_type?(type_defn)
  type_defn.kind.object? && (
      interfaces(type_defn).any? ||
      union_memberships(type_defn).any?
    )
end
visible_and_reachable_type?(type_defn) click to toggle source
# File lib/graphql/schema/warden.rb, line 238
def visible_and_reachable_type?(type_defn)
  @visible_and_reachable_type ||= read_through do |type_defn|
    next false unless visible_type?(type_defn)
    next true if root_type?(type_defn) || type_defn.introspection?

    if type_defn.kind.union?
      visible_possible_types?(type_defn) && (referenced?(type_defn) || orphan_type?(type_defn))
    elsif type_defn.kind.interface?
      visible_possible_types?(type_defn)
    else
      referenced?(type_defn) || visible_abstract_type?(type_defn)
    end
  end

  @visible_and_reachable_type[type_defn]
end
visible_possible_types?(type_defn) click to toggle source
# File lib/graphql/schema/warden.rb, line 322
def visible_possible_types?(type_defn)
  possible_types(type_defn).any? { |t| visible_and_reachable_type?(t) }
end