activeadmin-graphql 0.3.2 → 0.3.3

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 +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/app/controllers/active_admin/graphql_controller.rb +19 -4
  4. data/docs/changelogs/20261010091415_graphql-resource-enabled-default.md +34 -0
  5. data/docs/changelogs/20261012050051_graphql-surface-toggles.md +40 -0
  6. data/docs/changelogs/20261012064500_graphql-capabilities.md +35 -0
  7. data/docs/changelogs/20261012113500_release-0-3-3.md +45 -0
  8. data/docs/graphql-api.md +124 -8
  9. data/lib/active_admin/graphql/capability.rb +35 -0
  10. data/lib/active_admin/graphql/capability_definition_dsl.rb +64 -0
  11. data/lib/active_admin/graphql/integration.rb +19 -1
  12. data/lib/active_admin/graphql/policy_adapter.rb +94 -0
  13. data/lib/active_admin/graphql/policy_set_cache.rb +10 -6
  14. data/lib/active_admin/graphql/resource_capability_dsl.rb +31 -0
  15. data/lib/active_admin/graphql/resource_capability_maps.rb +59 -0
  16. data/lib/active_admin/graphql/resource_config.rb +19 -2
  17. data/lib/active_admin/graphql/resource_definition_dsl.rb +5 -4
  18. data/lib/active_admin/graphql/resource_methods.rb +7 -1
  19. data/lib/active_admin/graphql/resource_surface_dsl.rb +48 -0
  20. data/lib/active_admin/graphql/schema_builder/build.rb +52 -3
  21. data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +7 -0
  22. data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +8 -0
  23. data/lib/active_admin/graphql/schema_builder/mutation_create.rb +1 -0
  24. data/lib/active_admin/graphql/schema_builder/mutation_member.rb +8 -0
  25. data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +15 -10
  26. data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +2 -0
  27. data/lib/active_admin/graphql/schema_builder/query_type.rb +2 -1
  28. data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +3 -0
  29. data/lib/active_admin/graphql/schema_builder/query_type_member.rb +1 -0
  30. data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +9 -63
  31. data/lib/active_admin/graphql/schema_builder/resources.rb +34 -0
  32. data/lib/active_admin/graphql/schema_builder/types_object.rb +18 -8
  33. data/lib/active_admin/graphql/schema_builder/visibility.rb +22 -7
  34. data/lib/active_admin/graphql/schema_builder/wire.rb +3 -1
  35. data/lib/active_admin/graphql/schema_field.rb +18 -2
  36. data/lib/active_admin/graphql/version.rb +1 -1
  37. data/lib/active_admin/graphql.rb +2 -0
  38. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95e56a00a0c78ea55f44c186012af72c1ba6ff80ada2adbf905ce9b2d190c47d
4
- data.tar.gz: 1e1dc89e1ffd41d05c6f2962a83857188a239ba677e6f492dbc304a09bb123b9
3
+ metadata.gz: 36bec145bd90193ae32e34aa54b7a6ed01dff13a8b104269b771f797262b2480
4
+ data.tar.gz: 82724e08af33a67a1a9146813965e5d836006620ff503099842addcdeaab16cb
5
5
  SHA512:
