Giter Site home page Giter Site logo

graphql-guard's Introduction

graphql-guard

Build Status Coverage Status Code Climate Downloads Latest Version

This gem provides a field-level authorization for graphql-ruby.

Contents

Usage

Define a GraphQL schema:

# Define a type
class PostType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: true
end

# Define a query
class QueryType < GraphQL::Schema::Object
  field :posts, [PostType], null: false do
    argument :user_id, ID, required: true
  end

  def posts(user_id:)
    Post.where(user_id: user_id)
  end
end

# Define a schema
class Schema < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
end

# Execute query
Schema.execute(query, variables: { userId: 1 }, context: { current_user: current_user })

Inline policies

Add GraphQL::Guard to your schema:

class Schema < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
  use GraphQL::Guard.new
end

Now you can define guard for a field, which will check permissions before resolving the field:

class QueryType < GraphQL::Schema::Object
  field :posts, [PostType], null: false do
    argument :user_id, ID, required: true
    guard ->(obj, args, ctx) { args[:user_id] == ctx[:current_user].id }
  end
  ...
end

You can also define guard, which will be executed for every * field in the type:

class PostType < GraphQL::Schema::Object
  guard ->(obj, args, ctx) { ctx[:current_user].admin? }
  ...
end

If guard block returns nil or false, then it'll raise a GraphQL::Guard::NotAuthorizedError error.

Policy object

Alternatively, it's possible to extract and describe all policies by using PORO (Plain Old Ruby Object), which should implement a guard method. For example:

class GraphqlPolicy
  RULES = {
    QueryType => {
      posts: ->(obj, args, ctx) { args[:user_id] == ctx[:current_user].id }
    },
    PostType => {
      '*': ->(obj, args, ctx) { ctx[:current_user].admin? }
    }
  }

  def self.guard(type, field)
    RULES.dig(type, field)
  end
end

Pass this object to GraphQL::Guard:

class Schema < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
  use GraphQL::Guard.new(policy_object: GraphqlPolicy)
end

When using a policy object, you may want to allow introspection queries to skip authorization. A simple way to avoid having to whitelist every introspection type in the RULES hash of your policy object is to check the type parameter in the guard method:

def self.guard(type, field)
  type.introspection? ? ->(_obj, _args, _ctx) { true } : RULES.dig(type, field) # or "false" to restrict an access
end

Priority order

GraphQL::Guard will use the policy in the following order of priority:

  1. Inline policy on the field.
  2. Policy from the policy object on the field.
  3. Inline policy on the type.
  4. Policy from the policy object on the type.
class GraphqlPolicy
  RULES = {
    PostType => {
      '*': ->(obj, args, ctx) { ctx[:current_user].admin? },                                # <=== 4
      title: ->(obj, args, ctx) { ctx[:current_user].admin? }                               # <=== 2
    }
  }

  def self.guard(type, field)
    RULES.dig(type, field)
  end
end

class PostType < GraphQL::Schema::Object
  guard ->(obj, args, ctx) { ctx[:current_user].admin? }                                    # <=== 3
  field :title, String, null: true, guard: ->(obj, args, ctx) { ctx[:current_user].admin? } # <=== 1
end

class Schema < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
  use GraphQL::Guard.new(policy_object: GraphqlPolicy)
end

Integration

You can simply reuse your existing policies if you really want. You don't need any monkey patches or magic for it ;)

CanCanCan

# Define an ability
class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new
    if user.admin?
      can :manage, :all
    else
      can :read, Post, author_id: user.id
    end
  end
end

# Use the ability in your guard
class PostType < GraphQL::Schema::Object
  guard ->(post, args, ctx) { ctx[:current_ability].can?(:read, post) }
  ...
end

# Pass the ability
Schema.execute(query, context: { current_ability: Ability.new(current_user) })

Pundit

# Define a policy
class PostPolicy < ApplicationPolicy
  def show?
    user.admin? || record.author_id == user.id
  end
end

# Use the ability in your guard
class PostType < GraphQL::Schema::Object
  guard ->(post, args, ctx) { PostPolicy.new(ctx[:current_user], post).show? }
  ...
end

# Pass current_user
Schema.execute(query, context: { current_user: current_user })

Error handling

By default GraphQL::Guard raises a GraphQL::Guard::NotAuthorizedError exception if access to the field is not authorized. You can change this behavior, by passing custom not_authorized lambda. For example:

class SchemaWithErrors < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
  use GraphQL::Guard.new(
    # By default it raises an error
    # not_authorized: ->(type, field) do
    #   raise GraphQL::Guard::NotAuthorizedError.new("#{type}.#{field}")
    # end

    # Returns an error in the response
    not_authorized: ->(type, field) do
      GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}")
    end
  )
end

In this case executing a query will continue, but return nil for not authorized field and also an array of errors:

SchemaWithErrors.execute("query { posts(user_id: 1) { id title } }")
# => {
#   "data" => nil,
#   "errors" => [{
#     "messages" => "Not authorized to access Query.posts",
#     "locations": { "line" => 1, "column" => 9 },
#     "path" => ["posts"]
#   }]
# }

In more advanced cases, you may want not to return errors only for some unauthorized fields. Simply return nil if user is not authorized to access the field. You can achieve it, for example, by placing the logic into your PolicyObject:

