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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/app/controllers/active_admin/graphql_controller.rb +19 -4
- data/docs/changelogs/20261010091415_graphql-resource-enabled-default.md +34 -0
- data/docs/changelogs/20261012050051_graphql-surface-toggles.md +40 -0
- data/docs/changelogs/20261012064500_graphql-capabilities.md +35 -0
- data/docs/changelogs/20261012113500_release-0-3-3.md +45 -0
- data/docs/graphql-api.md +124 -8
- data/lib/active_admin/graphql/capability.rb +35 -0
- data/lib/active_admin/graphql/capability_definition_dsl.rb +64 -0
- data/lib/active_admin/graphql/integration.rb +19 -1
- data/lib/active_admin/graphql/policy_adapter.rb +94 -0
- data/lib/active_admin/graphql/policy_set_cache.rb +10 -6
- data/lib/active_admin/graphql/resource_capability_dsl.rb +31 -0
- data/lib/active_admin/graphql/resource_capability_maps.rb +59 -0
- data/lib/active_admin/graphql/resource_config.rb +19 -2
- data/lib/active_admin/graphql/resource_definition_dsl.rb +5 -4
- data/lib/active_admin/graphql/resource_methods.rb +7 -1
- data/lib/active_admin/graphql/resource_surface_dsl.rb +48 -0
- data/lib/active_admin/graphql/schema_builder/build.rb +52 -3
- data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +7 -0
- data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +8 -0
- data/lib/active_admin/graphql/schema_builder/mutation_create.rb +1 -0
- data/lib/active_admin/graphql/schema_builder/mutation_member.rb +8 -0
- data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +15 -10
- data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +2 -0
- data/lib/active_admin/graphql/schema_builder/query_type.rb +2 -1
- data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +3 -0
- data/lib/active_admin/graphql/schema_builder/query_type_member.rb +1 -0
- data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +9 -63
- data/lib/active_admin/graphql/schema_builder/resources.rb +34 -0
- data/lib/active_admin/graphql/schema_builder/types_object.rb +18 -8
- data/lib/active_admin/graphql/schema_builder/visibility.rb +22 -7
- data/lib/active_admin/graphql/schema_builder/wire.rb +3 -1
- data/lib/active_admin/graphql/schema_field.rb +18 -2
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +2 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36bec145bd90193ae32e34aa54b7a6ed01dff13a8b104269b771f797262b2480
|
|
4
|
+
data.tar.gz: 82724e08af33a67a1a9146813965e5d836006620ff503099842addcdeaab16cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
205
|
-
(for example `posts`), plus a
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
215
|
-
`*_batch_action`, `*_member_action`, and
|
|
216
|
-
resource defines the corresponding batch,
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
unless
|
|
11
|
-
|
|
12
|
-
|
|
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] ||=
|
|
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
|