graphql-pundit2 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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