class GraphqlPolicy
  RULES = {
    PostType => {
      '*': {
        guard: ->(obj, args, ctx) { ... },
        not_authorized: ->(type, field) { GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") }
      }
      title: {
        guard: ->(obj, args, ctx) { ... },
        not_authorized: ->(type, field) { nil } # simply return nil if not authorized, no errors
      }
    }
  }

  def self.guard(type, field)
    RULES.dig(type, field, :guard)
  end

  def self.not_authorized_handler(type, field)
    RULES.dig(type, field, :not_authorized) || RULES.dig(type, :'*', :not_authorized)
  end
end

class Schema < GraphQL::Schema
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  query QueryType
  mutation MutationType

  use GraphQL::Guard.new(
    policy_object: GraphqlPolicy,
    not_authorized: ->(type, field) {
      handler = GraphqlPolicy.not_authorized_handler(type, field)
      handler.call(type, field)
    }
  )
end

Schema masking

It's possible to hide fields from being introspectable and accessible based on the context. For example:

class PostType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: true do
    # The field "title" is accessible only for beta testers
    mask ->(ctx) { ctx[:current_user].beta_tester? }
  end
end

Installation

Add this line to your application's Gemfile:

gem 'graphql-guard'

And then execute:

$ bundle

Or install it yourself as:

$ gem install graphql-guard

Testing

It's possible to test fields with guard in isolation:

# Your type
class QueryType < GraphQL::Schema::Object
  field :posts, [PostType], null: false, guard ->(obj, args, ctx) { ... }
end

# Your test
require "graphql/guard/testing"

posts = QueryType.field_with_guard('posts')
result = posts.guard(obj, args, ctx)
expect(result).to eq(true)

If you would like to test your fields with policy objects:

# Your type
class QueryType < GraphQL::Schema::Object
  field :posts, [PostType], null: false
end

# Your policy object
class GraphqlPolicy
  def self.guard(type, field)
    ->(obj, args, ctx) { ... }
  end
end

# Your test
require "graphql/guard/testing"

posts = QueryType.field_with_guard('posts', GraphqlPolicy)
result = posts.guard(obj, args, ctx)
expect(result).to eq(true)

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/exAspArk/graphql-guard. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Graphql::Guard project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

graphql-guard's People

Contributors

exaspark avatar kymmt90 avatar sandboxws avatar sshaw avatar stanishev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-guard's Issues

Can't test guard proc on Mutation root

Related to the documentation on the testing of the guard proc: https://github.com/exAspArk/graphql-guard#testing

The documentation suggests that I should be able to do QueryType.field_with_guard('posts'), but when I try to do it with my own Mutations::Root object (which is a GraphQL::ObjectType), I get a no method error for field_with_guard:

Mutations::Root.field_with_guard('some_existing_mutation_endpoint')
NoMethodError: undefined method `field_with_guard' for Mutation:GraphQL::ObjectType

It seems to be the same for all other object types I defined in my GraphQL schema using graphql-ruby v1.8.4 and graphql-guard 1.0.0 & 1.2.0 (tested with both).

Undefined method guard for Resolvers Class

I am trying to use guard inside the Mutation, using Resolvers class:

class Resolvers::UpdateExample < GraphQL::Function
  argument :id, !types.ID
  argument :example, !Inputs::ExampleInput

  type Types::ExampleType

  guard ->(obj, args, ctx) { 
      true
    }

  def call(obj,args,ctx)
    puts "hello world"
  end
end

When I try to call this mutation I get error:

Failure/Error:
guard ->(obj, args, ctx) {
..

 NoMethodError:
   undefined method `guard' for Resolvers::UpdateExample:Class

Add support for Mutation

Is there any technical limitation for not supporting the DSL on mutations ?

Something like this:

class CreateDevice < GraphQL::Schema::Mutation
  guard -> (_, _, _) { raise StandardError.new }
   
  argument :arg1, String, null: false

  def resolve(arg1:)
    # My logic
  end
end

Deprecation warnings with graphql-1.13.1

Hello,

Deprecation warnings starting graphql-1.13.1:

Legacy `.to_graphql` objects are deprecated and will be removed in GraphQL-Ruby 2.0. Remove `.to_graphql` to use a class-based definition instead.

Called on #<GraphQL::Schema::Field Query.posts(...): [Post!]!> from:

graphql-guard/lib/graphql/guard/testing.rb:29:in `field_with_guard'
.....

From changelog

.to_graphql and .graphql_definition are deprecated and will be removed in GraphQL-Ruby 2.0. 
All features using those legacy definitions are already removed
and all behaviors should have been ported to class-based definitions. 
So, you should be able to remove those calls entirely.
Please open an issue if you have trouble with it! #3750 #3765

Also I noticed, that inline guards return Array wrapped guard Proc and not just proc right away. Is this intended outcome in graphql or some undesired behaviour - I don't know, should investigate more.

Guarding for the entire root of an object

QueryType = GraphQL::ObjectType.define do
  name 'Root'

  field :widget do
    type WidgetType
    argument :id, !types.ID

    # Can't guard here because we haven't queried for the object yet

    resolve ->(_obj, args, _ctx) do
      Widget.find(args['id'])
    end
  end
end

WidgetType = GraphQL::ObjectType.define do
  name 'Widget'

  guard ->(widget, _args, ctx) do
    ctx[:current_user].can?(:read, widget)
  end

  field :id, type: types.String # => Not authorized to access Widget.id
  field :title, TruncatedStringField.new # Returns the title because we haven't explicitly guarded on this path
end

How would one go about guarding the entire WidgetType and every field under it? It seems to me that TruncatedStringField should not be returning, but that is in fact what happens. The errors return for every field that's queried, which also seems non-ideal.

Can't Using Mutation Type and Query Type Together

I'd like to use mutation type and query type to be used in GraphQL Policy, but what i got, only "QueryType" that always got called even call mutation. Is there an idea why this happen?

Rails 5.2.3
Ruby 2.6.3

class PhoneBookSchema < GraphQL::Schema
  mutation(Types::MutationType)
  query(Types::QueryType)

  use GraphQL::Guard.new(
    policy_object: GraphqlPolicy,
    not_authorized: ->(type, field) { GraphQL::ExecutionError.new("Not authorized to access #{field}") }
  )
end

class GraphqlPolicy
    RULES = {
        Types::QueryType => {
          :user => ->(obj,args,ctx) { ctx[:current_user].is_admin || ctx[:current_ability].can?(:read,User.find(args[:id])) },
          :users => ->(obj,args,ctx) { ctx[:current_user].is_admin },
        },
        Types::MutationType => {
          :create_user => ->(obj,args,ctx) { ctx[:current_user].is_admin },
          :update_user => ->(obj,args,ctx) { ctx[:current_ability].can?(:update, User.find(args[:user_id])) || ctx[:current_user].is_admin }
        },
        
    }
  
    def self.guard(type, field)
      RULES.dig(type.metadata[:type_class], field)
    end
end

PORO Policy receiving incorrect (unexpected) type

Hey, first of all, thanks for making this! Its easy integration with Pundit and its good documentation are pretty great.

Running into an issue here - I'm looking to provide authorization and not_authorized handling on the field level. So, I copy-pasted your example, swapped out constants ... and found that the procs in the GraphqlPolicy weren't being executed.

This may be a misunderstanding of graphql-ruby itself.

Here are snippets:

the schema (verbatim from documentation, except for Types:: prefix):

  use GraphQL::Guard.new(
    policy_object: Types::GraphqlPolicy,
    not_authorized: ->(type, field) {
      handler = Types::GraphqlPolicy.not_authorized_handler(type, field)
      handler.call(type, field)
    }
  )

graphql/types/graphql_policy.rb
(named and placed that way to put it in the same load path as UserType etc, definitely open to suggestions) (pretty much copy-pasted)

module Types
  class GraphqlPolicy
    RULES = {
      UserType => {
        '*': {
          guard: ->(obj, args, ctx) { byebug; nil; UserPolicy.new(ctx[:current_user], obj.object).show? },
        },
        summary: {
          guard: ->(obj, args, ctx) { byebug; nil; UserPolicy.new(ctx[:current_user], obj.object).show_progress? },
          not_authorized: ->(type, field) { nil } # simply return nil if not authorized, no errors
        }
      }
    }

    def self.guard(type, field)
      byebug # This is the only byebug that is evaluated, and produces the output in next snippet
      RULES.dig(type, field, :guard)
    end
  
    def self.not_authorized_handler(type, field)
      Rails.logger.error("Calling not_authorized_handler for #{type.class}: #{type}, #{field}")
      RULES.dig(type, field, :not_authorized) || RULES.dig(type, :'*', :not_authorized)
    end
  end

The output (one of many) from that byebug statement, showing it's not matching the rule:

(byebug)
1: type = User
2: field = :*
3: RULES.dig(type, field, :guard) = nil

The output from the logger:

Calling not_authorized_handler for GraphQL::ObjectType: User, summary

Attempted workarounds:

  1. UserType to User -> No change

  2. UserType to GraphQL::ObjectType::User -> .../rails-root/app/graphql/types/graphql_policy.rb:4: warning: toplevel constant User referenced by GraphQL::ObjectType::User

  3. RULES.dig("#{type.to_s}Type", field, :not_authorized) || RULES.dig("#{type.to_s}Type", :'*', :not_authorized), and using a string as the key in RULES -> it works, but is hacky and adds unnecessary overhead in string conversions

So, the problem seems to be that the parameters sent to guard don't line up with that object's shape.

What's weird, to me, is that the type is User, which isn't the graphql type name at all:

module Types
  class UserType < Types::BaseObject

User is the model's class name.

The entire point of using the PORO was to be able to deny access only to that field, returning nil (but still returning an error, ideally, and definitely errors for other fields in other queries). Unfortunately, the best I can do with this is block the whole query:

{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "Not authorized to access summary on this User.",
      "locations": [
        {
          "line": 8,
          "column": 5
        }
      ],
      "path": [
        "user",
        "summary"
      ]
    }
  ]
}

IMO it would intuitive if we could just define not_authorized right there in the type definition:

field :summary, UserSummaryType, null: false do
not_authorized ->(type, field) { nil }
guard ->(obj, args, ctx) { UserPolicy.new(ctx[:current_user], obj.object).show_progress? }
end

but unfortunately

undefined method `not_authorized' for #<GraphQL::Schema::Field:>

PS, in the example for not_authorized_handler, there's a good chance of that returning nil, which throws a NoMethodError on call(). Shouldn't there also be a fallback to a default handler in the schema?

not_authorized: ->(type, field) {
      handler = Types::GraphqlPolicy.not_authorized_handler(type, field)
      handler ? handler.call(type, field) : GraphQL::ExecutionError.new("Not authorized to access #{field} on this #{type}.")
    }

Upgraded to 2.0 And Cannot `rescue_from GraphQL::Guard::NotAuthorizedError`

Was on 1.2.2, graphql 1.8.10 and just upgraded to 2.0 and graphql 1.11.5 and see that:

rescue_from GraphQL::Guard::NotAuthorizedError do |e|
  # some handling
end

Is not hit when said error is raised.

At first I thought it may be due to rmosolgo/graphql-ruby#2140 but looking at the stacktrace I think it may be due to the addition use GraphQL::Execution::Interpreter:

 # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-guard-2.0.0/lib/graphql/guard.rb:13:in `block in <class:Guard>'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-guard-2.0.0/lib/graphql/guard.rb:73:in `ensure_guarded'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-guard-2.0.0/lib/graphql/guard.rb:40:in `trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `block in call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/platform_tracing.rb:52:in `block in trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/new_relic_tracing.rb:34:in `block in platform_trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/newrelic_rpm-6.3.0.355/lib/new_relic/agent/method_tracer_helpers.rb:30:in `trace_execution_scoped'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/new_relic_tracing.rb:33:in `platform_trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/platform_tracing.rb:51:in `trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:67:in `trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter/runtime.rb:225:in `block (4 levels) in evaluate_selections'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/query.rb:353:in `block in with_error_handling'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/errors.rb:30:in `with_error_handling'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/query.rb:352:in `with_error_handling'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter/runtime.rb:224:in `block (3 levels) in evaluate_selections'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter/runtime.rb:385:in `resolve_with_directives'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter/runtime.rb:221:in `block (2 levels) in evaluate_selections'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter/runtime.rb:460:in `after_lazy'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter/runtime.rb:179:in `block in evaluate_selections'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter/runtime.rb:126:in `each'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter/runtime.rb:126:in `evaluate_selections'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter/runtime.rb:58:in `run_eager'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter.rb:73:in `block in evaluate'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:81:in `call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `block in call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-guard-2.0.0/lib/graphql/guard.rb:42:in `trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `block in call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/platform_tracing.rb:26:in `block in trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/new_relic_tracing.rb:34:in `block in platform_trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/newrelic_rpm-6.3.0.355/lib/new_relic/agent/method_tracer_helpers.rb:30:in `trace_execution_scoped'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/new_relic_tracing.rb:33:in `platform_trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/platform_tracing.rb:25:in `trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:67:in `trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter.rb:72:in `evaluate'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/interpreter.rb:45:in `begin_query'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/multiplex.rb:112:in `begin_query'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/multiplex.rb:83:in `block in run_as_multiplex'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/multiplex.rb:82:in `map'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/multiplex.rb:82:in `run_as_multiplex'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/multiplex.rb:61:in `block (2 levels) in run_queries'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/multiplex.rb:195:in `block in instrument_and_analyze'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/instrumentation.rb:29:in `block (2 levels) in apply_instrumenters'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/instrumentation.rb:46:in `block (2 levels) in each_query_call_hooks'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/instrumentation.rb:41:in `each_query_call_hooks'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/instrumentation.rb:45:in `block in each_query_call_hooks'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/instrumentation.rb:72:in `call_hooks'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/instrumentation.rb:44:in `each_query_call_hooks'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/instrumentation.rb:27:in `block in apply_instrumenters'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/instrumentation.rb:72:in `call_hooks'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/instrumentation.rb:26:in `apply_instrumenters'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/multiplex.rb:174:in `instrument_and_analyze'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/multiplex.rb:60:in `block in run_queries'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:81:in `call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `block in call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-guard-2.0.0/lib/graphql/guard.rb:42:in `trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `block in call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/platform_tracing.rb:26:in `block in trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/new_relic_tracing.rb:34:in `block in platform_trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/newrelic_rpm-6.3.0.355/lib/new_relic/agent/method_tracer_helpers.rb:30:in `trace_execution_scoped'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/new_relic_tracing.rb:33:in `platform_trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing/platform_tracing.rb:25:in `trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:83:in `call_tracers'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/tracing.rb:67:in `trace'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/multiplex.rb:58:in `run_queries'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/execution/multiplex.rb:48:in `run_all'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/schema.rb:1656:in `multiplex'
     # /Users/sshaw/.rvm/gems/ruby-2.5.7/gems/graphql-1.11.5/lib/graphql/schema.rb:1627:in `execute'
<snip app trace>

Add support for arguments

I'd like to mask certain arguments so they are hidden in the schema. I'm currently playing around with this

# in an initializer
GraphQL::Argument.accepts_definitions(mask: GraphQL::Define.assign_metadata_key(:mask))
GraphQL::Schema::Argument.accepts_definition(:mask)

and then use it like this

argument :foo, String, required: false, mask: ->(ctx) { false }

Is this the right way to do it? And can someone explain why I need to define the mask on GraphQL::Argument AND GraphQL::Schema::Argument?

Add support for context to not_authorized callback when using Policy

This is a question as well as a suggestion.

I'm combining graphql-ruby and graphql-guard with Doorkeeper+Sorcery to handle my authentication. In my graphql_controller.rb I have:

def current_user
    token = Doorkeeper.authenticate(request)
    unless token&.accessible?
      raise UnauthenticatedError
    end

    current_resource_owner
  rescue UnauthenticatedError => e
    GraphQL::ExecutionError.new('Unauthenticated', extensions: { code: 'AUTHENTICATION_ERROR' })
end

And my policy is:

...
Types::WorkoutType => {
    '*': ->(obj, args, ctx) { obj.try(:author) == ctx[:current_user] || ctx[:current_user].try(:admin) }
},
...

So when a user is not authenticated, I expect the Unauthenticated error to be returned, but instead I get the Not authorized to access #{type}.#{field} defined in graphl_controller.rb.

Question:

Would it make sense to add the context to the callback so one could do something like this?

use GraphQL::Guard.new(
    policy_object: GraphqlPolicy,
    not_authorized: ->(type, field, ctx) do
      ctx.add_error(GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}"))
    end
)

That way we wouldn't remove any other errors that are in there and we can see that we are in fact unauthenticated as well as unauthorized.

guard lambdas are receiving the schema type object, rather than the found instance

Hi,

I just found this gem, and am looking to integrate it. I've run into an issue where my guard lambdas are receiving the graphql type object as the object argument, rather than the found instance that I expect (and the examples in the README also imply that this is what I should be receiving.) This behavior is showing up in all cases.

I'm currently using version 2.0.0, with the latest versions of all the related GraphQL gems.

My setup for testing this is currently

PROJECT_MEMBER_GUARD = lambda { |object, _args, context|
  user = context.fetch(:current_user, nil)
  ProjectMemberPolicy.new(user, object).show?
}

as the guard, which is attached via

field :project,
      Types::ProjectType,
      null: false,
      description: 'Get a single project',
      guard: PROJECT_MEMBER_GUARD do
  argument :id, ID, required: true, description: 'The project ID'
end

def project(id:)
  # Project.find id also fails
  RecordLoader.for(Project).load(id)
end

Am I doing something wrong? Without guard / auth checks, the queries run as expected. I'm completely baffled right now.

The specific error i'm receiving is "undefined method `to_global_id' for #<Types::QueryType:0x000056063dace2e0>", but the issue is that the object shouldn't be a Types::QueryType object, as far as my understanding goes.

Arguments using loads don't get loaded in mutations unless in a subtype

Apparent bug

It seems like arguments of mutations that use loads to automatically load the types from the database don't get loaded unless they are nested in another type. With queries I have not experienced any issues.

Environment

rails: 6.0.3
graphql_ruby: 1.11.6
graphql_guard: 2.0

Description

This query works fine. The facility entity is present in args[facility] in the GuardPolicy. args is a hash.

field :facility, Types::Facility::FacilityType, 'Returns a certain Facility',
                        null: false, authenticate: false do
        argument :facility_id, GraphQL::Types::ID, required: true, loads: Types::Facility::FacilityType
        argument :locale, String, required: true
      end

def facility(facility:, locale:)
  facility
end

This mutation does not work. There is a args[exhibitor] key ('_id' is stripped) but it contains the bare exhibitor_id. In comparison to the query, args is of type class.

class UpdateExhibitor < Mutations::BaseMutation
  null false
  
  argument :locale, String, required: true
  argument :exhibitor_id, ID, required: true, loads: Types::Exhibitor::ExhibitorType
  argument :data, Types::Exhibitor::ExhibitorUpdateInput, required: true
  
  type Types::Exhibitor::ExhibitorType
  
  def resolve(locale:, exhibitor:, data:)
    EntityInteraction.update_entity(exhibitor, data)
  end
end

This mutation does work. It contains the loaded argument in a 'nested' type. The exhibition entity is available in args[:data][:exhibition]:

class CreateEvent < Mutations::BaseMutation
  null false

  argument :locale, String, required: true
  argument :data, Types::Shared::AssociatedWithExhibitionCreateInput, required: true

  type Types::Event::EventType

  def resolve(locale:, data:)
    EntityInteraction.create_entity(::Event, data)
  end
end
class AssociatedWithExhibitionCreateInput < BaseCreateInput
  description 'Attributes for create action of entities associated with exhibition'
  argument :exhibition_id, ID, required: true, loads: Exhibition::ExhibitionType
end

Outdated Documentation

class ShopType < Types::BaseObject
  field :title, String, null: false
  field :user, Types::Models::UserType, null: false
end

How would you include a guard on this field for the ShopType, we would like to limit the field user to only signed in users, but allow the other fields on a shop to be public without the need for authentication.

Authorization on Enum Values

I'd like to be able to restrict certain enum input values to only super_admin users. Is there a way to do something like this? (This doesn't work obviously)

