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.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Pundit
5
+ VERSION = '0.9.1'
6
+ end
7
+ 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