6
- metadata.gz: 02533503de4c86355e28bc7212115b043a9d7da293db40d1134251f7892539fca8257d6c03140c110d12da8e4de1b4112668c7e6fc2d721273c990d9fff930e3
7
- data.tar.gz: 6e13f9a69b6cc7df900cf9883ab60600be21bfea50196209c7324306030d74784cb3e66082ffc3ec225192c3c0c385fffb3f24840061e9f12415b60479de9d3c
6
+ metadata.gz: 0b9ddc6e9d85447807e5cbbf9d1e5c9b5c87b756eaa8325cb57fe8ce291e221b40e1afd200ddb683a73cba036b83b6eac6f8f5ab3e0ff9788c1864e7f550cc16
7
+ data.tar.gz: 806443d03640f31e48d893998cd9f693bafd5efce162798ff406ed2eee2ad43d7c0c588467b8fbd37fdab4a3a6049a9efcbadffbe337469cac7bc5ff958bf36f
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.3 (2026-10-12)
6
+
7
+ - Add `graphql { capability ... }` and namespace `graphql_capabilities` so one named grant can gate many fields, queries, and mutations (RFC 0008).
8
+ - Apply `fields:` capabilities to wired `belongs_to` associations; reject unknown capability field names and named visibility `profiles:` combined with `graphql_capabilities`.
9
+ - Add namespace setting `graphql_resource_enabled_default` (`true` by default) and `graphql { enable! }` so apps can opt resources into the schema instead of opting out with `disable!`.
10
+ - Add namespace toggles for `registered_resource` and `belongs_to` wiring (`graphql_registered_resource_enabled`, `graphql_belongs_to_associations_enabled`; both `true` by default).
11
+ - Add `graphql_policies_enabled` / `graphql_policies_enabled_default` for policy field publishing and per-resource seeds, plus `graphql_policy_adapter` (default `ActiveAdmin::GraphQL::PolicyAdapter`) for payload construction.
12
+ - Add per-resource surface defaults and DSL (`graphql_queries_enabled_default`, `graphql_mutations_enabled_default`, `graphql_run_actions_enabled_default`, plus `enable_*!` / `disable_*!`) so apps can trim list/show, CRUD, run-action, and policy fields without disabling the whole resource.
13
+ - Treat `queries_enabled` as the read surface: omit `registered_resource` union membership when queries are off; fail schema build when `Query` would have no fields.
14
+
5
15
  ## 0.3.2 (2026-10-10)
6
16
 
7
17
  - Add `graphql { identity ... }` so keyless views can declare GraphQL id columns, optional `separator`, and `encode` / `decode` (RFC 0007).
@@ -51,16 +51,31 @@ module ActiveAdmin
51
51
 
52
52
  def graphql_context
53
53
  ns = active_admin_namespace
54
- {
54
+ context = {
55
55
  auth: ActiveAdmin::GraphQL::AuthContext.new(
56
56
  user: current_active_admin_user,
57
57
  namespace: ns
58
58
  ),
59
59
  namespace: ns,
60
60
  request: request,
61
- current_user: current_active_admin_user,
62
- visibility_profile: graphql_visibility_profile_for(ns)
63
- }.compact
61
+ current_user: current_active_admin_user
62
+ }
63
+ # Named visibility profiles cache a fixed context; per-request capabilities
64
+ # need the live request context (dynamic visibility). Skip the profile key.
65
+ unless ns.graphql_capabilities.respond_to?(:call)
66
+ profile = graphql_visibility_profile_for(ns)
67
+ context[:visibility_profile] = profile if profile
68
+ end
69
+ capabilities = graphql_capabilities_for(ns, context)
70
+ context[:capabilities] = capabilities unless capabilities.nil?
71
+ context
72
+ end
73
+
74
+ def graphql_capabilities_for(ns, context)
75
+ hook = ns.graphql_capabilities
76
+ return nil unless hook.respond_to?(:call)
77
+
78
+ ActiveAdmin::GraphQL::Capability.normalize_set(hook.call(context))
64
79
  end
65
80
 
66
81
  def graphql_visibility_profile_for(ns)