Types::OrganizationTypeEnumType = GraphQL::EnumType.define do
  name 'OrganizationTypeEnumType'
  description 'Enum for the possible organization types'

  value('PROVIDER', 'provider', value: 'provider'), guard: ->(obj, _args, ctx) { ctx[:current_user].super_admin? }
  value('CUSTOMER', 'customer', value: 'customer')
  value('THIRD_PARTY', 'third_party', value: 'third_party')
end

Cancancan documentation

Hi!

Thanks for your awesome's work on this gem!

I've followed your CanCanCan documentation but it didn't work until i've added this configuration:

# app/graph/graphql_schema.rb
GraphqlSchema = GraphQL::Schema.define do
  query QueryType
  mutation MutationType

  use GraphQL::Guard.new

  resolve_type -> (obj, args, ctx) {
    type_name = obj.class.name
    Schema.types[type_name]
  }
end

I use graphql-guard (~> 1.0.0) on rails 5.1

Did i missed something?

Skip authorization on return fields

I am having issues with mutations where I return a type that is guarded. Let's say for example I have a mutation where a user can reset the password with a token it got via a mail and I return a UserType.

mutation($reset_password_token: String, $password: String) {
    newPassword(
      reset_password_token: $reset_password_token,
      password: $password,
      password_confirmation: $password
  ) { id }
}

