graphql-armory 0.1.0.pre → 0.1.0.pre2
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 +4 -4
- data/CHANGELOG.md +19 -0
- data/LICENSE +0 -0
- data/MIGRATING.md +0 -0
- data/README.md +20 -7
- data/lib/graphql/armory/authorization/adapters/cancan.rb +0 -0
- data/lib/graphql/armory/authorization/adapters/null.rb +0 -0
- data/lib/graphql/armory/authorization/adapters/pundit.rb +0 -0
- data/lib/graphql/armory/authorization/adapters/substitute.rb +0 -0
- data/lib/graphql/armory/authorization/audit/command.rb +0 -0
- data/lib/graphql/armory/authorization/audit/report.rb +0 -0
- data/lib/graphql/armory/authorization/audit.rb +53 -11
- data/lib/graphql/armory/authorization/authorize.rb +0 -0
- data/lib/graphql/armory/authorization/configuration.rb +0 -0
- data/lib/graphql/armory/authorization/controls/actor.rb +0 -0
- data/lib/graphql/armory/authorization/controls/context.rb +0 -0
- data/lib/graphql/armory/authorization/controls/rule.rb +0 -0
- data/lib/graphql/armory/authorization/controls/subject.rb +0 -0
- data/lib/graphql/armory/authorization/errors.rb +0 -0
- data/lib/graphql/armory/authorization/integration/argument.rb +0 -0
- data/lib/graphql/armory/authorization/integration/base.rb +3 -1
- data/lib/graphql/armory/authorization/integration/field.rb +1 -1
- data/lib/graphql/armory/authorization/integration/find_by.rb +0 -0
- data/lib/graphql/armory/authorization/integration/input_object.rb +31 -0
- data/lib/graphql/armory/authorization/integration/object.rb +0 -0
- data/lib/graphql/armory/authorization/integration/resolver.rb +1 -1
- data/lib/graphql/armory/authorization/integration/schema.rb +0 -0
- data/lib/graphql/armory/authorization/integration.rb +31 -0
- data/lib/graphql/armory/authorization/memo.rb +0 -0
- data/lib/graphql/armory/authorization/rspec.rb +0 -0
- data/lib/graphql/armory/authorization/rule.rb +0 -0
- data/lib/graphql/armory/authorization/scope.rb +0 -0
- data/lib/graphql/armory/authorization/telemetry.rb +0 -0
- data/lib/graphql/armory/authorization.rb +0 -0
- data/lib/graphql/armory/version.rb +1 -1
- data/lib/graphql/armory.rb +0 -0
- data/lib/graphql-armory.rb +0 -0
- data/lib/tasks/graphql_armory.rake +0 -0
- metadata +6 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17454310f4c6b683fac81e5fc5e4ea5f810a645319eb083841e42910e7f881b3
|
|
4
|
+
data.tar.gz: cea12c9b171113f1cb3d6df4658676508056bab51335f8e937adabba18e28817
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 915b3068e7fecaee1f409b24cbc0b24832053322da848e7a242302c701479ae293d5b735d9d4ab49784b7f950fbfedb909034f07ea90bf877cb815471926e660
|
|
7
|
+
data.tar.gz: 9d4809e01699f91be71abc2b2ac4209417ca5d5b39484f91ba492fc9c3bdc24a2d048b1c83c9c9e2aaf6238e193bdd9d17f4b14bdae5ed6c31ae0de4b1977250
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.0.pre2 (2026-10-29)
|
|
6
|
+
|
|
7
|
+
Closes the known gaps of the authorization module.
|
|
8
|
+
|
|
9
|
+
- `loads:` inside input objects: `include GraphQL::Armory::Authorization::Base` in the base input object loads them through
|
|
10
|
+
the user's scope; the audit reports input objects whose `loads:` skip it.
|
|
11
|
+
|
|
12
|
+
- Audit of interfaces: an interface field is reported once, on the interface that declares it; `authorize: true` on an
|
|
13
|
+
interface field and interface fields whose field class lacks `Base` (such as Relay `Node.id`) on a bypassed implementer
|
|
14
|
+
are reported.
|
|
15
|
+
|
|
16
|
+
- Global IDs in `loads:`: when the schema defines `object_from_id`, the id is decoded with it, a record of another model is
|
|
17
|
+
refused, and the record is looked up again inside the user's scope.
|
|
18
|
+
|
|
19
|
+
## 0.1.0.pre (2026-10-29)
|
|
20
|
+
|
|
21
|
+
First prerelease on RubyGems: Pundit and CanCanCan adapters, deny-by-default coverage, scoped lists and lookups, schema audit,
|
|
22
|
+
visibility, argument rules, mutations and resolvers, interfaces and unions. The entries below describe it.
|
|
23
|
+
|
|
5
24
|
- Interfaces and unions: an interface field with no rule is denied when an implementing type is a bypass, and the audit
|
|
6
25
|
reports it; `include Base` in an interface or union is refused with a pointer to the possible types.
|
|
7
26
|
|
data/LICENSE
CHANGED
|
File without changes
|
data/MIGRATING.md
CHANGED
|
File without changes
|
data/README.md
CHANGED
|
@@ -18,7 +18,7 @@ The first module is **authorization**. It plugs [Pundit](https://github.com/varv
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
20
|
```ruby
|
|
21
|
-
gem "graphql-armory"
|
|
21
|
+
gem "graphql-armory", "~> 0.1.0.pre2" # a prerelease: `gem install graphql-armory --pre`
|
|
22
22
|
gem "pundit" # or "cancancan", whichever you use
|
|
23
23
|
```
|
|
24
24
|
|
|
@@ -97,7 +97,9 @@ field :invoice_via_ref, InvoiceType, authorize: :none, reason: "loaded through t
|
|
|
97
97
|
end
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
`find_by:` and `loads:` look the record up **inside the user's scope**, so another tenant's ID answers exactly like a missing one. Opt out of the scope for a `loads:` only on purpose, with `load_through_scope: false`; the audit lists it.
|
|
100
|
+
`find_by:` and `loads:` (also inside input objects whose base class includes `Base`) look the record up **inside the user's scope**, so another tenant's ID answers exactly like a missing one. Opt out of the scope for a field's `loads:` only on purpose, with `load_through_scope: false`; the audit lists it. On `Execution::Next` the opt-out also reaches `loads:` nested in that field's input objects (the interpreter keeps those scoped), and the audit lists them too (`Query.field(filter.invoice_id)`). An input object that overrides `object_from_id` or defines `load_<argument>` itself is honoured on the interpreter and not seen by the audit.
|
|
101
|
+
|
|
102
|
+
**Global IDs.** When your schema defines `object_from_id` (Relay global IDs), `loads:` decodes the id with it, refuses a record of another model, and looks the record up again inside the scope (one extra query per load). Without `object_from_id`, the id is the primary key. `object_from_id` must return the record itself, not a lazy value: a lazy value is denied. A decoder that raises `ActiveRecord::RecordNotFound` (like `GlobalID.find`) or `ArgumentError` (a malformed id) is answered like a missing record; other errors propagate. `loads:` of an interface or union (for example `Types::Node`) raises `ConfigurationError`: load a concrete type. Composite primary keys are not supported.
|
|
101
103
|
|
|
102
104
|
## The audit
|
|
103
105
|
|
|
@@ -224,7 +226,7 @@ end
|
|
|
224
226
|
- An unknown `on_denied:` raises `ArgumentError`; a mutation with a rule but no model raises `ConfigurationError`.
|
|
225
227
|
- **Class-level answers are weak.** With no loaded record of the model (no `loads:`, an empty `loads: [ID]`, or a `loads:` of another type) the rule is asked about the model's class: CanCan answers true for anyone holding the right, and a Pundit policy that reads `record` attributes raises. Load the record you are authorizing.
|
|
226
228
|
- **`ready?` runs before authorization** (and, on the interpreter, before a failing `loads:`). Keep it free of side effects.
|
|
227
|
-
- **`loads:`
|
|
229
|
+
- **`loads:` inside an input object** goes through the scope only when your base input object includes `Base` (`class Types::BaseInputObject < GraphQL::Schema::InputObject; include GraphQL::Armory::Authorization::Base; end`); otherwise the audit reports it as `:unscoped_load`.
|
|
228
230
|
- **Do not put `authorize:` on a mutation field or on payload fields**; the mutation's own rule is the one that counts. A subclass that redeclares `authorize :none` is open to everyone, on purpose.
|
|
229
231
|
|
|
230
232
|
## Interfaces and unions
|
|
@@ -235,8 +237,9 @@ What an interface adds is its fields, declared once and resolved on many types:
|
|
|
235
237
|
|
|
236
238
|
- A field with no rule is **covered** by the rule of the Base types that implement the interface (a plain implementer runs no check at all), unless one of them is a bypass (`authorize :none`). Then the field is **denied for every implementer**, because it cannot tell which type it runs on, and the audit reports it (`:field_without_rule`, for example `Document.number`). Declare it (`authorize: :none, reason:` or an action) to fix it.
|
|
237
239
|
- `visible_when:` is not supported on an interface field; put it on the concrete types' fields.
|
|
238
|
-
- `authorize: true` is not valid on an interface field: it is always denied, and the audit
|
|
239
|
-
- An interface whose field class does not include `Base`, such as graphql-ruby's built-in Relay `Node`, runs no field check, so its `id` stays readable on a bypassed implementer.
|
|
240
|
+
- `authorize: true` is not valid on an interface field: it is always denied, and the audit reports it. Use an action or `:none`.
|
|
241
|
+
- An interface whose field class does not include `Base`, such as graphql-ruby's built-in Relay `Node`, runs no field check, so its `id` stays readable on a bypassed implementer. The audit reports it (`:unprotected`, `Node.id`); give your own `Node` interface a Base field class if that matters.
|
|
242
|
+
- The audit reports an interface field once, on the interface that declares it, not again on each implementer.
|
|
240
243
|
|
|
241
244
|
## Telemetry
|
|
242
245
|
|
|
@@ -246,7 +249,7 @@ Pass `sinks: [->(signal, data) {}]`. Signals are `:allowed`, `:denied`, `:bypass
|
|
|
246
249
|
|
|
247
250
|
Covered by the black-box suite in `spec/security`, on both engines: user A cannot read user B's objects by root field, nested association or `loads:`; lists never contain foreign objects; uncovered types fail closed and show in the audit; a field hidden with `visible?` is absent from introspection and error suggestions (that is graphql-ruby's behaviour, pinned by a test).
|
|
248
251
|
|
|
249
|
-
|
|
252
|
+
Policies must return `true`/`false`/`nil`; anything else raises `AdapterError`. Do not use `dataloader.request` inside policies; use `dataloader.with(...).load`.
|
|
250
253
|
|
|
251
254
|
## Engines
|
|
252
255
|
|
|
@@ -279,13 +282,23 @@ GRAPHQL_ARMORY_ENGINE=next docker compose run --rm test # full suite, Ex
|
|
|
279
282
|
GRAPHQL_VERSION="~> 2.4.0" docker compose run --rm test # another graphql version
|
|
280
283
|
```
|
|
281
284
|
|
|
285
|
+
### Releasing
|
|
286
|
+
|
|
287
|
+
Releases are published from GitHub Actions with RubyGems Trusted Publishing (`.github/workflows/release.yml`): no API key is stored and no OTP is asked. Bump `GraphQL::Armory::VERSION` and the CHANGELOG on `main`, then push the matching tag:
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
git tag v0.1.0.pre2 && git push origin v0.1.0.pre2
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The workflow checks that the tag matches the version, runs the specs and pushes the gem. It only publishes a tag on a commit of `main`. The `release` environment can require a maintainer's approval first (on a public repository, or a private one on a paid GitHub plan).
|
|
294
|
+
|
|
282
295
|
## Roadmap
|
|
283
296
|
|
|
284
297
|
Status: ✅ done, 🚧 in progress, ⬜ not started. Update this table in the same PR that changes a module.
|
|
285
298
|
|
|
286
299
|
| Order | Module | Status | Current limits |
|
|
287
300
|
|---|---|---|---|
|
|
288
|
-
| 1 | Authorization (Pundit, CanCanCan) |
|
|
301
|
+
| 1 | Authorization (Pundit, CanCanCan) | ✅ | Released in 0.1.0.pre2 (prerelease). Pundit and CanCanCan (the audit checks less with CanCanCan). Global IDs in `loads:` decoded through `object_from_id`; `loads:` inside input objects scoped with `include Base`. `Execution::Next` needs graphql >= 2.6.2. |
|
|
289
302
|
| 2 | Encrypted, versioned IDs and cursors | ⬜ | Research not started. |
|
|
290
303
|
| 3 | Persisted queries / operation whitelisting | ⬜ | Research not started. |
|
|
291
304
|
| 4 | Rate limiting | ⬜ | Research not started. |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -38,6 +38,7 @@ module GraphQL
|
|
|
38
38
|
|
|
39
39
|
audited_types(schema).each { |type| audit_type(type, findings) }
|
|
40
40
|
schema.types.each_value { |type| audit_interface(type, schema, findings) if type.kind.interface? && !type.introspection? }
|
|
41
|
+
schema.types.each_value { |type| audit_input_loads(type, findings) if type.kind.input_object? && !type.introspection? }
|
|
41
42
|
findings.report
|
|
42
43
|
end
|
|
43
44
|
|
|
@@ -72,18 +73,32 @@ module GraphQL
|
|
|
72
73
|
end
|
|
73
74
|
end
|
|
74
75
|
|
|
75
|
-
#
|
|
76
|
+
# Interface fields are reported once, on the interface that declares them, not on every implementer.
|
|
77
|
+
# Undeclared ones are denied when an implementer is a bypass; ones whose field class lacks Base are
|
|
78
|
+
# never checked, so on a bypassed implementer they are open; `authorize: true` names no action.
|
|
76
79
|
def audit_interface(interface, schema, findings)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
bypassed = Integration.bypassed_implementer?(interface, schema)
|
|
81
|
+
interface.own_fields.each_value do |field|
|
|
82
|
+
path = "#{interface.graphql_name}.#{field.original_name}"
|
|
83
|
+
unless field.respond_to?(:armory_declared)
|
|
84
|
+
findings.problem(:unprotected, path, "its field class does not include Base, so it is not checked on a bypassed implementer") if bypassed
|
|
85
|
+
next
|
|
86
|
+
end
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
case field.armory_declared
|
|
89
|
+
when true
|
|
90
|
+
findings.problem(:field_without_rule, path, "uses authorize: true, but an interface has no action of its own, so it is always denied")
|
|
91
|
+
when nil
|
|
92
|
+
findings.problem(:field_without_rule, path, "declares no rule and an implementing type is bypassed, so it is denied") if bypassed
|
|
93
|
+
end
|
|
84
94
|
end
|
|
85
95
|
end
|
|
86
96
|
|
|
97
|
+
# A field an implementer inherits from an interface is reported on the interface.
|
|
98
|
+
def interface_field?(field)
|
|
99
|
+
field.owner.respond_to?(:kind) && field.owner.kind.interface?
|
|
100
|
+
end
|
|
101
|
+
|
|
87
102
|
def model_of(type, path, findings)
|
|
88
103
|
type.armory_model
|
|
89
104
|
rescue ConfigurationError => error
|
|
@@ -101,11 +116,13 @@ module GraphQL
|
|
|
101
116
|
def audit_fields(type, findings, model:, bypassed:, reported:)
|
|
102
117
|
type.all_field_definitions.each do |field|
|
|
103
118
|
path = "#{type.graphql_name}.#{field.original_name}"
|
|
104
|
-
|
|
119
|
+
# An inherited interface field is reported as uncovered on its interface; every other check still runs here.
|
|
120
|
+
inherited = bypassed && interface_field?(field)
|
|
121
|
+
next findings.problem(:field_without_rule, path, "its field class does not include Base, so it is denied") if bypassed && !inherited && !field.respond_to?(:armory_declared)
|
|
105
122
|
next unless field.respond_to?(:armory_declared)
|
|
106
123
|
next audit_resolver(field, path, findings) if field.resolver.respond_to?(:armory_rule)
|
|
107
124
|
|
|
108
|
-
audit_declaration(field, path, findings, model: model, bypassed: bypassed, reported: reported)
|
|
125
|
+
audit_declaration(field, path, findings, model: model, bypassed: bypassed && !inherited, reported: reported)
|
|
109
126
|
audit_visibility(field, path, findings, model: model, reported: reported)
|
|
110
127
|
audit_loads(field, path, findings)
|
|
111
128
|
audit_arguments(field, path, findings, reported: reported)
|
|
@@ -170,12 +187,37 @@ module GraphQL
|
|
|
170
187
|
def audit_loads(field, path, findings)
|
|
171
188
|
return if field.armory_load_through_scope?
|
|
172
189
|
|
|
173
|
-
field.all_argument_definitions.
|
|
174
|
-
name = GraphQL::Schema::Member::BuildType.underscore(argument.graphql_name)
|
|
190
|
+
opted_out_loads(field.all_argument_definitions, Set.new).each do |name|
|
|
175
191
|
findings.problem(:unscoped_load, "#{path}(#{name})", "loads through the app's object_from_id, not through the user's scope")
|
|
176
192
|
end
|
|
177
193
|
end
|
|
178
194
|
|
|
195
|
+
# Execution::Next loads `loads:` nested in input objects with the field's loader, so a field's opt-out
|
|
196
|
+
# reaches them too: list them as `filter.invoice_id`. `seen` stops recursive input types.
|
|
197
|
+
def opted_out_loads(arguments, seen, prefix = nil)
|
|
198
|
+
arguments.flat_map do |argument|
|
|
199
|
+
name = [prefix, GraphQL::Schema::Member::BuildType.underscore(argument.graphql_name)].compact.join(".")
|
|
200
|
+
next [name] if argument.loads
|
|
201
|
+
|
|
202
|
+
input = argument.type.unwrap
|
|
203
|
+
next [] unless input.kind.input_object? && seen.add?(input)
|
|
204
|
+
|
|
205
|
+
opted_out_loads(input.all_argument_definitions, seen, name)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# An input object loads its own `loads:` arguments; without Base they skip the user's scope.
|
|
210
|
+
def audit_input_loads(input, findings)
|
|
211
|
+
return if input.respond_to?(:armory_scoped_loads?)
|
|
212
|
+
|
|
213
|
+
# A mutation's generated input (`from_resolver`) is loaded, scoped, by the mutation itself.
|
|
214
|
+
input.all_argument_definitions.select(&:loads).reject(&:from_resolver?).each do |argument|
|
|
215
|
+
name = GraphQL::Schema::Member::BuildType.underscore(argument.graphql_name)
|
|
216
|
+
findings.problem(:unscoped_load, "#{input.graphql_name}(#{name})",
|
|
217
|
+
"its input object class does not include Base, so it loads through the app's object_from_id, not the user's scope")
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
179
221
|
# Collects what one audit finds.
|
|
180
222
|
class Findings
|
|
181
223
|
def initialize
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -21,11 +21,13 @@ module GraphQL
|
|
|
21
21
|
elsif base <= GraphQL::Schema::Resolver
|
|
22
22
|
base.extend(Integration::ResolverClass)
|
|
23
23
|
base.include(Integration::Resolver)
|
|
24
|
+
elsif base <= GraphQL::Schema::InputObject
|
|
25
|
+
base.extend(Integration::InputObject)
|
|
24
26
|
elsif base <= GraphQL::Schema::Argument
|
|
25
27
|
base.include(Integration::Argument)
|
|
26
28
|
else
|
|
27
29
|
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"
|
|
30
|
+
"GraphQL::Armory::Authorization::Base can only be included in a GraphQL object, field, argument, input object, resolver or mutation class, not #{base}. Interfaces and unions are not authorized themselves: declare the rule on their possible types"
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
end
|
|
@@ -38,7 +38,7 @@ module GraphQL
|
|
|
38
38
|
def object_from_id(type, id, context)
|
|
39
39
|
return super unless @armory_load_through_scope
|
|
40
40
|
|
|
41
|
-
Integration.
|
|
41
|
+
Integration.load(type, id, context)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# A `loads:` id that is not found is answered like a denial, so the client cannot tell the two apart.
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphQL
|
|
4
|
+
module Armory
|
|
5
|
+
module Authorization
|
|
6
|
+
module Integration
|
|
7
|
+
# Extended into an input object class by `include GraphQL::Armory::Authorization::Base`.
|
|
8
|
+
# graphql-ruby loads an input object's `loads:` arguments with the class's own loader, not the
|
|
9
|
+
# field's, so the scoped lookup has to live here too.
|
|
10
|
+
#
|
|
11
|
+
# class Types::BaseInputObject < GraphQL::Schema::InputObject
|
|
12
|
+
# include GraphQL::Armory::Authorization::Base
|
|
13
|
+
# end
|
|
14
|
+
module InputObject
|
|
15
|
+
def armory_scoped_loads?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def object_from_id(type, id, context)
|
|
20
|
+
Integration.load(type, id, context)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A `loads:` id that is not found is answered like a denial, so the client cannot tell the two apart.
|
|
24
|
+
def load_application_object_failed(error)
|
|
25
|
+
raise GraphQL::UnauthorizedError.new(object: nil, type: error.argument.loads, context: error.context)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
File without changes
|
|
@@ -55,7 +55,7 @@ module GraphQL
|
|
|
55
55
|
|
|
56
56
|
# `loads:` arguments are looked up inside the user's scope, so a foreign id answers like a missing one.
|
|
57
57
|
def object_from_id(type, id, context)
|
|
58
|
-
Integration.
|
|
58
|
+
Integration.load(type, id, context)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def load_application_object_failed(error)
|
|
File without changes
|
|
@@ -6,6 +6,7 @@ require "graphql/armory/authorization/integration/find_by"
|
|
|
6
6
|
require "graphql/armory/authorization/integration/field"
|
|
7
7
|
require "graphql/armory/authorization/integration/argument"
|
|
8
8
|
require "graphql/armory/authorization/integration/resolver"
|
|
9
|
+
require "graphql/armory/authorization/integration/input_object"
|
|
9
10
|
require "graphql/armory/authorization/integration/base"
|
|
10
11
|
|
|
11
12
|
module GraphQL
|
|
@@ -62,6 +63,36 @@ module GraphQL
|
|
|
62
63
|
record if record && allowed?(record, rule, context)
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
# Loads a `loads:` argument inside the user's scope. When the schema defines `object_from_id` (global
|
|
67
|
+
# IDs), it decodes the id; the record must be of the type's model, and is then looked up again
|
|
68
|
+
# through the scope, so a foreign id still looks like a missing one. Otherwise the id is the primary key.
|
|
69
|
+
def self.load(type, id, context)
|
|
70
|
+
schema = context.schema
|
|
71
|
+
return find(type, nil, id, context) unless decodes_ids?(schema) && type.respond_to?(:armory_model)
|
|
72
|
+
|
|
73
|
+
model = type.armory_model
|
|
74
|
+
record = decode(schema, id, context)
|
|
75
|
+
return nil unless record.is_a?(model)
|
|
76
|
+
|
|
77
|
+
find(type, nil, record.public_send(model.primary_key), context)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# `GlobalID.find`, graphql-ruby's documented `object_from_id`, raises for a missing record, and a
|
|
81
|
+
# decoder may raise for a malformed id. Both must answer like a record out of scope: nil.
|
|
82
|
+
def self.decode(schema, id, context)
|
|
83
|
+
schema.object_from_id(id, context)
|
|
84
|
+
rescue StandardError => error
|
|
85
|
+
raise unless error.is_a?(ArgumentError) || error.class.name == "ActiveRecord::RecordNotFound"
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
private_class_method :decode
|
|
90
|
+
|
|
91
|
+
# graphql-ruby's default `object_from_id` only raises, so an override is the app decoding its IDs.
|
|
92
|
+
def self.decodes_ids?(schema)
|
|
93
|
+
schema.method(:object_from_id).owner != GraphQL::Schema.singleton_class
|
|
94
|
+
end
|
|
95
|
+
|
|
65
96
|
# An interface field with no rule of its own cannot name the type it is resolved on, so it is only
|
|
66
97
|
# covered when no implementer is a bypass: a field on a bypassed type is denied, and so is this one.
|
|
67
98
|
def self.bypassed_implementer?(interface, schema)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/lib/graphql/armory.rb
CHANGED
|
File without changes
|
data/lib/graphql-armory.rb
CHANGED
|
File without changes
|
|
File without changes
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graphql-armory
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.
|
|
4
|
+
version: 0.1.0.pre2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Israel
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: graphql
|
|
@@ -34,7 +33,6 @@ description: 'Open-source extensions for graphql-ruby built on its public extens
|
|
|
34
33
|
points. The first module is authorization: deny-by-default coverage, scoped lists
|
|
35
34
|
and connections, IDOR-safe lookups, mutations, interfaces and unions, visibility
|
|
36
35
|
driven by the policy, and a schema audit for CI.'
|
|
37
|
-
email:
|
|
38
36
|
executables: []
|
|
39
37
|
extensions: []
|
|
40
38
|
extra_rdoc_files: []
|
|
@@ -65,6 +63,7 @@ files:
|
|
|
65
63
|
- lib/graphql/armory/authorization/integration/base.rb
|
|
66
64
|
- lib/graphql/armory/authorization/integration/field.rb
|
|
67
65
|
- lib/graphql/armory/authorization/integration/find_by.rb
|
|
66
|
+
- lib/graphql/armory/authorization/integration/input_object.rb
|
|
68
67
|
- lib/graphql/armory/authorization/integration/object.rb
|
|
69
68
|
- lib/graphql/armory/authorization/integration/resolver.rb
|
|
70
69
|
- lib/graphql/armory/authorization/integration/schema.rb
|
|
@@ -84,7 +83,6 @@ metadata:
|
|
|
84
83
|
changelog_uri: https://github.com/israelb/graphql-armory/blob/main/CHANGELOG.md
|
|
85
84
|
bug_tracker_uri: https://github.com/israelb/graphql-armory/issues
|
|
86
85
|
rubygems_mfa_required: 'true'
|
|
87
|
-
post_install_message:
|
|
88
86
|
rdoc_options: []
|
|
89
87
|
require_paths:
|
|
90
88
|
- lib
|
|
@@ -95,12 +93,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
95
93
|
version: '3.1'
|
|
96
94
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
95
|
requirements:
|
|
98
|
-
- - "
|
|
96
|
+
- - ">="
|
|
99
97
|
- !ruby/object:Gem::Version
|
|
100
|
-
version:
|
|
98
|
+
version: '0'
|
|
101
99
|
requirements: []
|
|
102
|
-
rubygems_version: 3.
|
|
103
|
-
signing_key:
|
|
100
|
+
rubygems_version: 3.6.9
|
|
104
101
|
specification_version: 4
|
|
105
102
|
summary: 'Authorization for graphql-ruby: Pundit and CanCanCan, deny-by-default, with
|
|
106
103
|
a schema audit.'
|