graphql-armory 0.1.0.pre

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +26 -0
  3. data/LICENSE +21 -0
  4. data/MIGRATING.md +85 -0
  5. data/README.md +311 -0
  6. data/lib/graphql/armory/authorization/adapters/cancan.rb +92 -0
  7. data/lib/graphql/armory/authorization/adapters/null.rb +28 -0
  8. data/lib/graphql/armory/authorization/adapters/pundit.rb +83 -0
  9. data/lib/graphql/armory/authorization/adapters/substitute.rb +46 -0
  10. data/lib/graphql/armory/authorization/audit/command.rb +36 -0
  11. data/lib/graphql/armory/authorization/audit/report.rb +51 -0
  12. data/lib/graphql/armory/authorization/audit.rb +213 -0
  13. data/lib/graphql/armory/authorization/authorize.rb +68 -0
  14. data/lib/graphql/armory/authorization/configuration.rb +35 -0
  15. data/lib/graphql/armory/authorization/controls/actor.rb +15 -0
  16. data/lib/graphql/armory/authorization/controls/context.rb +29 -0
  17. data/lib/graphql/armory/authorization/controls/rule.rb +15 -0
  18. data/lib/graphql/armory/authorization/controls/subject.rb +15 -0
  19. data/lib/graphql/armory/authorization/errors.rb +29 -0
  20. data/lib/graphql/armory/authorization/integration/argument.rb +74 -0
  21. data/lib/graphql/armory/authorization/integration/base.rb +34 -0
  22. data/lib/graphql/armory/authorization/integration/field.rb +102 -0
  23. data/lib/graphql/armory/authorization/integration/find_by.rb +22 -0
  24. data/lib/graphql/armory/authorization/integration/object.rb +67 -0
  25. data/lib/graphql/armory/authorization/integration/resolver.rb +84 -0
  26. data/lib/graphql/armory/authorization/integration/schema.rb +60 -0
  27. data/lib/graphql/armory/authorization/integration.rb +104 -0
  28. data/lib/graphql/armory/authorization/memo.rb +35 -0
  29. data/lib/graphql/armory/authorization/rspec.rb +21 -0
  30. data/lib/graphql/armory/authorization/rule.rb +61 -0
  31. data/lib/graphql/armory/authorization/scope.rb +37 -0
  32. data/lib/graphql/armory/authorization/telemetry.rb +42 -0
  33. data/lib/graphql/armory/authorization.rb +41 -0
  34. data/lib/graphql/armory/version.rb +7 -0
  35. data/lib/graphql/armory.rb +13 -0
  36. data/lib/graphql-armory.rb +2 -0
  37. data/lib/tasks/graphql_armory.rake +11 -0
  38. metadata +107 -0
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ module Adapters
7
+ # An adapter with canned answers that keeps the questions it was asked, for tests.
8
+ class Substitute
9
+ attr_reader :asked, :scoped_for
10
+
11
+ def initialize(allow: true, actor: nil, scoped: :itself.to_proc, raises: nil, raises_on_scope: nil)
12
+ @allow = allow
13
+ @actor = actor
14
+ @scoped = scoped
15
+ @raises = raises
16
+ @raises_on_scope = raises_on_scope
17
+ @asked = []
18
+ @scoped_for = []
19
+ end
20
+
21
+ def actor(_context)
22
+ @actor
23
+ end
24
+
25
+ def authorized?(actor, subject, rule)
26
+ raise @raises if @raises
27
+
28
+ @asked << [actor, subject, rule]
29
+ @allow
30
+ end
31
+
32
+ def scope(actor, items, rule)
33
+ raise @raises_on_scope if @raises_on_scope
34
+
35
+ @scoped_for << [actor, items, rule]
36
+ @scoped.call(items)
37
+ end
38
+
39
+ def check(_type, _rule)
40
+ []
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ class Audit
7
+ # What `rake graphql_armory:audit[YourSchema]` runs: prints the report and answers an exit status.
8
+ class Command
9
+ def initialize(out: $stdout)
10
+ @out = out
11
+ end
12
+
13
+ def call(schema_name)
14
+ return fail_with("Name the schema: rake graphql_armory:audit[YourSchema]") if schema_name.to_s.empty?
15
+
16
+ schema = Object.const_get(schema_name)
17
+ report = Audit.(schema)
18
+ @out.print(report)
19
+ report.ok? ? 0 : 1
20
+ rescue NameError => error
21
+ raise unless error.name.to_s == schema_name.to_s.split("::").last
22
+
23
+ fail_with("No schema named #{schema_name}")
24
+ end
25
+
26
+ private
27
+
28
+ def fail_with(message)
29
+ @out.puts(message)
30
+ 1
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ class Audit
7
+ # What an audit found: what is not covered, and every bypass someone declared.
8
+ class Report
9
+ Problem = Struct.new(:kind, :path, :message) do
10
+ def to_s
11
+ "#{path} (#{kind}): #{message}"
12
+ end
13
+ end
14
+
15
+ Bypass = Struct.new(:path, :reason)
16
+
17
+ attr_reader :problems, :bypasses
18
+
19
+ def initialize(problems:, bypasses:)
20
+ @problems = problems.freeze
21
+ @bypasses = bypasses.freeze
22
+ freeze
23
+ end
24
+
25
+ def ok?
26
+ problems.empty?
27
+ end
28
+
29
+ def to_s
30
+ [problems_text, bypasses_text].compact.join("\n\n") << "\n"
31
+ end
32
+
33
+ private
34
+
35
+ def problems_text
36
+ return "No problems." if ok?
37
+
38
+ ["#{problems.size} #{problems.size == 1 ? 'problem' : 'problems'}:", *problems.map { |problem| " #{problem}" }].join("\n")
39
+ end
40
+
41
+ def bypasses_text
42
+ return if bypasses.empty?
43
+
44
+ ["#{bypasses.size} declared #{bypasses.size == 1 ? 'bypass' : 'bypasses'}:",
45
+ *bypasses.map { |bypass| " #{bypass.path}: #{bypass.reason}" }].join("\n")
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "graphql/armory/authorization/audit/report"
5
+ require "graphql/armory/authorization/audit/command"
6
+
7
+ module GraphQL
8
+ module Armory
9
+ module Authorization
10
+ # Walks a schema, without running a query, and reports everything that is not covered:
11
+ # types nobody protected, types without a rule or a policy, fields with no rule on a bypassed
12
+ # type, and `loads:` that skip the scope. Every declared bypass is listed with its reason.
13
+ class Audit
14
+ RELAY_TYPES = [GraphQL::Types::Relay::BaseConnection, GraphQL::Types::Relay::BaseEdge,
15
+ GraphQL::Types::Relay::PageInfo].freeze
16
+
17
+ attr_reader :adapter
18
+
19
+ # `new` is inert: with no adapter nothing can be verified, so nothing is covered.
20
+ def initialize(adapter: Adapters::Null.new)
21
+ @adapter = adapter
22
+ end
23
+
24
+ def self.build(schema)
25
+ configuration = schema.armory_configuration if schema.respond_to?(:armory_configuration)
26
+ configuration ? new(adapter: configuration.adapter) : new
27
+ end
28
+
29
+ def self.call(schema)
30
+ build(schema).(schema)
31
+ end
32
+
33
+ def call(schema)
34
+ findings = Findings.new
35
+ unless schema.respond_to?(:armory_configuration) && schema.armory_configuration
36
+ findings.problem(:plugin, "Schema", "never installed the plugin: add `use GraphQL::Armory::Authorization, adapter: ...`")
37
+ end
38
+
39
+ audited_types(schema).each { |type| audit_type(type, findings) }
40
+ schema.types.each_value { |type| audit_interface(type, schema, findings) if type.kind.interface? && !type.introspection? }
41
+ findings.report
42
+ end
43
+
44
+ private
45
+
46
+ # Walks `schema.types`, which a schema that hides types with `visible?` would filter.
47
+ def audited_types(schema)
48
+ schema.types.values.select do |type|
49
+ type.kind.object? && !type.introspection? && RELAY_TYPES.none? { |relay| type <= relay }
50
+ end
51
+ end
52
+
53
+ def audit_type(type, findings)
54
+ path = type.graphql_name
55
+ unless type.respond_to?(:armory_rule)
56
+ findings.problem(:unprotected, path, "its base class does not include GraphQL::Armory::Authorization::Base, so it is open")
57
+ return
58
+ end
59
+
60
+ rule = type.armory_rule
61
+ if rule.nil?
62
+ findings.problem(:no_rule, path, "has no rule and no default_action, so it is denied")
63
+ elsif rule.none?
64
+ findings.bypass(path, rule.reason)
65
+ audit_fields(type, findings, model: nil, bypassed: !rule.covers_fields?, reported: Set.new)
66
+ else
67
+ model = model_of(type, path, findings)
68
+ reported = Set.new
69
+ check(path, model, rule, findings, reported) if model
70
+ check(path, model, Rule.new(rule.visible_when, policy: rule.policy), findings, reported) if model && rule.visible_when
71
+ audit_fields(type, findings, model: model, bypassed: false, reported: reported)
72
+ end
73
+ end
74
+
75
+ # An interface field with no rule is denied when any implementer is a bypass; the fix is to declare it.
76
+ def audit_interface(interface, schema, findings)
77
+ return unless Integration.bypassed_implementer?(interface, schema)
78
+
79
+ interface.fields.each_value do |field|
80
+ next unless field.respond_to?(:armory_declared) && field.armory_declared.nil?
81
+
82
+ findings.problem(:field_without_rule, "#{interface.graphql_name}.#{field.original_name}",
83
+ "declares no rule and an implementing type is bypassed, so it is denied")
84
+ end
85
+ end
86
+
87
+ def model_of(type, path, findings)
88
+ type.armory_model
89
+ rescue ConfigurationError => error
90
+ findings.problem(:model, path, error.message)
91
+ nil
92
+ end
93
+
94
+ # An adapter problem shows once per type, however many fields declare an action of their own.
95
+ def check(path, model, rule, findings, reported)
96
+ adapter.check(model, rule).each do |message|
97
+ findings.problem(:adapter, path, message) if reported.add?(message)
98
+ end
99
+ end
100
+
101
+ def audit_fields(type, findings, model:, bypassed:, reported:)
102
+ type.all_field_definitions.each do |field|
103
+ path = "#{type.graphql_name}.#{field.original_name}"
104
+ next findings.problem(:field_without_rule, path, "its field class does not include Base, so it is denied") if bypassed && !field.respond_to?(:armory_declared)
105
+ next unless field.respond_to?(:armory_declared)
106
+ next audit_resolver(field, path, findings) if field.resolver.respond_to?(:armory_rule)
107
+
108
+ audit_declaration(field, path, findings, model: model, bypassed: bypassed, reported: reported)
109
+ audit_visibility(field, path, findings, model: model, reported: reported)
110
+ audit_loads(field, path, findings)
111
+ audit_arguments(field, path, findings, reported: reported)
112
+ end
113
+ end
114
+
115
+ # A resolver or mutation carries its own rule: no rule is denied, `:none` is listed, an action is checked.
116
+ def audit_resolver(field, path, findings)
117
+ resolver = field.resolver
118
+ rule = resolver.armory_rule
119
+ return findings.problem(:no_rule, path, "its mutation or resolver declares no `authorize`, so it is denied") if rule.nil?
120
+ return findings.bypass(path, rule.reason) if rule.none?
121
+
122
+ model = begin
123
+ resolver.armory_model
124
+ rescue ConfigurationError => error
125
+ return findings.problem(:model, path, error.message)
126
+ end
127
+ check(path, model, rule, findings, Set.new)
128
+ end
129
+
130
+ # The action a field is hidden with is asked of the policy like any other, so it must exist.
131
+ def audit_visibility(field, path, findings, model:, reported:)
132
+ return unless model && field.armory_visible_when
133
+
134
+ check(path.split(".").first, model, Rule.new(field.armory_visible_when, attribute: field.original_name), findings, reported)
135
+ end
136
+
137
+ def audit_declaration(field, path, findings, model:, bypassed:, reported:)
138
+ case (declared = field.armory_declared)
139
+ when :none
140
+ findings.bypass(path, field.armory_reason)
141
+ when nil, true
142
+ if bypassed
143
+ findings.problem(:field_without_rule, path, "declares no rule and its type is bypassed, so it is denied")
144
+ end
145
+ else
146
+ check(path.split(".").first, model, Rule.new(declared), findings, reported) if model
147
+ end
148
+ end
149
+
150
+ # An argument that declares a rule is listed (a bypass with its reason) or checked like a field's action.
151
+ def audit_arguments(field, path, findings, reported:)
152
+ field.all_argument_definitions.each do |argument|
153
+ next unless argument.respond_to?(:armory_declared)
154
+
155
+ apath = "#{path}(#{argument.keyword})"
156
+ declared = argument.armory_declared
157
+ findings.bypass(apath, argument.armory_reason) if declared == :none
158
+ [declared, argument.armory_visible_when].compact.reject { |action| action == :none }.each do |action|
159
+ audit_argument_action(argument, apath, action, findings, reported)
160
+ end
161
+ end
162
+ end
163
+
164
+ def audit_argument_action(argument, path, action, findings, reported)
165
+ check(path.split(".").first, argument.armory_model, Rule.new(action, attribute: argument.keyword), findings, reported)
166
+ rescue ConfigurationError => error
167
+ findings.problem(:model, path, error.message)
168
+ end
169
+
170
+ def audit_loads(field, path, findings)
171
+ return if field.armory_load_through_scope?
172
+
173
+ field.all_argument_definitions.select(&:loads).each do |argument|
174
+ name = GraphQL::Schema::Member::BuildType.underscore(argument.graphql_name)
175
+ findings.problem(:unscoped_load, "#{path}(#{name})", "loads through the app's object_from_id, not through the user's scope")
176
+ end
177
+ end
178
+
179
+ # Collects what one audit finds.
180
+ class Findings
181
+ def initialize
182
+ @problems = []
183
+ @bypasses = []
184
+ end
185
+
186
+ def problem(kind, path, message)
187
+ @problems << Report::Problem.new(kind, path, message)
188
+ end
189
+
190
+ def bypass(path, reason)
191
+ @bypasses << Report::Bypass.new(path, reason)
192
+ end
193
+
194
+ def report
195
+ Report.new(problems: @problems.sort_by { |problem| [problem.path, problem.kind.to_s] },
196
+ bypasses: @bypasses.sort_by(&:path))
197
+ end
198
+ end
199
+
200
+ # Answers the report it was given, for tests.
201
+ class Substitute
202
+ def initialize(report)
203
+ @report = report
204
+ end
205
+
206
+ def call(_schema)
207
+ @report
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ # Can this actor perform this rule's action on this subject? Anything but a clear `true` is a denial.
7
+ class Authorize
8
+ attr_reader :adapter, :memo, :telemetry
9
+
10
+ # `new` is inert: with no configuration it denies everything.
11
+ def initialize(adapter: Adapters::Null.new, memo: Memo.new, telemetry: Telemetry.new)
12
+ @adapter = adapter
13
+ @memo = memo
14
+ @telemetry = telemetry
15
+ end
16
+
17
+ def self.build(context, adapter:, sinks: [])
18
+ new(adapter: adapter, memo: Memo.build(context), telemetry: Telemetry.build(sinks: sinks))
19
+ end
20
+
21
+ def self.call(subject, rule, context, adapter:, sinks: [])
22
+ build(context, adapter: adapter, sinks: sinks).(subject, rule, context)
23
+ end
24
+
25
+ def call(subject, rule, context)
26
+ return bypass(subject, rule) if rule.none?
27
+
28
+ memo.fetch(subject, rule) { decide(subject, rule, context) }
29
+ end
30
+
31
+ private
32
+
33
+ def bypass(subject, rule)
34
+ telemetry.record(:bypassed, type: subject.class.name, reason: rule.reason)
35
+ true
36
+ end
37
+
38
+ def decide(subject, rule, context)
39
+ answer = adapter.authorized?(adapter.actor(context), subject, rule)
40
+
41
+ case answer
42
+ when true, false, nil
43
+ allowed = answer == true
44
+ telemetry.record(allowed ? :allowed : :denied, type: subject.class.name, action: rule.action)
45
+ allowed
46
+ else
47
+ raise AdapterError, "#{adapter.class} answered #{answer.inspect}; it must answer true, false or nil"
48
+ end
49
+ end
50
+
51
+ # Answers what it was told and keeps the calls, for tests.
52
+ class Substitute
53
+ attr_reader :calls
54
+
55
+ def initialize(answer: true)
56
+ @answer = answer
57
+ @calls = []
58
+ end
59
+
60
+ def call(subject, rule, _context)
61
+ @calls << [subject, rule]
62
+ @answer
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ # What `use GraphQL::Armory::Authorization, ...` stores on the schema.
7
+ class Configuration
8
+ MODES = %i[null error raise].freeze
9
+
10
+ attr_reader :adapter, :on_unauthorized, :sinks
11
+
12
+ def initialize(adapter:, on_unauthorized: :null, sinks: [])
13
+ unless MODES.include?(on_unauthorized)
14
+ raise ArgumentError, "on_unauthorized must be one of #{MODES.inspect}, got #{on_unauthorized.inspect}"
15
+ end
16
+
17
+ @adapter = resolve(adapter)
18
+ @on_unauthorized = on_unauthorized
19
+ @sinks = sinks
20
+ end
21
+
22
+ private
23
+
24
+ def resolve(adapter)
25
+ case adapter
26
+ when :pundit then Adapters::Pundit.new
27
+ when :cancan then Adapters::CanCan.new
28
+ when Symbol, String then raise ArgumentError, "unknown adapter #{adapter.inspect}; use :pundit, :cancan or an adapter object"
29
+ else adapter
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ module Controls
7
+ Actor = Struct.new(:id) do
8
+ def self.example(id: 1)
9
+ new(id)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ module Controls
7
+ # Stands in for GraphQL::Query::Context: values plus a namespace per library.
8
+ class Context
9
+ def self.example(**values)
10
+ new(values)
11
+ end
12
+
13
+ def initialize(values)
14
+ @values = values
15
+ @namespaces = Hash.new { |namespaces, name| namespaces[name] = {} }
16
+ end
17
+
18
+ def [](key)
19
+ @values[key]
20
+ end
21
+
22
+ def namespace(name)
23
+ @namespaces[name]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ module Controls
7
+ module Rule
8
+ def self.example(action: :read, **options)
9
+ Authorization::Rule.new(action, **options)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ module Controls
7
+ Subject = Struct.new(:id) do
8
+ def self.example(id: 1)
9
+ new(id)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ class Error < Armory::Error; end
7
+
8
+ # An adapter answered something other than true, false or nil.
9
+ class AdapterError < Error; end
10
+
11
+ # An adapter cannot narrow a list to what the actor may see.
12
+ class MissingScopeError < Error; end
13
+
14
+ # The schema or a class was not set up the way the integration needs.
15
+ class ConfigurationError < Error; end
16
+
17
+ # Raised on a denial when the schema asks for `on_unauthorized: :raise`.
18
+ # It names the type but never the record.
19
+ class NotAuthorizedError < Error
20
+ attr_reader :type_name
21
+
22
+ def initialize(type_name = nil)
23
+ @type_name = type_name
24
+ super("Not authorized to access #{type_name || 'this object'}")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ module Integration
7
+ # Included in an argument class by `include GraphQL::Armory::Authorization::Base`.
8
+ #
9
+ # argument :include_internal, Boolean, authorize: :manage # checked when the client sends it
10
+ # argument :tag, String, visible_when: :export # hidden from users who cannot export
11
+ # argument :status, String, authorize: :none, reason: "public" # declared, listed in the audit
12
+ #
13
+ # An argument with no rule is covered by its field: the field already decides whether the user may call it.
14
+ # A declared rule is checked on top, only when the client sends the argument (graphql-ruby skips
15
+ # arguments left at their default), and a denial denies the whole field.
16
+ #
17
+ # The question is asked of the parent object when it is an instance of the model, else of the model's
18
+ # class (a root field has no record). The model is the one the field's type wraps, or `model:`.
19
+ module Argument
20
+ def initialize(*args, authorize: nil, reason: nil, model: nil, visible_when: nil, **kwargs, &block)
21
+ unless [nil, :none].include?(authorize) || authorize.is_a?(Symbol)
22
+ raise ArgumentError, "an argument's `authorize:` is an action or :none, not #{authorize.inspect}"
23
+ end
24
+
25
+ if authorize && authorize != :none && kwargs.key?(:default_value)
26
+ raise ArgumentError,
27
+ "a restricted argument cannot have a default_value: the interpreter skips the check for a default " \
28
+ "and Execution::Next applies it, so the engines would disagree"
29
+ end
30
+
31
+ @armory_declared = authorize
32
+ @armory_reason = reason
33
+ @armory_model = model
34
+ @armory_visible_when = visible_when&.to_sym
35
+ Rule.none(reason: reason) if authorize == :none
36
+ super(*args, **kwargs, &block)
37
+ end
38
+
39
+ attr_reader :armory_declared, :armory_reason, :armory_visible_when
40
+
41
+ def armory_model
42
+ @armory_model || armory_field_model
43
+ end
44
+
45
+ def authorized?(object, value, context)
46
+ return super if @armory_declared.nil? || @armory_declared == :none
47
+
48
+ model = armory_model
49
+ subject = object.is_a?(model) ? object : model
50
+ Integration.allowed?(subject, Rule.new(@armory_declared, attribute: keyword), context) && super
51
+ end
52
+
53
+ def visible?(context)
54
+ return super unless @armory_visible_when
55
+
56
+ super && Integration.visible?(armory_model, Rule.new(@armory_visible_when, attribute: keyword), context)
57
+ end
58
+
59
+ private
60
+
61
+ def armory_field_model
62
+ type = owner.respond_to?(:owner) ? owner.owner : owner
63
+ model = type.armory_model if type.respond_to?(:armory_model)
64
+ return model if model
65
+
66
+ raise ConfigurationError, "#{keyword}: cannot tell which model to ask about, pass `model: YourModel`"
67
+ rescue ConfigurationError
68
+ raise ConfigurationError, "#{keyword}: cannot tell which model to ask about, pass `model: YourModel`"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Armory
5
+ module Authorization
6
+ # One module for every base class. It detects what it was included in.
7
+ #
8
+ # class Types::BaseObject < GraphQL::Schema::Object
9
+ # include GraphQL::Armory::Authorization::Base
10
+ # default_action :read
11
+ # end
12
+ # class Types::BaseField < GraphQL::Schema::Field
13
+ # include GraphQL::Armory::Authorization::Base
14
+ # end
15
+ module Base
16
+ def self.included(base)
17
+ if base <= GraphQL::Schema::Object
18
+ base.extend(Integration::Object)
19
+ elsif base <= GraphQL::Schema::Field
20
+ base.include(Integration::Field)
21
+ elsif base <= GraphQL::Schema::Resolver
22
+ base.extend(Integration::ResolverClass)
23
+ base.include(Integration::Resolver)
24
+ elsif base <= GraphQL::Schema::Argument
25
+ base.include(Integration::Argument)
26
+ else
27
+ raise ArgumentError,
28
+ "GraphQL::Armory::Authorization::Base can only be included in a GraphQL object, field, argument, resolver or mutation class, not #{base}. Interfaces and unions are not authorized themselves: declare the rule on their possible types"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end