@@ -0,0 +1,34 @@
1
+ # GraphQL resource enabled default
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov
6
+
7
+ ## Decisions
8
+
9
+ Add namespace setting `graphql_resource_enabled_default`, default `true`, so
10
+ 0.3.x behavior stays: turning on the namespace endpoint still includes every
11
+ ActiveRecord resource unless `graphql { disable! }` is set.
12
+
13
+ Apps that want support-style opt-in set the setting to `false`. Then a resource
14
+ enters the schema only when registration runs `graphql { }` or
15
+ `graphql { enable! }`. `disable!` still omits.
16
+
17
+ `ResourceConfig` takes the initial `enabled` flag from the namespace setting.
18
+ Entering a `graphql` block sets `enabled = true` before the block body runs so
19
+ an empty block is a conscious opt-in.
20
+
21
+ ## Effects
22
+
23
+ New request specs cover default true, default false without a block, empty
24
+ block, `enable!`, and `disable!`.
25
+
26
+ ## Next
27
+
28
+ Folded into 0.3.3 release metadata.
29
+
30
+ ## Source
31
+
32
+ CHANGELOG 0.3.3. docs/graphql-api.md namespace settings and per-resource
33
+ DSL. spec/requests/graphql_resource_enabled_default_spec.rb.
34
+ docs/changelogs/20261012113500_release-0-3-3.md.
@@ -0,0 +1,40 @@
1
+ # GraphQL surface toggles
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov
6
+
7
+ ## Decisions
8
+
9
+ Add namespace and per-resource controls for schema surfaces that were always on
10
+ once a resource was enabled. Defaults stay `true` so 0.3.x behavior is
11
+ unchanged.
12
+
13
+ Namespace-only switches: `graphql_registered_resource_enabled`,
14
+ `graphql_belongs_to_associations_enabled`, `graphql_policies_enabled`.
15
+ Policy payloads use `graphql_policy_adapter` (default `PolicyAdapter`).
16
+ Per-resource policy seed is `graphql_policies_enabled_default`.
17
+
18
+ Per-resource seeds from namespace defaults: queries, mutations (CRUD),
19
+ run-actions, and policies. Resource DSL exposes matching `enable_*!` /
20
+ `disable_*!` methods. `queries_enabled` also gates `registered_resource`
21
+ membership. Schema build raises if `Query` would have no fields.
22
+
23
+ ## Effects
24
+
25
+ Schema builder skips policy query fields, registered_resource union, belongs_to
26
+ wiring, list/show fields, CRUD mutations, and run-action mutations when the
27
+ matching flag is off. Request specs cover the new settings and DSL, including
28
+ empty-Query rejection and registered_resource gating by queries.
29
+
30
+ ## Next
31
+
32
+ Folded into 0.3.3 release metadata.
33
+
34
+ ## Source
35
+
36
+ CHANGELOG 0.3.3. docs/graphql-api.md.
37
+ spec/requests/graphql_surface_toggles_spec.rb.
38
+ spec/requests/graphql_policy_surface_spec.rb.
39
+ spec/requests/graphql_query_surface_contract_spec.rb.
40
+ docs/changelogs/20261012113500_release-0-3-3.md.
@@ -0,0 +1,35 @@
1
+ # GraphQL capabilities
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov
6
+
7
+ ## Decisions
8
+
9
+ Add RFC 0008 capability grants. Resource DSL `capability` associates many
10
+ object fields, queries, and mutations with one name. Namespace
11
+ `graphql_capabilities` fills `context[:capabilities]` per request. SchemaField
12
+ checks capability in `visible?` and `authorized?`. Capabilities do not replace
13
+ ActiveAdmin authorization. Subscriptions stay out of scope.
14
+
15
+ ## Effects
16
+
17
+ Gated members fail closed without the grant. With `graphql_visibility`,
18
+ introspection omits them. Aggregate run-action resolvers also check
19
+ per-action capability keys (`member_*`, `batch_*`, `collection_*`).
20
+ `belongs_to` association fields honor `fields:` capabilities. Schema build
21
+ rejects unknown capability field names and rejects named visibility profiles
22
+ combined with `graphql_capabilities` (dynamic visibility required).
23
+
24
+ ## Next
25
+
26
+ Folded into 0.3.3 release metadata. Follow-ons remain: policy payloads listing
27
+ field capabilities, mutation input attribute maps, and transport gates if
28
+ subscriptions ship later.
29
+
30
+ ## Source
31
+
32
+ RFC 0008. CHANGELOG 0.3.3. docs/graphql-api.md. spec/unit/capability_spec.rb,
33
+ spec/unit/capability_dsl_spec.rb, spec/requests/graphql_capabilities_spec.rb,
34
+ spec/requests/graphql_capabilities_edge_spec.rb.
35
+ docs/changelogs/20261012113500_release-0-3-3.md.
@@ -0,0 +1,45 @@
1
+ # Release 0.3.3 metadata
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov
6
+
7
+ ## Decisions
8
+
9
+ Cut 0.3.3 after published 0.3.2. User-visible work since the 0.3.2 tag:
10
+ `graphql_resource_enabled_default` and `enable!` opt-in, surface toggles for
11
+ queries, mutations, run-actions, policies, `registered_resource`, and
12
+ `belongs_to`, plus GraphQL capabilities (RFC 0008). Defaults stay `true` so
13
+ 0.3.x schema shape is unchanged unless an app opts in to narrower surfaces.
14
+
15
+ CHANGELOG Unreleased bullets moved under 0.3.3 dated 2026-10-12.
16
+
17
+ Version constant is 0.3.3. Path gem pins in Gemfile.lock and appraisal
18
+ lockfiles match.
19
+
20
+ Internal `stringify_attrs` safe-navigation cleanup stays out of CHANGELOG.md.
21
+
22
+ make release (usr/bin/release.rb) still owns gem build, gem push, git tag, and
23
+ GitHub release. This pass only prepares metadata.
24
+
25
+ ## Effects
26
+
27
+ lib/active_admin/graphql/version.rb VERSION is 0.3.3.
28
+
29
+ CHANGELOG surface-toggle and capability bullets live under 0.3.3.
30
+
31
+ PATH specs for activeadmin-graphql in Gemfile.lock,
32
+ gemfiles/rails72.gemfile.lock, gemfiles/rails8ruby4.gemfile.lock, and
33
+ gemfiles/rails8ruby34.gemfile.lock are 0.3.3.
34
+
35
+ ## Next
36
+
37
+ Run make release from a clean tree when ready to push the gem and tag 0.3.3.
38
+
39
+ ## Source
40
+
41
+ docs/changelogs/20261010091415_graphql-resource-enabled-default.md
42
+ docs/changelogs/20261012050051_graphql-surface-toggles.md
43
+ docs/changelogs/20261012064500_graphql-capabilities.md
44
+ rfcs/0008-graphql-capabilities.md
45
+ usr/bin/release.rb
data/docs/graphql-api.md CHANGED
@@ -33,6 +33,12 @@ ActiveAdmin.setup do |config|
33
33
  admin.graphql = true
