Skip to content

Commit

Permalink
Add support for GraphQL gem version 1.10+ with the Interpreter
Browse files Browse the repository at this point in the history
  • Loading branch information
exAspArk committed Apr 7, 2020
1 parent 7ac7c6e commit 87e5c87
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 154 deletions.
106 changes: 41 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,40 +36,39 @@ Define a GraphQL schema:

```ruby
# Define a type
PostType = GraphQL::ObjectType.define do
name "Post"

field :id, !types.ID
field :title, types.String
class PostType < GraphQL::Schema::Object
field :id, ID, null: false
field :title, String, null: true
end

# Define a query
QueryType = GraphQL::ObjectType.define do
name "Query"
class QueryType < GraphQL::Schema::Object
field :posts, [PostType], null: false do
argument :user_id, ID, required: true
end

field :posts, !types[!PostType] do
argument :user_id, !types.ID
resolve ->(obj, args, ctx) { Post.where(user_id: args[:user_id]) }
def posts(user_id:)
Post.where(user_id: user_id)
end
end

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

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

### Inline policies

Add `GraphQL::Guard` to your schema:

<pre>
Schema = GraphQL::Schema.define do
class Schema < GraphQL::Schema
use GraphQL::Execution::Interpreter
use GraphQL::Analysis::AST
query QueryType
Expand All @@ -80,22 +79,19 @@ end
Now you can define `guard` for a field, which will check permissions before resolving the field:

<pre>
QueryType = GraphQL::ObjectType.define do
name "Query"

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

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

<pre>
PostType = GraphQL::ObjectType.define do
name "Post"
class PostType < GraphQL::Schema::Object
<b>guard ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b>
...
end
Expand Down Expand Up @@ -124,27 +120,10 @@ class <b>GraphqlPolicy</b>
end
</pre>

With `graphql-ruby` gem version >= 1.8 and class-based type definitions, use `camelCased` field names in the policy object.
You'd also need to use `type.metadata` (related to [rmosolgo/graphql-ruby#1429](https://github.com/rmosolgo/graphql-ruby/issues/1429)) to get the type class:

<pre>
class GraphqlPolicy
RULES = {
MutationType => {
<b>createPost</b>: ->(obj, args, cts) { ctx[:current_user].admin? }
}
}

def self.guard(type, field)
RULES.dig(<b>type.metadata[:type_class]</b>, field)
end
end
</pre>

Pass this object to `GraphQL::Guard`:

<pre>
Schema = GraphQL::Schema.define do
class Schema < GraphQL::Schema
use GraphQL::Execution::Interpreter
use GraphQL::Analysis::AST
query QueryType
Expand Down Expand Up @@ -173,8 +152,8 @@ end
class <b>GraphqlPolicy</b>
RULES = {
PostType => {
<b>'*': ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b>, # <=== <b>4</b>
<b>title: ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b> # <=== <b>2</b>
<b>'*': ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b>, # <=== <b>4</b>
<b>title: ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b> # <=== <b>2</b>
}
}

Expand All @@ -183,13 +162,12 @@ class <b>GraphqlPolicy</b>
end
end

PostType = GraphQL::ObjectType.define do
name "Post"
<b>guard ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b> # <=== <b>3</b>
<b>field :title</b>, !types.String, <b>guard: ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b> # <=== <b>1</b>
class PostType < GraphQL::Schema::Object
<b>guard ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b> # <=== <b>3</b>
field :title, String, null: true, <b>guard: ->(obj, args, ctx) {</b> ctx[:current_user].admin? <b>}</b> # <=== <b>1</b>
end

Schema = GraphQL::Schema.define do
class Schema < GraphQL::Schema
use GraphQL::Execution::Interpreter
use GraphQL::Analysis::AST
query QueryType
Expand Down Expand Up @@ -219,8 +197,7 @@ class <b>Ability</b>
end

# Use the ability in your guard
PostType = GraphQL::ObjectType.define do
name "Post"
class PostType < GraphQL::Schema::Object
guard ->(post, args, ctx) { <b>ctx[:current_ability].can?(:read, post)</b> }
...
end
Expand All @@ -240,8 +217,7 @@ class <b>PostPolicy</b> < ApplicationPolicy
end

# Use the ability in your guard
PostType = GraphQL::ObjectType.define do
name "Post"
class PostType < GraphQL::Schema::Object
guard ->(post, args, ctx) { <b>PostPolicy.new(ctx[:current_user], post).show?</b> }
...
end
Expand All @@ -256,16 +232,20 @@ By default `GraphQL::Guard` raises a `GraphQL::Guard::NotAuthorizedError` except
You can change this behavior, by passing custom `not_authorized` lambda. For example:

<pre>
SchemaWithErrors = GraphQL::Schema.define do
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) { raise GraphQL::Guard::NotAuthorizedError.new("#{type}.#{field}") }
# not_authorized: ->(type, field) do
# raise GraphQL::Guard::NotAuthorizedError.new("#{type.graphql_definition}.#{field}")
# end

# Returns an error in the response
<b>not_authorized: ->(type, field) { GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") }</b>
<b>not_authorized: ->(type, field) do
GraphQL::ExecutionError.new("Not authorized to access #{type.graphql_definition}.#{field}")
end</b>
)
end
</pre>
Expand Down Expand Up @@ -310,7 +290,7 @@ class <b>GraphqlPolicy</b>
end
end

Schema = GraphQL::Schema.define do
class Schema < GraphQL::Schema
use GraphQL::Execution::Interpreter
use GraphQL::Analysis::AST
query QueryType
Expand All @@ -331,11 +311,9 @@ end
It's possible to hide fields from being introspectable and accessible based on the context. For example:

<pre>
PostType = GraphQL::ObjectType.define do
name "Post"

field :id, !types.ID
field :title, types.String do
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
<b>mask ->(ctx) {</b> ctx[:current_user].beta_tester? <b>}</b>
end
Expand Down Expand Up @@ -364,9 +342,8 @@ It's possible to test fields with `guard` in isolation:

<pre>
# Your type
QueryType = GraphQL::ObjectType.define do
name "Query"
<b>field :posts</b>, !types[!PostType], <b>guard ->(obj, args, ctx) {</b> ... <b>}</b>
class QueryType < GraphQL::Schema::Object
field :posts, [PostType], null: false, <b>guard ->(obj, args, ctx) {</b> ... <b>}</b>
end

# Your test
Expand All @@ -382,9 +359,8 @@ If you would like to test your fields with policy objects:

<pre>
# Your type
QueryType = GraphQL::ObjectType.define do
name "Query"
<b>field :posts</b>, !types[!PostType]
class QueryType < GraphQL::Schema::Object
field :posts, [PostType], null: false
end

# Your policy object
Expand Down
2 changes: 1 addition & 1 deletion graphql-guard.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Gem::Specification.new do |spec|

spec.required_ruby_version = '>= 2.1.0' # keyword args

spec.add_runtime_dependency "graphql", ">= 1.6.0", "< 2"
spec.add_runtime_dependency "graphql", ">= 1.10.0", "< 2"

spec.add_development_dependency "bundler", "~> 1.15"
spec.add_development_dependency "rake", "~> 13.0"
Expand Down
89 changes: 38 additions & 51 deletions lib/graphql/guard.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
# frozen_string_literal: true

require "graphql"
require "graphql/guard/field_extension"
require "graphql/guard/version"

module GraphQL
class Guard
NotAuthorizedError = Class.new(StandardError)

ANY_FIELD_NAME = :'*'
DEFAULT_NOT_AUTHORIZED = ->(type, field) { raise NotAuthorizedError.new("Not authorized to access: #{type}.#{field}") }

NotAuthorizedError = Class.new(StandardError)
DEFAULT_NOT_AUTHORIZED = ->(type, field) do
raise NotAuthorizedError.new("Not authorized to access: #{type.graphql_definition}.#{field}")
end

MASKING_FILTER = ->(schema_member, ctx) do
mask = schema_member.graphql_definition.metadata[:mask]
mask ? mask.call(ctx) : true
end

attr_reader :policy_object, :not_authorized

Expand All @@ -18,75 +27,53 @@ def initialize(policy_object: nil, not_authorized: DEFAULT_NOT_AUTHORIZED)
end

def use(schema_definition)
schema_definition.instrument(:field, self)
add_schema_masking!(schema_definition)
end

def instrument(type, field)
guard_proc = guard_proc(type, field)
return field unless guard_proc

old_resolve_proc = field.resolve_proc
new_resolve_proc = ->(object, arguments, context) do
authorized = guard_proc.call(object, arguments, context)

if authorized
old_resolve_proc.call(object, arguments, context)
else
not_authorized.call(type, field.name.to_sym)
if schema_definition.interpreter?
fields(schema_definition).each do |field|
field.type_class.extension(GraphQL::Guard::FieldExtension, guard_instance: self)
end
else
raise "Please use the graphql gem version >= 1.10 with GraphQL::Execution::Interpreter"
end

field.redefine { resolve(new_resolve_proc) }
add_schema_masking!(schema_definition)
end

def guard_proc(type, field)
inline_field_guard(field) ||
def find_guard_proc(type, field)
inline_guard(field) ||
policy_object_guard(type, field.name.to_sym) ||
inline_type_guard(type) ||
inline_guard(type) ||
policy_object_guard(type, ANY_FIELD_NAME)
end

private

def fields(schema_definition)
schema_definition.types.values.flat_map { |type|
type.fields.values if type.name && type.respond_to?(:fields)
}.compact
end

def add_schema_masking!(schema_definition)
default_filter_proc = Proc.new do
def default_filter
GraphQL::Filter.new(except: default_mask).merge(only: ->(schema_member, ctx) {
schema_member.metadata[:mask] ? schema_member.metadata[:mask].call(ctx) : true
})
schema_definition.class_eval do
def self.default_filter
GraphQL::Filter.new(except: default_mask).merge(only: MASKING_FILTER)
end
end

if schema_definition.is_a?(Class) # GraphQL-Ruby version >= 1.10
schema_definition.class_eval(&default_filter_proc)
else
schema_definition.target.instance_eval(&default_filter_proc)
end
end

def policy_object_guard(type, field_name)
policy_object && policy_object.guard(type, field_name)
@policy_object && @policy_object.guard(type.type_class, field_name)
end

def inline_field_guard(field)
field.metadata[:guard]
end

def inline_type_guard(type)
type.metadata[:guard]
def inline_guard(type_or_field)
type_or_field.graphql_definition.metadata[:guard]
end
end
end

if GraphQL::ObjectType.respond_to?(:accepts_definitions) # GraphQL-Ruby version < 1.8
GraphQL::ObjectType.accepts_definitions(guard: GraphQL::Define.assign_metadata_key(:guard))
GraphQL::Field.accepts_definitions(guard: GraphQL::Define.assign_metadata_key(:guard))
GraphQL::Field.accepts_definitions(mask: GraphQL::Define.assign_metadata_key(:mask))
end

if defined?(GraphQL::Schema::Object) && GraphQL::Schema::Object.respond_to?(:accepts_definition) # GraphQL-Ruby version >= 1.8
GraphQL::Schema::Object.accepts_definition(:guard)
GraphQL::Schema::Field.accepts_definition(:guard)
GraphQL::Schema::Field.accepts_definition(:mask)
end
GraphQL::ObjectType.accepts_definitions(guard: GraphQL::Define.assign_metadata_key(:guard))
GraphQL::Field.accepts_definitions(guard: GraphQL::Define.assign_metadata_key(:guard))
GraphQL::Field.accepts_definitions(mask: GraphQL::Define.assign_metadata_key(:mask))
GraphQL::Schema::Object.accepts_definition(:guard)
GraphQL::Schema::Field.accepts_definition(:guard)
GraphQL::Schema::Field.accepts_definition(:mask)
16 changes: 16 additions & 0 deletions lib/graphql/guard/field_extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module GraphQL
class Guard
class FieldExtension < GraphQL::Schema::FieldExtension
def resolve(object:, arguments:, **rest)
guard_proc = options[:guard_instance].find_guard_proc(field.owner, field)
return yield(object, arguments) unless guard_proc

if guard_proc.call(object, arguments, rest[:context])
yield(object, arguments)
else
options[:guard_instance].not_authorized.call(field.owner, field.name.to_sym)
end
end
end
end
end
Loading

0 comments on commit 87e5c87

Please sign in to comment.