graphql-pundit2 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.codeclimate.yml +16 -0
- data/.gitignore +15 -0
- data/.hound.yml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +95 -0
- data/.ruby-version +1 -0
- data/.stickler.yml +7 -0
- data/.travis.yml +19 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +368 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/changelog.md +13 -0
- data/graphql-pundit2.gemspec +40 -0
- data/lib/graphql-pundit/authorization.rb +93 -0
- data/lib/graphql-pundit/common.rb +34 -0
- data/lib/graphql-pundit/field.rb +17 -0
- data/lib/graphql-pundit/instrumenter.rb +34 -0
- data/lib/graphql-pundit/instrumenters/after_scope.rb +24 -0
- data/lib/graphql-pundit/instrumenters/authorization.rb +77 -0
- data/lib/graphql-pundit/instrumenters/before_scope.rb +30 -0
- data/lib/graphql-pundit/instrumenters/scope.rb +90 -0
- data/lib/graphql-pundit/scope.rb +62 -0
- data/lib/graphql-pundit/version.rb +7 -0
- data/lib/graphql-pundit2.rb +53 -0
- metadata +258 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql-pundit/common'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Pundit
|
7
|
+
# Authorization methods to be included in the used Field class
|
8
|
+
module Authorization
|
9
|
+
def self.prepended(base)
|
10
|
+
base.include(GraphQL::Pundit::Common)
|
11
|
+
end
|
12
|
+
|
13
|
+
# rubocop:disable Metrics/ParameterLists
|
14
|
+
def initialize(*args, authorize: nil,
|
15
|
+
record: nil,
|
16
|
+
policy: nil,
|
17
|
+
**kwargs, &block)
|
18
|
+
# rubocop:enable Metrics/ParameterLists
|
19
|
+
# authorize! is not a valid variable name
|
20
|
+
authorize_bang = kwargs.delete(:authorize!)
|
21
|
+
@record = record if record
|
22
|
+
@policy = policy if policy
|
23
|
+
@authorize = authorize_bang || authorize
|
24
|
+
@do_raise = !!authorize_bang
|
25
|
+
super(*args, policy: policy, record: record, **kwargs, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def authorize(*args, record: nil, policy: nil)
|
29
|
+
@authorize = args[0] || true
|
30
|
+
@record = record if record
|
31
|
+
@policy = policy if policy
|
32
|
+
end
|
33
|
+
|
34
|
+
def authorize!(*args, record: nil, policy: nil)
|
35
|
+
@do_raise = true
|
36
|
+
authorize(*args, record: record, policy: policy)
|
37
|
+
end
|
38
|
+
|
39
|
+
def resolve(obj, args, ctx)
|
40
|
+
raise ::Pundit::NotAuthorizedError unless do_authorize(obj, args, ctx)
|
41
|
+
|
42
|
+
super(obj, args, ctx)
|
43
|
+
rescue ::Pundit::NotAuthorizedError
|
44
|
+
raise GraphQL::ExecutionError, "You're not authorized to do this" if @do_raise
|
45
|
+
end
|
46
|
+
|
47
|
+
alias resolve_field resolve
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def do_authorize(root, arguments, context)
|
52
|
+
return true unless @authorize
|
53
|
+
return @authorize.call(root, arguments, context) if callable? @authorize
|
54
|
+
|
55
|
+
query = infer_query(@authorize)
|
56
|
+
record = infer_record(@record, root, arguments, context)
|
57
|
+
policy = infer_policy(@policy, record, arguments, context)
|
58
|
+
|
59
|
+
policy.new(context[self.class.current_user], record).public_send query
|
60
|
+
end
|
61
|
+
|
62
|
+
def infer_query(auth_value)
|
63
|
+
# authorize can be callable, true (for inference) or a policy query
|
64
|
+
query = auth_value.equal?(true) ? method_sym : auth_value
|
65
|
+
"#{query}?"
|
66
|
+
end
|
67
|
+
|
68
|
+
def infer_record(record, root, arguments, context)
|
69
|
+
# record can be callable, nil (for inference) or just any other value
|
70
|
+
if callable?(record)
|
71
|
+
record.call(root, arguments, context)
|
72
|
+
elsif record.equal?(nil)
|
73
|
+
root
|
74
|
+
else
|
75
|
+
record
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def infer_policy(policy, record, arguments, context)
|
80
|
+
# policy can be callable, nil (for inference) or a policy class
|
81
|
+
if callable?(policy)
|
82
|
+
policy.call(record, arguments, context)
|
83
|
+
elsif policy.equal?(nil)
|
84
|
+
infer_from = model?(record) ? record.model : record
|
85
|
+
infer_from = object?(record) ? record.object : infer_from
|
86
|
+
::Pundit::PolicyFinder.new(infer_from).policy!
|
87
|
+
else
|
88
|
+
policy
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Pundit
|
5
|
+
# Common methods used for authorization and scopes
|
6
|
+
module Common
|
7
|
+
# Class methods to be included in the Field class
|
8
|
+
module ClassMethods
|
9
|
+
def current_user(current_user = nil)
|
10
|
+
return @current_user unless current_user
|
11
|
+
|
12
|
+
@current_user = current_user
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.included(base)
|
17
|
+
@current_user = :current_user
|
18
|
+
base.extend(ClassMethods)
|
19
|
+
end
|
20
|
+
|
21
|
+
def callable?(thing)
|
22
|
+
thing.respond_to?(:call)
|
23
|
+
end
|
24
|
+
|
25
|
+
def model?(thing)
|
26
|
+
thing.respond_to?(:model)
|
27
|
+
end
|
28
|
+
|
29
|
+
def object?(thing)
|
30
|
+
thing.respond_to?(:object)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
require 'graphql-pundit/authorization'
|
5
|
+
require 'graphql-pundit/scope'
|
6
|
+
|
7
|
+
module GraphQL
|
8
|
+
# Our custom pundit module
|
9
|
+
module Pundit
|
10
|
+
# Field class that contains authorization and scope behavior
|
11
|
+
# This only works with graphql >= 1.8.0
|
12
|
+
class Field < GraphQL::Schema::Field
|
13
|
+
prepend GraphQL::Pundit::Scope
|
14
|
+
prepend GraphQL::Pundit::Authorization
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
require 'graphql-pundit/instrumenters/authorization'
|
5
|
+
require 'graphql-pundit/instrumenters/before_scope'
|
6
|
+
require 'graphql-pundit/instrumenters/after_scope'
|
7
|
+
|
8
|
+
module GraphQL
|
9
|
+
module Pundit
|
10
|
+
# Intrumenter combining the authorization and scope instrumenters
|
11
|
+
class Instrumenter
|
12
|
+
attr_reader :current_user,
|
13
|
+
:authorization_instrumenter,
|
14
|
+
:before_scope_instrumenter,
|
15
|
+
:after_scope_instrumenter
|
16
|
+
|
17
|
+
def initialize(current_user = :current_user)
|
18
|
+
@current_user = current_user
|
19
|
+
@authorization_instrumenter =
|
20
|
+
Instrumenters::Authorization.new(current_user)
|
21
|
+
@before_scope_instrumenter =
|
22
|
+
Instrumenters::BeforeScope.new(current_user)
|
23
|
+
@after_scope_instrumenter = Instrumenters::AfterScope.new(current_user)
|
24
|
+
end
|
25
|
+
|
26
|
+
def instrument(type, field)
|
27
|
+
before_scoped_field = before_scope_instrumenter.instrument(type, field)
|
28
|
+
after_scoped_field = after_scope_instrumenter
|
29
|
+
.instrument(type, before_scoped_field)
|
30
|
+
authorization_instrumenter.instrument(type, after_scoped_field)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
require_relative 'scope'
|
5
|
+
|
6
|
+
module GraphQL
|
7
|
+
module Pundit
|
8
|
+
module Instrumenters
|
9
|
+
# Instrumenter that supplies `after_scope`
|
10
|
+
class AfterScope < Scope
|
11
|
+
SCOPE_KEY = :after_scope
|
12
|
+
|
13
|
+
# Applies the scoping to the passed object
|
14
|
+
class ScopeResolver < ScopeResolver
|
15
|
+
def call(root, arguments, context)
|
16
|
+
resolver_result = old_resolver.call(root, arguments, context)
|
17
|
+
scope_proc = new_scope(scope)
|
18
|
+
scope_proc.call(resolver_result, arguments, context)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Pundit
|
7
|
+
module Instrumenters
|
8
|
+
# Instrumenter that supplies `authorize`
|
9
|
+
class Authorization
|
10
|
+
# This does the actual Pundit authorization
|
11
|
+
class AuthorizationResolver
|
12
|
+
attr_reader :current_user, :old_resolver, :options
|
13
|
+
|
14
|
+
def initialize(current_user, old_resolver, options)
|
15
|
+
@current_user = current_user
|
16
|
+
@old_resolver = old_resolver
|
17
|
+
@options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(root, arguments, context)
|
21
|
+
raise ::Pundit::NotAuthorizedError unless authorize(root, arguments, context)
|
22
|
+
|
23
|
+
old_resolver.call(root, arguments, context)
|
24
|
+
rescue ::Pundit::NotAuthorizedError
|
25
|
+
raise GraphQL::ExecutionError, "You're not authorized to do this" if options[:raise]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def authorize(root, arguments, context)
|
31
|
+
if options[:proc]
|
32
|
+
options[:proc].call(root, arguments, context)
|
33
|
+
else
|
34
|
+
record = record(root, arguments, context)
|
35
|
+
::Pundit::PolicyFinder.new(policy(record)).policy!
|
36
|
+
.new(context[current_user], record).public_send(query)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def query
|
41
|
+
@query ||= "#{options[:query]}?"
|
42
|
+
end
|
43
|
+
|
44
|
+
def policy(record)
|
45
|
+
options[:policy] || record
|
46
|
+
end
|
47
|
+
|
48
|
+
def record(root, arguments, context)
|
49
|
+
if options[:record].respond_to?(:call)
|
50
|
+
options[:record].call(root, arguments, context)
|
51
|
+
else
|
52
|
+
options[:record] || root
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :current_user
|
58
|
+
|
59
|
+
def initialize(current_user = :current_user)
|
60
|
+
@current_user = current_user
|
61
|
+
end
|
62
|
+
|
63
|
+
def instrument(_type, field)
|
64
|
+
return field unless field.metadata[:authorize]
|
65
|
+
|
66
|
+
old_resolver = field.resolve_proc
|
67
|
+
resolver = AuthorizationResolver.new(current_user,
|
68
|
+
old_resolver,
|
69
|
+
field.metadata[:authorize])
|
70
|
+
field.redefine do
|
71
|
+
resolve resolver
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
require_relative 'scope'
|
5
|
+
|
6
|
+
module GraphQL
|
7
|
+
module Pundit
|
8
|
+
module Instrumenters
|
9
|
+
# Instrumenter that supplies `before_scope`
|
10
|
+
class BeforeScope < Scope
|
11
|
+
SCOPE_KEY = :before_scope
|
12
|
+
|
13
|
+
# Applies the scoping to the passed object
|
14
|
+
class ScopeResolver < ScopeResolver
|
15
|
+
def call(root, arguments, context)
|
16
|
+
if field.metadata[:before_scope][:deprecated]
|
17
|
+
Kernel.warn <<~DEPRECATION_WARNING
|
18
|
+
Using `scope` is deprecated and might be removed in the future.
|
19
|
+
Please use `before_scope` or `after_scope` instead.
|
20
|
+
DEPRECATION_WARNING
|
21
|
+
end
|
22
|
+
scope_proc = new_scope(scope)
|
23
|
+
resolver_result = scope_proc.call(root, arguments, context)
|
24
|
+
old_resolver.call(resolver_result, arguments, context)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Pundit
|
7
|
+
module Instrumenters
|
8
|
+
# Base instrumenter for `before_scope` and `after_scope`
|
9
|
+
class Scope
|
10
|
+
# Applies the scoping to the passed object
|
11
|
+
class ScopeResolver
|
12
|
+
attr_reader :current_user, :scope, :old_resolver, :field
|
13
|
+
|
14
|
+
def initialize(current_user, scope, old_resolver, field)
|
15
|
+
@current_user = current_user
|
16
|
+
@old_resolver = old_resolver
|
17
|
+
@field = field
|
18
|
+
|
19
|
+
raise ArgumentError, 'Invalid value passed to `scope`' unless valid_value?(scope)
|
20
|
+
|
21
|
+
@scope = scope
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def new_scope(scope)
|
27
|
+
return scope if proc?(scope)
|
28
|
+
|
29
|
+
lambda do |root, _arguments, context|
|
30
|
+
scope = find_scope(root, scope)
|
31
|
+
scope.new(context[current_user], root).resolve
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def find_scope(root, scope)
|
36
|
+
if inferred?(scope)
|
37
|
+
# Special case for Sequel datasets that do not respond to
|
38
|
+
# ActiveModel's model_name
|
39
|
+
infer_from = if root.respond_to?(:model)
|
40
|
+
root.model
|
41
|
+
else
|
42
|
+
root
|
43
|
+
end
|
44
|
+
::Pundit::PolicyFinder.new(infer_from).scope!
|
45
|
+
else
|
46
|
+
scope::Scope
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def valid_value?(value)
|
51
|
+
value.is_a?(Class) || inferred?(value) || proc?(value)
|
52
|
+
end
|
53
|
+
|
54
|
+
def proc?(value)
|
55
|
+
value.respond_to?(:call)
|
56
|
+
end
|
57
|
+
|
58
|
+
def inferred?(value)
|
59
|
+
value == :infer_scope
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
attr_reader :current_user
|
64
|
+
|
65
|
+
def initialize(current_user = :current_user)
|
66
|
+
@current_user = current_user
|
67
|
+
end
|
68
|
+
|
69
|
+
# rubocop:disable Metrics/MethodLength
|
70
|
+
def instrument(_type, field)
|
71
|
+
# rubocop:enable Metrics/MethodLength
|
72
|
+
scope_metadata = field.metadata[self.class::SCOPE_KEY]
|
73
|
+
return field unless scope_metadata
|
74
|
+
|
75
|
+
scope = scope_metadata[:proc]
|
76
|
+
|
77
|
+
old_resolver = field.resolve_proc
|
78
|
+
resolver = self.class::ScopeResolver.new(current_user,
|
79
|
+
scope,
|
80
|
+
old_resolver,
|
81
|
+
field)
|
82
|
+
|
83
|
+
field.redefine do
|
84
|
+
resolve resolver
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql-pundit/common'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Pundit
|
7
|
+
# Scope methods to be included in the used Field class
|
8
|
+
module Scope
|
9
|
+
def self.prepended(base)
|
10
|
+
base.include(GraphQL::Pundit::Common)
|
11
|
+
end
|
12
|
+
|
13
|
+
# rubocop:disable Metrics/ParameterLists
|
14
|
+
def initialize(*args, policy: nil,
|
15
|
+
record: nil,
|
16
|
+
before_scope: nil,
|
17
|
+
after_scope: nil,
|
18
|
+
**kwargs, &block)
|
19
|
+
@before_scope = before_scope
|
20
|
+
@after_scope = after_scope
|
21
|
+
@policy = policy
|
22
|
+
@record = record
|
23
|
+
super(*args, **kwargs, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
# rubocop:enable Metrics/ParameterLists
|
27
|
+
|
28
|
+
def before_scope(scope = true)
|
29
|
+
@before_scope = scope
|
30
|
+
end
|
31
|
+
|
32
|
+
def after_scope(scope = true)
|
33
|
+
@after_scope = scope
|
34
|
+
end
|
35
|
+
|
36
|
+
def resolve(obj, args, ctx)
|
37
|
+
before_scope_return = apply_scope(@before_scope, obj, args, ctx)
|
38
|
+
field_return = super(before_scope_return, args, ctx)
|
39
|
+
apply_scope(@after_scope, field_return, args, ctx)
|
40
|
+
end
|
41
|
+
|
42
|
+
alias resolve_field resolve
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def apply_scope(scope, root, arguments, context)
|
47
|
+
return root unless scope
|
48
|
+
|
49
|
+
record = @record || root
|
50
|
+
return scope.call(record, arguments, context) if scope.respond_to?(:call)
|
51
|
+
|
52
|
+
scope = infer_scope(record) if scope.equal?(true)
|
53
|
+
scope::Scope.new(context[self.class.current_user], record).resolve
|
54
|
+
end
|
55
|
+
|
56
|
+
def infer_scope(root)
|
57
|
+
infer_from = model?(root) ? root.model : root
|
58
|
+
::Pundit::PolicyFinder.new(infer_from).policy!
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql-pundit/instrumenter'
|
4
|
+
require 'graphql-pundit/field'
|
5
|
+
require 'graphql-pundit/authorization'
|
6
|
+
require 'graphql-pundit/scope'
|
7
|
+
require 'graphql-pundit/version'
|
8
|
+
|
9
|
+
require 'graphql'
|
10
|
+
|
11
|
+
# Defines authorization related helpers
|
12
|
+
module GraphQL
|
13
|
+
# Defines `authorize` and `authorize!` helpers
|
14
|
+
class AuthorizationHelper
|
15
|
+
attr_reader :raise_unauthorized
|
16
|
+
|
17
|
+
def initialize(raise_unauthorized)
|
18
|
+
@raise_unauthorized = raise_unauthorized
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(defn, *args, policy: nil, record: nil)
|
22
|
+
query = args[0] || defn.name
|
23
|
+
opts = { record: record,
|
24
|
+
query: query,
|
25
|
+
policy: policy,
|
26
|
+
raise: raise_unauthorized }
|
27
|
+
opts = { proc: query, raise: raise_unauthorized } if query.respond_to?(:call)
|
28
|
+
Define::InstanceDefinable::AssignMetadataKey.new(:authorize)
|
29
|
+
.call(defn, opts)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Defines `scope` helper
|
34
|
+
class ScopeHelper
|
35
|
+
def initialize(before_or_after, deprecated: false)
|
36
|
+
@before_or_after = before_or_after
|
37
|
+
@deprecated = deprecated
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(defn, proc = :infer_scope)
|
41
|
+
opts = { proc: proc, deprecated: @deprecated }
|
42
|
+
Define::InstanceDefinable::AssignMetadataKey
|
43
|
+
.new(:"#{@before_or_after}_scope")
|
44
|
+
.call(defn, opts)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Field.accepts_definitions(authorize: AuthorizationHelper.new(false),
|
49
|
+
authorize!: AuthorizationHelper.new(true),
|
50
|
+
after_scope: ScopeHelper.new(:after),
|
51
|
+
before_scope: ScopeHelper.new(:before),
|
52
|
+
scope: ScopeHelper.new(:before, deprecated: true))
|
53
|
+
end
|