34
34
  # Optional — default is "graphql", i.e. POST /admin/graphql
35
35
  # admin.graphql_path = "graphql"
36
+ # Optional — set false so only resources with a graphql block enter the schema
37
+ # admin.graphql_resource_enabled_default = false
38
+ # Optional — trim always-on surfaces for a narrow API
39
+ # admin.graphql_policies_enabled = false
40
+ # admin.graphql_registered_resource_enabled = false
41
+ # admin.graphql_belongs_to_associations_enabled = false
36
42
  end
37
43
  end
38
44
  ```
@@ -45,6 +51,33 @@ Run `bin/rails active_admin:install` or reload routes if needed so
45
51
  Each namespace inherits these options (defaults in parentheses):
46
52
 
47
53
  * `graphql` — set to `true` to mount the endpoint (`false` by default).
54
+ * `graphql_resource_enabled_default` — whether each registered ActiveRecord
55
+ resource is included in the schema without a `graphql` block (`true` by
56
+ default, matching 0.3.x). Set to `false` for opt-in: only resources that call
57
+ `graphql { }` (or `graphql { enable! }`) appear in the schema. `disable!`
58
+ still omits a resource.
59
+ * `graphql_queries_enabled_default` — seed for per-resource read fields
60
+ (`true` by default): named list/show fields and membership in
61
+ `registered_resource` / `ActiveAdminRegisteredResource`. Set `false` and use
62
+ `enable_queries!` for opt-in reads. Object types may still exist for mutations.
63
+ * `graphql_mutations_enabled_default` — seed for per-resource CRUD mutations
64
+ (`true` by default). Set `false` and use `enable_mutations!` for opt-in writes.
65
+ * `graphql_run_actions_enabled_default` — seed for batch/member/collection
66
+ action mutations (`true` by default).
67
+ * `graphql_policies_enabled` — publish `activeadmin_policies` query fields and
68
+ allow per-object policy fields (`true` by default).
69
+ * `graphql_policies_enabled_default` — seed for per-resource `policies_enabled`
70
+ (`true` by default). Use `enable_policies!` / `disable_policies!` per resource.
71
+ * `graphql_policy_adapter` — class that builds `activeadmin_policies` payloads
72
+ (`ActiveAdmin::GraphQL::PolicyAdapter` by default). Set a subclass (or constant
73
+ `String`) to replace payload logic; the default adapter still honors
74
+ `graphql_policy_actions` / `graphql_policy_action_mapper` /
75
+ `graphql_policy_extra` / `graphql_policy_transform`.
76
+ * `graphql_registered_resource_enabled` — include `registered_resource` and the
77
+ `ActiveAdminRegisteredResource` union when at least one queryable resource
78
+ exists (`true` by default).
79
+ * `graphql_belongs_to_associations_enabled` — wire non-polymorphic `belongs_to`
80
+ fields between registered resource types (`true` by default).
48
81
  * `graphql_path` — path segment under the namespace mount (`"graphql"`).
49
82
  * `graphql_multiplex_max` — maximum operations per multiplexed JSON array
50
83
  request (`20`).
@@ -70,6 +103,11 @@ Each namespace inherits these options (defaults in parentheses):
70
103
  default `context[:visibility_profile]` for each HTTP request (symbols accepted).
71
104
  * `graphql_schema_visible` — optional `Proc` `(context, meta) -> Boolean` when
72
105
  `graphql_visibility` is enabled; see Schema visibility below.
106
+ * `graphql_capabilities` — optional `Proc` `(context) -> Enumerable` of capability
107
+ names for the request (RFC 0008). Normalized into `context[:capabilities]`.
108
+ Cannot be combined with named `graphql_visibility` `profiles:`; schema build
109
+ forces `dynamic: true` and omits `visibility_profile` from the request context
110
+ so introspection uses the live capability set.
73
111
  * `graphql_configure_schema` — `Proc` receiving the schema class after build
74
112
  (extensions, extra types, and so on).
75
113
 
@@ -199,21 +237,90 @@ UI, including any `authorize!` or `authorize_resource!` calls.
199
237
 
200
238
  See the ActiveAdmin guide [Authorization adapter](https://activeadmin.info/13-authorization-adapter) for adapter setup; the same rules apply to GraphQL.
201
239
 
240
+ Policy preflight fields (`activeadmin_policies`, `activeadmin_policies_for`, and
241
+ per-object `activeadmin_policies`) are published when `graphql_policies_enabled`
242
+ is true. Payloads come from `graphql_policy_adapter`. The default
243
+ `ActiveAdmin::GraphQL::PolicyAdapter` asks the authorization adapter which
244
+ actions are allowed and applies the optional `graphql_policy_*` hooks. Assign a
245
+ subclass that implements `policy_set` (and optionally `include_resource?`) to
246
+ replace the payload builder.
247
+
248
+ ### Capabilities (RFC 0008)
249
+
250
+ Associate many object fields, queries, and mutations with one named capability
251
+ inside `graphql do`. The same capability name can cover a mixed set of members:
252
+
253
+ ```ruby
254
+ ActiveAdmin.register Post do
255
+ graphql do
256
+ capability :pii do
257
+ fields :body
258
+ end
259
+
260
+ capability :editor do
261
+ fields :title
262
+ queries :index, :show
263
+ mutations :create, :update, :destroy
264
+ end
265
+
266
+ capability :publish, member_actions: %i[publish]
267
+ end
268
+ end
269
+ ```
270
+
271
+ Kwargs (`fields:`, `queries:`, `mutations:`, `member_actions:`,
272
+ `batch_actions:`, `collection_actions:`) work without a block. Query aliases:
273
+ `index` / `list` → collection, `show` → member. Mutation alias: `delete` →
274
+ `destroy`. Run-action sugar builds keys like `member_publish`.
275
+
276
+ Grant capabilities per request with `graphql_capabilities`. Map OAuth scopes or
277
+ roles there; the gem stores a symbol set on `context[:capabilities]`:
278
+
279
+ ```ruby
280
+ admin.graphql_capabilities = lambda { |ctx|
281
+ Array(ctx[:current_user]&.oauth_scopes)
282
+ }
283
+ ```
284
+
285
+ A gated member fails closed when the capability is missing from that set:
286
+ resolve returns null for the field (graphql-ruby unauthorized field). Enable
287
+ `graphql_visibility` so introspection and validation also omit ungated members.
288
+ `fields:` applies to generated attributes and wired `belongs_to` associations.
289
+ Unknown field names raise at schema build. Capabilities do not replace the
290
+ authorization adapter.
291
+
292
+ Custom `SchemaField` definitions may pass `capability: :name` the same way.
293
+ Do not use named visibility `profiles:` together with `graphql_capabilities`
294
+ (cached profile context ignores per-request grants).
295
+
202
296
  ## Schema overview
203
297
 
204
- The `Query` type includes one Relay-style connection field per resource route
205
- (for example `posts`), plus a singular field for a single record (for example
206
- `post`). Optional `graphql_field` entries on `register_page` appear here too,
207
- plus `registered_resource` (returning the `ActiveAdminRegisteredResource` union)
208
- when at least one resource is exposed.
298
+ With the default surface flags (`true`), the `Query` type includes one
299
+ Relay-style connection field per resource route (for example `posts`), plus a
300
+ singular field for a single record (for example `post`). Optional
301
+ `graphql_field` entries on `register_page` appear here too, plus
302
+ `registered_resource` (returning the `ActiveAdminRegisteredResource` union)
303
+ when at least one queryable resource exists.
304
+
305
+ List and show fields require per-resource `queries_enabled` (seeded from
306
+ `graphql_queries_enabled_default`). Turning queries off still leaves the object
307
+ type available for mutations. `registered_resource` and the union also require
308
+ `graphql_registered_resource_enabled`. Policy preflight fields require
309
+ `graphql_policies_enabled`. Schema build fails when `Query` would have no
310
+ fields: no list/show, no policies, no `registered_resource`, and no page
311
+ `graphql_field`.
209
312
 
210
313
  Every resource object type implements the `ActiveAdminResource` interface
211
314
  (`id: ID!`) so clients can share fragments or rely on a common shape.
315
+ Non-polymorphic `belongs_to` fields between registered types are wired when
316
+ `graphql_belongs_to_associations_enabled` is true.
212
317
 
213
318
  The `Mutation` type is present when at least one operation exists: per-resource
214
- `create_*`, `update_*`, and `delete_*` when those controller actions exist, plus
215
- `*_batch_action`, `*_member_action`, and `*_collection_action` fields when the
216
- resource defines the corresponding batch, member, or collection actions.
319
+ `create_*`, `update_*`, and `delete_*` when those controller actions exist and
320
+ `mutations_enabled` is true, plus `*_batch_action`, `*_member_action`, and
321
+ `*_collection_action` fields when the resource defines the corresponding batch,
322
+ member, or collection actions and `run_actions_enabled` is true. Both flags are
323
+ seeded from the matching namespace defaults.
217
324
 
218
325
  Object type names default to the model name (for example `Post`). Typed input
219
326
  objects use assignable columns (see the per-resource section below):
@@ -405,7 +512,16 @@ ActiveAdmin.register Post do
405
512
  end
406
513
  ```