The problem is that the UserType is guarded but within the mutation I want to skip the UserType guard policy. Is this possible?

Pass field name to guard block

I have my abilities defined in CanCanCan on a field level basis and I'd like to authorize the fields and objects inside a general guard block. I have a BaseObject from which every GraphQL object inherits:

class BaseObject < GraphQL::Schema::Object
  guard ->(obj, _args, context) do
    context.fetch(:current_ability).can?(:read, obj.object)
  end
end

But I'm missing the field name here, I'd like to do something like this:

class BaseObject < GraphQL::Schema::Object
  guard ->(obj, _args, ctx, field) do
    ctx.fetch(:current_ability).can?(:read, obj.object, field)
  end
end

Am I missing something? Defining the guard block at every field would lead to a lot of duplication and very noisy code:

  field :name, String do
    guard ->(obj, args, ctx) {  ctx.fetch(:current_ability).can?(:read, obj.object, :name) }
  end

(of course one could define some helpers to reduce the noise, but it would still be "define the guard at every field" by hand).

I also had a look at your Policy Objects (see https://github.com/exAspArk/graphql-guard#policy-object) but I didn't have any luck with them (it seems to me that the .guard method of the policy object is only called once and not on every call, but I'm not sure).

So, is there a way to get the field inside the guard block to lazy evaluate if the field is authorized or not?

How to tell which record is causing `GraphQL::Guard::NotAuthorizedError` to be raised?

I'm getting the Not authorized to access: Xxxxx.id (GraphQL::Guard::NotAuthorizedError) error in my logs, but are often not sure which record it's talking about, and it's making it a real pain to debug issues.

It usually happens when a GraphQL query with multiple levels of nested fields is executed, and the error is something deeper in the structure, so it can't easily be worked out, by just looking at the arguments.

What I would like to do, is override the default error message so that it prints something like:

raise NotAuthorizedError.new("Not authorized to access: #{type}.#{field} for #{type}.id = #{trace_data[:object].try(:id)}")

and then I'll better understand which record it's referring to when I see this error.

Looking at the source code, I think that it's not currently possible since the 'object' is not passed to the proc. But just putting this here as I think it would be really useful to be able to do.

allow introspection field types to skip authentication

Hello,

Would you be open to making it configurable for users of graphql-guard to indicate that introspection fields should not be checked for authentication?

We've worked around this by using a policy_object, and checking for type.introspection? in the guard method of the policy_object itself, but it does feel like this should be offered out of the box.

If yes, I would even suggest it should be the default option, so that introspection fields would by default be "guard free", and if someone wants to hide their schema they would explicitly have to flip that option (but perhaps that's just me).

Happy to submit a PR if you think this is a sensible idea.

Cheers,
Vlad

Using guard in base/parent class always takes priority

If using guard ->(obj, args, ctx) { some_method } inside a base type class, and then inheriting from that class you cannot specify overrides per sub class or even per field. It will always use the guard from the parent class.

class ParentType < BaseType
  guard ->(obj, args, ctx) { false }
end

class ChildType < ParentType
  guard ->(obj, args, ctx) { true } # will not work

  field :id, ID, required: true, guard ->(obj, args, ctx) { true } # also will not work
end

GraphQL::Guard::NotAuthorizedError: 500 instead of 401. How to rescue?

This is NOT a bug and I immediately apologize for this issue but I'm newbie in Ruby, Rails and this amazing gem.

When I get this: GraphQL::Guard::NotAuthorizedError:

{
	"status": 500,
	"error": "Internal Server Error",
	"exception": "#<GraphQL::Guard::NotAuthorizedError: Query.users>",
	"traces": {
		"Application Trace": [
			{
				"id": 35,
				"trace": "app/controllers/graphql_controller.rb:11:in `execute'"
			}
		],
		"Framework Trace": [
			{
				"id": 0,
				"trace": "graphql-guard (1.0.0) lib/graphql/guard.rb:12:in `block in <class:Guard>'"
			},
			{
				"id": 1,
				"trace": "graphql-guard (1.0.0) lib/graphql/guard.rb:38:in `block in instrument'"
			},
			{
				"id": 2,
				"trace": "graphql (1.7.6) lib/graphql/field.rb:230:in `resolve'"
			},
			{
				"id": 3,
				"trace": "graphql (1.7.6) lib/graphql/execution/execute.rb:254:in `call'"
			},
			{
				"id": 4,
				"trace": "graphql (1.7.6) lib/graphql/schema/middleware_chain.rb:45:in `invoke_core'"
			},
			{
				"id": 5,
				"trace": "graphql (1.7.6) lib/graphql/schema/middleware_chain.rb:38:in `invoke'"
			}, ....................

I would prefer something like REST API Unauthorized error 401.

How to do that?

I have to rescue that exception?

Something like:

render json: ["Error 401. Unauthorized."], status: :unauthorized

type.introspection? || RULES.dig(type, field) doesn't working.

If I don't use this:

def self.guard(type, field)
  type.introspection? || RULES.dig(type, field)
end

I can get introspection types without problems which is not good:

Started POST "/graphql" for 172.18.0.1 at 2017-11-26 12:07:58 +0000
Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"\n  query IntrospectionQuery {\n    __schema {\n      queryType { name }\n      mutationType { name }\n      subscriptionType { name }\n      types {\n        ...FullType\n      }\n      directives {\n        name\n        description\n        locations\n        args {\n          ...InputValue\n        }\n      }\n    }\n  }\n\n  fragment FullType on __Type {\n    kind\n    name\n    description\n    fields(includeDeprecated: true) {\n      name\n      description\n      args {\n        ...InputValue\n      }\n      type {\n        ...TypeRef\n      }\n      isDeprecated\n      deprecationReason\n    }\n    inputFields {\n      ...InputValue\n    }\n    interfaces {\n      ...TypeRef\n    }\n    enumValues(includeDeprecated: true) {\n      name\n      description\n      isDeprecated\n      deprecationReason\n    }\n    possibleTypes {\n      ...TypeRef\n    }\n  }\n\n  fragment InputValue on __InputValue {\n    name\n    description\n    type { ...TypeRef }\n    defaultValue\n  }\n\n  fragment TypeRef on __Type {\n    kind\n    name\n    ofType {\n      kind\n      name\n      ofType {\n
        kind\n        name\n        ofType {\n          kind\n          name\n          ofType {\n            kind\n            name\n            ofType {\n              kind\n              name\n              ofType {\n
        kind\n                name\n                ofType {\n                  kind\n                  name\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n", "graphql"=>{"query"=>"\n  query IntrospectionQuery {\n    __schema {\n      queryType { name }\n      mutationType { name }\n      subscriptionType { name }\n      types {\n        ...FullType\n      }\n      directives {\n        name\n        description\n        locations\n        args {\n          ...InputValue\n        }\n      }\n    }\n  }\n\n  fragment FullType on __Type {\n    kind\n    name\n    description\n    fields(includeDeprecated: true) {\n      name\n
    description\n      args {\n        ...InputValue\n      }\n      type {\n        ...TypeRef\n      }\n      isDeprecated\n      deprecationReason\n    }\n    inputFields {\n      ...InputValue\n    }\n    interfaces {\n
 ...TypeRef\n    }\n    enumValues(includeDeprecated: true) {\n      name\n      description\n      isDeprecated\n      deprecationReason\n    }\n    possibleTypes {\n      ...TypeRef\n    }\n  }\n\n  fragment InputValue on __InputValue {\n    name\n    description\n    type { ...TypeRef }\n    defaultValue\n  }\n\n  fragment TypeRef on __Type {\n    kind\n    name\n    ofType {\n      kind\n      name\n      ofType {\n        kind\n        name\n
   ofType {\n          kind\n          name\n          ofType {\n            kind\n            name\n            ofType {\n              kind\n              name\n              ofType {\n                kind\n                name\n                ofType {\n                  kind\n                  name\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n"}}
[active_model_serializers] Rendered ActiveModel::Serializer::Null with GraphQL::Query::Result (12.33ms)
Completed 200 OK in 61ms (Views: 13.7ms | ActiveRecord: 0.0ms)

My GraphQLPolicy:

class GraphqlPolicy
  
  RULES = {
  
    Types::QueryType => {
      '*': ->(obj, args, ctx) { ctx[:current_user] } #nil
    }
  
  }

  def self.guard(type, field)
    RULES.dig(type, field)
  end

end

Why is introspection skipping authorization?

Does not seem to work with GraphQL::Execution::Interpreter

graphql gem v1.10 adds GraphQL::Execution::Interpreter by default in newly generated schemas, and that somehow sidesteps graphql-guard. For example, with following configuration:

class MySchema < GraphQL::Schema
  query(Types::QueryType)

  # Opt in to the new runtime (default in future graphql-ruby versions)
  use(GraphQL::Execution::Interpreter)
  use(GraphQL::Analysis::AST)

  use(GraphQL::Guard.new)
end

module Types
  class QueryType < Types::BaseObject
    guard ->(obj, args, ctx) { false }
    field :current_user, UserType, null: false
  end
end

the query

query {
  currentUser {
    id
  }
}

is successful. After commenting out

  use(GraphQL::Execution::Interpreter)
  use(GraphQL::Analysis::AST)

block, the same query returns an error, as expected.

Way to `nil` out the whole field rather than each of it's sub-fields if unauthorized to view it.

Right now we configure this library this way:

  use GraphQL::Guard.new(
    not_authorized: ->(type, field) { GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") }
  )

And we add gaurds on each of the types:

Types::LocationType = GraphQL::ObjectType.define do
  name 'Location'
  guard ->(location, _args, context) { context[:current_ability].can?(:read, location) }
end

(We use cancancan as our ability library)

However this query...

{
  location(id:12) {
    id
    users {name}
  }
}

Has a semi-odd behavior:

{
  "data": {
    "location": {
      "id": null,
      "users": null
    }
  },
  "errors": [
    {
      "message": "Not authorized to access Location.id",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "location",
        "id"
      ]
    },
    {
      "message": "Not authorized to access Location.users",
      "locations": [
        {
          "line": 4,
          "column": 5
        }
      ],
      "path": [
        "location",
        "users"
      ]
    }
  ]
}

Rather than:

{
  "data": {
    "location": null
  },
  "errors": [
    {
      "message": "Not authorized to access Location id=12",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "location"
      ]
    }
  ]
}

Is there an easy way to do that with this library?

we managed to resolve it by doing:

  field :location, Types::LocationType do
    argument :id, !types.ID
    description 'Find a Location by ID'

    resolve(lambda do |_obj, args, context|
      location = Location.find_by(id: args['id'])
      if location.present? &&
         context[:current_ability].cannot?(:read, location)
        return GraphQL::ExecutionError.new("Not authorized to access Location id=#{args['id']}")
      end
      location
    end)
  end

However we have to do that for every field we want to protect. 🤷‍♂️

Usage with graphql-ruby 1.8 class syntax

graphql-ruby 1.8 is introducing a new class-based syntax, however it seems to break compatibility with graphql-guard.

class Types::Query < GraphQL::Schema::Object
  field :posts, [Types::Post], null: false do
    guard ->(obj, args, ctx) { true }
  end
end

results in undefined method 'guard' for GraphQL::Schema::Field

Is there a potential workaround (aside from just not using the new syntax)?

Policy lambda receiving inconsistent value in test

I am using a graphql policy like so:

      Types::UserType => {
          email: {
              guard: ->(obj, args, ctx) { puts obj.inspect; UserPolicy.new(ctx[:current_user], obj).show_protected_fields? }
          },
...

This works great. If I use the graphql endpoint and request a user:

{
  user(id: 1){
  	name
        email
  }
}

It works fine, and the output in the console is :

 #<Types::UserType:0x00007f03c20e5750 @object=#<User member_id: 1, email: "[email protected]" ...

However, when I run the same query in rspec, the guard lambda is seemingly receiving the same object (the structure is the same, not the contents).

#<Types::UserType:0x000055a2afb7d6b0 @object=#<User member_id: 10288

However, all my tests fail with:

       undefined method `member_id' for #<Types::UserType:0x0000562c884e4f20>
     # ./app/policies/user_policy.rb:10:in `show_protected_fields?'
     # ./app/graphql/graphql_policy.rb:10:in `block in <class:GraphqlPolicy>'
     # /usr/local/bundle/gems/graphql-guard-2.0.0/lib/graphql/guard.rb:70:in `ensure_guarded'
     # /usr/local/bundle/gems/graphql-guard-2.0.0/lib/graphql/guard.rb:40:in `trace'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/tracing.rb:84:in `call_tracers'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/tracing.rb:68:in `trace'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:225:in `block (4 levels) in evaluate_selections'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/query.rb:354:in `block in with_error_handling'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/errors.rb:30:in `with_error_handling'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/query.rb:353:in `with_error_handling'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:224:in `block (3 levels) in evaluate_selections'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:382:in `resolve_with_directives'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:221:in `block (2 levels) in evaluate_selections'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:457:in `after_lazy'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:177:in `block in evaluate_selections'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:124:in `each'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:124:in `evaluate_selections'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:334:in `block in continue_field'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:457:in `after_lazy'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:329:in `continue_field'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:375:in `continue_field'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:238:in `block (4 levels) in evaluate_selections'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:457:in `after_lazy'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:232:in `block (3 levels) in evaluate_selections'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:382:in `resolve_with_directives'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:221:in `block (2 levels) in evaluate_selections'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:457:in `after_lazy'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:177:in `block in evaluate_selections'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:124:in `each'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:124:in `evaluate_selections'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter/runtime.rb:60:in `run_eager'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter.rb:73:in `block in evaluate'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/tracing.rb:82:in `call_tracers'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/tracing.rb:84:in `block in call_tracers'
     # /usr/local/bundle/gems/graphql-guard-2.0.0/lib/graphql/guard.rb:42:in `trace'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/tracing.rb:84:in `call_tracers'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/tracing.rb:68:in `trace'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter.rb:72:in `evaluate'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/interpreter.rb:45:in `begin_query'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/multiplex.rb:113:in `begin_query'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/multiplex.rb:84:in `block in run_as_multiplex'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/multiplex.rb:83:in `map'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/multiplex.rb:83:in `run_as_multiplex'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/multiplex.rb:62:in `block (2 levels) in run_queries'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/multiplex.rb:196:in `block in instrument_and_analyze'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/instrumentation.rb:29:in `block (2 levels) in apply_instrumenters'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/instrumentation.rb:46:in `block (2 levels) in each_query_call_hooks'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/instrumentation.rb:41:in `each_query_call_hooks'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/instrumentation.rb:45:in `block in each_query_call_hooks'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/instrumentation.rb:72:in `call_hooks'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/instrumentation.rb:44:in `each_query_call_hooks'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/instrumentation.rb:27:in `block in apply_instrumenters'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/instrumentation.rb:72:in `call_hooks'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/instrumentation.rb:26:in `apply_instrumenters'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/multiplex.rb:175:in `instrument_and_analyze'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/multiplex.rb:61:in `block in run_queries'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/tracing.rb:82:in `call_tracers'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/tracing.rb:84:in `block in call_tracers'
     # /usr/local/bundle/gems/graphql-guard-2.0.0/lib/graphql/guard.rb:42:in `trace'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/tracing.rb:84:in `call_tracers'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/tracing.rb:68:in `trace'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/multiplex.rb:59:in `run_queries'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/execution/multiplex.rb:49:in `run_all'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/schema.rb:1633:in `multiplex'
     # /usr/local/bundle/gems/graphql-1.10.8/lib/graphql/schema.rb:1604:in `execute'

I am using the latest version of graphql, graphql-guard and pundit. My UserPolicy for reference:


  def show?
    true
  end

  def show_protected_fields?
    puts record.to_json
    !user.nil? && (user.admin? || user.member_id == record.member_id)
  end
end```

Hiding unauthorized fields from introspection?

Looking at the docs and other issues I don't think I've seen an exact question like mine, so here goes:

Is there a way to hide a field from introspection if the user is unauthorized to see it? I'd like to have a schema that mixes public and internal fields, and hides the internal fields completely from external users of the application. Is this possible?

Pundit field whitelisting

First of all, thanks for this great gem! I love the simplicity.

By default this gem follows a blacklisting strategy where you disable access to queries, fields, etc. I'd like it the other way around and whitelist everything that is allowed to be used.

So far I have an implementation that works (based on other discussions on this gem).

The problem with my implementation is that for each field I will need to have a proc which tests the access.

See: https://gist.github.com/yourivdlans/7d1093e5500820804a7ca8d263c98ecf

This might grow to something unmanageable which I'd like to avoid.

What could work is if the field name would be passed into the proc, but I'm not sure if this is the right approach.

Would love to hear your opinion and thoughts :)

Thanks!

Testing get an error with Rspec

I have created a spec file as below:

require 'graphql/guard/testing'

RSpec.describe "GraphQL Guard" do
  it 'Authorization for rentals API' do
    rentals = Types::QueryType.field_with_guard('rentals', GraphqlPolicy)
    result = rentals.guard(obj, args, ctx)
    expect(result).to eq(true)
  end
end

When I try to run rspec, I got an error as followings:

     
NameError:
undefined local variable or method `obj' for #<RSpec::ExampleGroups::GraphQLGuard:0x0000559aa47de350>

Please let me know what I'm wrong?

[Bug] Guarding or masking a field on an Object Type hides the entire object and not just the individual field

Versions
graphql 1.11.2
graphql-guard 2.0.0

Example Code

module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: true
    field :masked_or_guarded_field, String, null: true, mask: -> (ctx) { !ctx[:current_user].is_admin?} # or, guard: -> (obj, args, ctx) { !ctx[:current_user].is_admin?}
  end
end

Expected Response

{
  "id": "1",
  "name": "This is a name",
  "masked_or_guarded_field": null, // or just not there at all
}

Actual Response

null

I've also tried it using this syntax with the same results:

field :masked_or_guarded_field, String, null: true do
  mask: -> (ctx) { !ctx[:current_user].is_admin?} # or, guard: -> (obj, args, ctx) { !ctx[:current_user].is_admin?}
end

Let me know if I can provide any more information!

not_authorized calls with Interface type instead of the real owner

When handling the errors on field which is defined in an Interface, field.owner returns the interface module, instead of the real owner (the subclass of GraphQL::Schema::Object). Probably it should be changed to trace_data[:owner]?

    def ensure_guarded(trace_data)
      field = trace_data[:field]
      guard_proc = find_guard_proc(field.owner, field)
      return yield unless guard_proc

      if guard_proc.call(trace_data[:object], args(trace_data), trace_data[:query].context)
        yield
      else
        not_authorized.call(field.owner.graphql_definition, field.name.to_sym)
      end
    end

Type-level guard taking higher priority than field-level

Hi! I am trying to setup field-level and type-level permissioning, and I'm running to an issue where the type guard seems to have a higher priority than the field. Here is my code:

module Types
  ExampleType = GraphQL::ObjectType.define do
    name "Example"
    description "Example Description"

    implements GraphQL::Relay::Node.interface

    guard ->(obj, _args, context) { false }

    global_id_field :id
    field :weight, types.String
    field :pulse, types.String
    field :height do
      type types.String
      guard ->(obj, args, context) { true }

      resolve lambda {|obj, _args, _context|
        obj.height
      }
    end
  end
end

I'm not able to see any of the fields, including height, even though the guard returns true for height.

Changing NotAuthorizedError to Be A Subclass of GraphQL::ExecutionError

Yes, breaking change but I was thinking with NotAuthorizedError as a superclass one gets proper error response formatting for free, as GraphQL Ruby will insert the exception's message into the "errors" property of the response.

And related, how do you feel about changing the default message of NotAuthorizedError from Query.queryName to Not authorized to access: Query.queryName (or similar)?

I just started using this and saw 500 errors with a log message of "Query.queryName". Not a helpful message.

Use on resolver

Hi,

Can you add guard to Resolver

Thanks for your open source contributions!

Compatibility with graphql-ruby's new interpreter

At first, thank you very much for the great gem, it saved us a lot of headaches! I just came across that the new (experimental) interpreter of graphql-ruby 1.9 does not work with graphql-guard (probably because it doesn't call lambdas/procs anymore?). Is there a plan to support the new interpreter in the future?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.