407
514
 
515
+ * `enable!` — include this resource when `graphql_resource_enabled_default` is
516
+ `false`. An empty `graphql` block has the same effect.
408
517
  * `disable!` — resource is not included in Query or Mutation.
518
+ * `enable_queries!` / `disable_queries!` — list/show fields and
519
+ `registered_resource` membership for this resource.
520
+ * `enable_mutations!` / `disable_mutations!` — create/update/delete mutations.
521
+ * `enable_run_actions!` / `disable_run_actions!` — batch/member/collection
522
+ action mutations.
523
+ * `enable_policies!` / `disable_policies!` — object `activeadmin_policies` and
524
+ inclusion in the `activeadmin_policies` resources matrix.
409
525
  * `identity` — declare GraphQL id columns (and optional `separator`, `encode`,
410
526
  `decode`) when the model has no ActiveRecord primary key and no `id` column
411
527
  (RFC 0007).
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Named grants carried in GraphQL +context[:capabilities]+ (RFC 0008).
6
+ module Capability
7
+ module_function
8
+
9
+ def normalize_set(raw)
10
+ return nil if raw.nil?
11
+
12
+ Set.new(
13
+ Array(raw).filter_map do |item|
14
+ next if item.nil?
15
+
16
+ name = item.to_s.strip
17
+ next if name.empty?
18
+
19
+ name.to_sym
20
+ end
21
+ )
22
+ end
23
+
24
+ def granted?(context, capability_name)
25
+ return true if capability_name.nil?
26
+
27
+ caps = context[:capabilities]
28
+ return false if caps.nil?
29
+
30
+ caps = normalize_set(caps) unless caps.is_a?(Set)
31
+ caps.include?(capability_name.to_sym)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Block body for +capability :name do ... end+ (RFC 0008).
6
+ class CapabilityDefinitionDSL
7
+ QUERY_ALIASES = {
8
+ index: :collection,
9
+ list: :collection,
10
+ show: :member
11
+ }.freeze
12
+
13
+ MUTATION_ALIASES = {
14
+ delete: :destroy
15
+ }.freeze
16
+
17
+ def initialize(config, capability_name)
18
+ @config = config
19
+ @capability_name = capability_name.to_sym
20
+ end
21
+
22
+ def fields(*names)
23
+ @config.assign_field_capabilities!(@capability_name, names)
24
+ end
25
+
26
+ def queries(*names)
27
+ keys = names.flatten.map { |name| normalize_query_key(name) }
28
+ @config.assign_query_capabilities!(@capability_name, keys)
29
+ end
30
+
31
+ def mutations(*names)
32
+ keys = names.flatten.map { |name| normalize_mutation_key(name) }
33
+ @config.assign_mutation_capabilities!(@capability_name, keys)
34
+ end
35
+
36
+ def member_actions(*names)
37
+ keys = names.flatten.map { |name| :"member_#{name.to_s.tr("-", "_")}" }
38
+ @config.assign_mutation_capabilities!(@capability_name, keys)
39
+ end
40
+
41
+ def batch_actions(*names)
42
+ keys = names.flatten.map { |name| :"batch_#{name.to_s.tr("-", "_")}" }
43
+ @config.assign_mutation_capabilities!(@capability_name, keys)
44
+ end
45
+
46
+ def collection_actions(*names)
47
+ keys = names.flatten.map { |name| :"collection_#{name.to_s.tr("-", "_")}" }
48
+ @config.assign_mutation_capabilities!(@capability_name, keys)
49
+ end
50
+
51
+ private
52
+
53
+ def normalize_query_key(name)
54
+ key = name.to_sym
55
+ QUERY_ALIASES.fetch(key, key)
56
+ end
57
+
58
+ def normalize_mutation_key(name)
59
+ key = name.to_sym
60
+ MUTATION_ALIASES.fetch(key, key)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "policy_adapter"
3
4
  require_relative "resource_definition_dsl"
4
5
  require_relative "resource_methods"
5
6
 
@@ -26,12 +27,26 @@ module ActiveAdmin
26
27
  def register_namespace_settings!
27
28
  ns = ActiveAdmin::NamespaceSettings
28
29
  ns.register :graphql, false
30
+ # true preserves 0.3.x: every ActiveRecord resource is in the schema once the namespace endpoint is on.
31
+ # Set false for opt-in per resource (empty graphql block or enable!).
32
+ ns.register :graphql_resource_enabled_default, true
33
+ ns.register :graphql_queries_enabled_default, true
34
+ ns.register :graphql_mutations_enabled_default, true
35
+ ns.register :graphql_run_actions_enabled_default, true
36
+ ns.register :graphql_policies_enabled, true
37
+ ns.register :graphql_policies_enabled_default, true
38
+ # Payload builder for policy fields when graphql_policies_enabled is true.
39
+ ns.register :graphql_policy_adapter, ActiveAdmin::GraphQL::PolicyAdapter
40
+ ns.register :graphql_registered_resource_enabled, true
41
+ ns.register :graphql_belongs_to_associations_enabled, true
29
42
  ns.register :graphql_path, "graphql"
30
43
  ns.register :graphql_multiplex_max, 20
31
44
  ns.register :graphql_dataloader, nil
32
45
  ns.register :graphql_visibility, nil
33
46
  ns.register :graphql_visibility_profile, nil
34
47
  ns.register :graphql_schema_visible, nil
48
+ # Optional Proc +(context_hash) -> Enumerable+ of capability names (RFC 0008).
49
+ ns.register :graphql_capabilities, nil
35
50
  ns.register :graphql_max_complexity, 300
36
51
  ns.register :graphql_max_depth, 18
37
52
  ns.register :graphql_batch_action_max_ids, 5000
@@ -64,7 +79,10 @@ module ActiveAdmin
64
79
  module ResourceDSLMethods
65
80
  def graphql(&block)
66
81
  if block
67
- ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(config.graphql_config).instance_exec(&block)
82
+ cfg = config.graphql_config
83
+ # Presence of a graphql block is an intentional opt-in when the namespace default is false.
84
+ cfg.enabled = true
85
+ ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(cfg).instance_exec(&block)
68
86
  end
69
87
  config.graphql_config
70
88
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Builds +activeadmin_policies+ payloads for a namespace.
6
+ #
7
+ # Set +graphql_policy_adapter+ to this class (default), another subclass, or a
8
+ # constant name +String+. Omit policy fields with +graphql_policies_enabled = false+.
9
+ class PolicyAdapter
10
+ POLICY_ACTIONS = {
11
+ read: ActiveAdmin::Authorization::READ,
12
+ create: ActiveAdmin::Authorization::CREATE,
13
+ update: ActiveAdmin::Authorization::UPDATE,
14
+ destroy: ActiveAdmin::Authorization::DESTROY
15
+ }.freeze
16
+
17
+ attr_reader :namespace
18
+
19
+ def initialize(namespace:)
20
+ @namespace = namespace
21
+ end
22
+
23
+ # Whether this resource gets an object +activeadmin_policies+ field and a
24
+ # row in the +activeadmin_policies+ resources matrix.
25
+ def include_resource?(aa_resource)
26
+ aa_resource.graphql_config.policies_enabled
27
+ end
28
+
29
+ def policy_set(auth:, subject_owner:, subject:, context:)
30
+ base = {
31
+ allowed_actions: allowed_base_actions(
32
+ auth: auth,
33
+ subject_owner: subject_owner,
34
+ subject: subject,
35
+ context: context
36
+ ),
37
+ allowed_member_actions: [],
38
+ allowed_collection_actions: [],
39
+ allowed_batch_actions: []
40
+ }
41
+
42
+ append_resource_action_names!(base, auth: auth, subject_owner: subject_owner, subject: subject)
43
+
44
+ if (extra = namespace.graphql_policy_extra).respond_to?(:call)
45
+ out = extra.call(context, subject_owner, subject, base)
46
+ base = out if out.is_a?(Hash)
47
+ end
48
+ if (transform = namespace.graphql_policy_transform).respond_to?(:call)
49
+ out = transform.call(context, subject_owner, subject, base)
50
+ base = out if out.is_a?(Hash)
51
+ end
52
+ base
53
+ end
54
+
55
+ private
56
+
57
+ def policy_action_pairs
58
+ hook = namespace.graphql_policy_actions
59
+ if hook.respond_to?(:call)
60
+ raw = hook.call(namespace)
61
+ return raw.to_h.transform_keys(&:to_sym) if raw.respond_to?(:to_h)
62
+ end
63
+ POLICY_ACTIONS
64
+ end
65
+
66
+ def allowed_base_actions(auth:, subject_owner:, subject:, context:)
67
+ action_mapper = namespace.graphql_policy_action_mapper
68
+ policy_action_pairs.each_with_object([]) do |(name, default_action), out|
69
+ allowed = if action_mapper.respond_to?(:call)
70
+ action_mapper.call(context, subject_owner, subject, name, default_action)
71
+ else
72
+ auth.authorized?(subject_owner, default_action, subject)
73
+ end
74
+ out << name.to_s if allowed
75
+ end
76
+ end
77
+
78
+ def append_resource_action_names!(base, auth:, subject_owner:, subject:)
79
+ return unless subject_owner.is_a?(ActiveAdmin::Resource)
80
+
81
+ class_subject = subject_owner.resource_class
82
+ if subject.is_a?(Class)
83
+ return unless auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, class_subject)
84
+
85
+ base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
86
+ base[:allowed_collection_actions] = subject_owner.collection_actions.map { |a| a.name.to_s }
87
+ base[:allowed_batch_actions] = subject_owner.batch_actions.map { |a| a.sym.to_s }
88
+ elsif auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, subject)
89
+ base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -6,10 +6,11 @@ module ActiveAdmin
6
6
  class PolicySetCache
7
7
  class << self
8
8
  def fetch(context, subject_owner:, subject:)
9
- namespace = context[:namespace]
10
- unless namespace
11
- return policy_builder(context).send(
12
- :build_policy_set,
9
+ adapter = policy_adapter(context)
10
+ raise ActiveAdmin::DependencyError, "graphql_policy_adapter is disabled" unless adapter
11
+
12
+ unless context[:namespace]
13
+ return adapter.policy_set(
13
14
  auth: context[:auth],
14
15
  subject_owner: subject_owner,
15
16
  subject: subject,
@@ -19,8 +20,7 @@ module ActiveAdmin
19
20
 
20
21
  cache = context[:policy_set_cache] ||= {}
21
22
  key = cache_key(subject_owner, subject)
22
- cache[key] ||= policy_builder(context).send(
23
- :build_policy_set,
23
+ cache[key] ||= adapter.policy_set(
24
24
  auth: context[:auth],
25
25
  subject_owner: subject_owner,
26
26
  subject: subject,
@@ -28,6 +28,10 @@ module ActiveAdmin
28
28
  )
29
29
  end
30
30
 
31
+ def policy_adapter(context)
32
+ policy_builder(context).send(:policy_adapter)
33
+ end
34
+
31
35
  def policy_builder(context)
32
36
  context[:policy_schema_builder] ||= SchemaBuilder.new(context[:namespace])
33
37
  end