activeadmin-graphql 0.3.2 → 0.4.0
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 +27 -1
- 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/changelogs/20261012200000_host-customization-hooks.md +27 -0
- data/docs/changelogs/20261013080000_release-0-4-0.md +48 -0
- data/docs/graphql-api.md +173 -10
- data/docs/issues/20261012200000_host-customization-gaps.md +26 -0
- data/lib/active_admin/graphql/capability.rb +35 -0
- data/lib/active_admin/graphql/capability_definition_dsl.rb +69 -0
- data/lib/active_admin/graphql/collection_query_policy.rb +103 -0
- data/lib/active_admin/graphql/integration.rb +43 -1
- data/lib/active_admin/graphql/member_action_capability.rb +14 -0
- data/lib/active_admin/graphql/member_action_dry_run.rb +121 -0
- 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 +32 -0
- data/lib/active_admin/graphql/resource_capability_maps.rb +77 -0
- data/lib/active_admin/graphql/resource_config.rb +19 -2
- data/lib/active_admin/graphql/resource_definition_dsl.rb +22 -10
- 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/run_action_capability.rb +62 -0
- data/lib/active_admin/graphql/run_action_control_extension.rb +72 -0
- data/lib/active_admin/graphql/run_action_field.rb +103 -0
- data/lib/active_admin/graphql/run_action_mutation_config.rb +8 -0
- data/lib/active_admin/graphql/run_action_mutation_dsl.rb +18 -0
- data/lib/active_admin/graphql/run_action_preview.rb +20 -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 +9 -0
- data/lib/active_admin/graphql/schema_builder/mutation_create.rb +1 -0
- data/lib/active_admin/graphql/schema_builder/mutation_member.rb +13 -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/resolvers.rb +46 -7
- data/lib/active_admin/graphql/schema_builder/resources.rb +34 -0
- data/lib/active_admin/graphql/schema_builder/types_object.rb +19 -8
- data/lib/active_admin/graphql/schema_builder/visibility.rb +22 -7
- data/lib/active_admin/graphql/schema_builder/wire.rb +4 -1
- data/lib/active_admin/graphql/schema_field.rb +97 -6
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +9 -0
- metadata +21 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Block body for +capability :name do ... end+ (RFC 0008, RFC 0009).
|
|
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, mode: nil)
|
|
18
|
+
@config = config
|
|
19
|
+
@capability_name = capability_name.to_sym
|
|
20
|
+
@mode = mode
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fields(*names, mode: nil)
|
|
24
|
+
@config.assign_field_capabilities!(
|
|
25
|
+
@capability_name,
|
|
26
|
+
names,
|
|
27
|
+
mode: mode.nil? ? @mode : mode
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def queries(*names)
|
|
32
|
+
keys = names.flatten.map { |name| normalize_query_key(name) }
|
|
33
|
+
@config.assign_query_capabilities!(@capability_name, keys)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def mutations(*names)
|
|
37
|
+
keys = names.flatten.map { |name| normalize_mutation_key(name) }
|
|
38
|
+
@config.assign_mutation_capabilities!(@capability_name, keys)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def member_actions(*names)
|
|
42
|
+
keys = names.flatten.map { |name| :"member_#{name.to_s.tr("-", "_")}" }
|
|
43
|
+
@config.assign_mutation_capabilities!(@capability_name, keys)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def batch_actions(*names)
|
|
47
|
+
keys = names.flatten.map { |name| :"batch_#{name.to_s.tr("-", "_")}" }
|
|
48
|
+
@config.assign_mutation_capabilities!(@capability_name, keys)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def collection_actions(*names)
|
|
52
|
+
keys = names.flatten.map { |name| :"collection_#{name.to_s.tr("-", "_")}" }
|
|
53
|
+
@config.assign_mutation_capabilities!(@capability_name, keys)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def normalize_query_key(name)
|
|
59
|
+
key = name.to_sym
|
|
60
|
+
QUERY_ALIASES.fetch(key, key)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def normalize_mutation_key(name)
|
|
64
|
+
key = name.to_sym
|
|
65
|
+
MUTATION_ALIASES.fetch(key, key)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Built-in helpers for +graphql_collection_query_policy+ (RFC 0011).
|
|
6
|
+
module CollectionQueryPolicy
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def require_visible_fields!(
|
|
10
|
+
context:,
|
|
11
|
+
aa_resource:,
|
|
12
|
+
q: nil,
|
|
13
|
+
order: nil,
|
|
14
|
+
allow_q_keys: [],
|
|
15
|
+
field_grants: {},
|
|
16
|
+
infer_field_grants: true
|
|
17
|
+
)
|
|
18
|
+
allow = Array(allow_q_keys).map(&:to_s)
|
|
19
|
+
grants = merge_field_grants(aa_resource, field_grants, infer: infer_field_grants)
|
|
20
|
+
|
|
21
|
+
normalize_q(q).each_key do |raw_key|
|
|
22
|
+
key = raw_key.to_s
|
|
23
|
+
next if allow.include?(key)
|
|
24
|
+
|
|
25
|
+
field_name = field_from_predicate_key(key)
|
|
26
|
+
unless field_name
|
|
27
|
+
raise ::GraphQL::ExecutionError, "unsupported collection filter key #{key.inspect}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
assert_field_filterable!(context, aa_resource, field_name, grants)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
assert_order_filterable!(context, aa_resource, order, grants) if order.present?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def merge_field_grants(aa_resource, field_grants, infer: true)
|
|
37
|
+
explicit = field_grants.to_h.transform_keys(&:to_s).transform_values(&:to_sym)
|
|
38
|
+
return explicit unless infer
|
|
39
|
+
|
|
40
|
+
inferred = aa_resource.graphql_config.field_capabilities.to_h do |name, capability|
|
|
41
|
+
[name.to_s, capability.to_sym]
|
|
42
|
+
end
|
|
43
|
+
inferred.merge(explicit)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def normalize_q(q)
|
|
47
|
+
return {} if q.nil?
|
|
48
|
+
return q.to_unsafe_h if q.respond_to?(:to_unsafe_h)
|
|
49
|
+
|
|
50
|
+
q.respond_to?(:to_h) ? q.to_h : {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def field_from_predicate_key(key)
|
|
54
|
+
predicates = ransack_predicate_names
|
|
55
|
+
predicates.sort_by(&:length).reverse_each do |predicate|
|
|
56
|
+
suffix = "_#{predicate}"
|
|
57
|
+
return key[0...-suffix.length] if key.end_with?(suffix) && key.length > suffix.length
|
|
58
|
+
end
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def ransack_predicate_names
|
|
63
|
+
if defined?(Ransack) && Ransack.respond_to?(:predicates)
|
|
64
|
+
Ransack.predicates.keys.map(&:to_s)
|
|
65
|
+
else
|
|
66
|
+
%w[eq cont start end gt lt gteq lteq in not_in null present blank matches does_not_match]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def assert_order_filterable!(context, aa_resource, order, grants)
|
|
71
|
+
column = order.to_s.sub(/_(asc|desc|ascs|descs)\z/i, "")
|
|
72
|
+
return if column.blank?
|
|
73
|
+
|
|
74
|
+
assert_field_filterable!(context, aa_resource, column, grants)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def assert_field_filterable!(context, aa_resource, field_name, grants)
|
|
78
|
+
unless field_schema_visible?(context, aa_resource, field_name)
|
|
79
|
+
raise ::GraphQL::ExecutionError, "not authorized to filter on #{field_name}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
capability = grants[field_name.to_s]
|
|
83
|
+
return if capability.nil?
|
|
84
|
+
return if Capability.granted?(context, capability)
|
|
85
|
+
|
|
86
|
+
raise ::GraphQL::ExecutionError, "not authorized to filter on #{field_name}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def field_schema_visible?(context, aa_resource, field_name)
|
|
90
|
+
type_name = graphql_type_name_for(aa_resource)
|
|
91
|
+
type = context.schema.get_type(type_name, context)
|
|
92
|
+
return false unless type
|
|
93
|
+
|
|
94
|
+
!context.schema.get_field(type, field_name.to_s, context).nil?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def graphql_type_name_for(aa_resource)
|
|
98
|
+
aa_resource.graphql_config.graphql_type_name.presence ||
|
|
99
|
+
aa_resource.resource_class.name.delete_prefix("::").gsub("::", "__")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
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
|
|
|
@@ -25,13 +26,51 @@ module ActiveAdmin
|
|
|
25
26
|
|
|
26
27
|
def register_namespace_settings!
|
|
27
28
|
ns = ActiveAdmin::NamespaceSettings
|
|
29
|
+
register_surface_namespace_settings!(ns)
|
|
30
|
+
register_capability_namespace_settings!(ns)
|
|
31
|
+
register_limit_namespace_settings!(ns)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def register_surface_namespace_settings!(ns)
|
|
28
35
|
ns.register :graphql, false
|
|
36
|
+
# true preserves 0.3.x: every ActiveRecord resource is in the schema once the namespace endpoint is on.
|
|
37
|
+
# Set false for opt-in per resource (empty graphql block or enable!).
|
|
38
|
+
ns.register :graphql_resource_enabled_default, true
|
|
39
|
+
ns.register :graphql_queries_enabled_default, true
|
|
40
|
+
ns.register :graphql_mutations_enabled_default, true
|
|
41
|
+
ns.register :graphql_run_actions_enabled_default, true
|
|
42
|
+
ns.register :graphql_policies_enabled, true
|
|
43
|
+
ns.register :graphql_policies_enabled_default, true
|
|
44
|
+
# Payload builder for policy fields when graphql_policies_enabled is true.
|
|
45
|
+
ns.register :graphql_policy_adapter, ActiveAdmin::GraphQL::PolicyAdapter
|
|
46
|
+
ns.register :graphql_registered_resource_enabled, true
|
|
47
|
+
ns.register :graphql_belongs_to_associations_enabled, true
|
|
29
48
|
ns.register :graphql_path, "graphql"
|
|
30
49
|
ns.register :graphql_multiplex_max, 20
|
|
31
50
|
ns.register :graphql_dataloader, nil
|
|
32
51
|
ns.register :graphql_visibility, nil
|
|
33
52
|
ns.register :graphql_visibility_profile, nil
|
|
34
53
|
ns.register :graphql_schema_visible, nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def register_capability_namespace_settings!(ns)
|
|
57
|
+
# Optional Proc +(context_hash) -> Enumerable+ of capability names (RFC 0008).
|
|
58
|
+
ns.register :graphql_capabilities, nil
|
|
59
|
+
# RFC 0009: default field capability mode (:omit or :null).
|
|
60
|
+
ns.register :graphql_field_capability_mode_default, :omit
|
|
61
|
+
# RFC 0009: optional Proc +(context, meta) -> Boolean+ to omit object/root fields.
|
|
62
|
+
ns.register :graphql_field_visible, nil
|
|
63
|
+
# RFC 0010: optional Proc +(context, meta) -> void+ after run-action outcomes.
|
|
64
|
+
ns.register :graphql_run_action_audit, nil
|
|
65
|
+
# RFC 0011: optional Proc +(context, meta) -> void+ at collection resolve start.
|
|
66
|
+
ns.register :graphql_collection_query_policy, nil
|
|
67
|
+
# RFC 0012: optional default capabilities for run-action mutation fields.
|
|
68
|
+
ns.register :graphql_member_action_capability_default, nil
|
|
69
|
+
ns.register :graphql_batch_action_capability_default, nil
|
|
70
|
+
ns.register :graphql_collection_action_capability_default, nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def register_limit_namespace_settings!(ns)
|
|
35
74
|
ns.register :graphql_max_complexity, 300
|
|
36
75
|
ns.register :graphql_max_depth, 18
|
|
37
76
|
ns.register :graphql_batch_action_max_ids, 5000
|
|
@@ -64,7 +103,10 @@ module ActiveAdmin
|
|
|
64
103
|
module ResourceDSLMethods
|
|
65
104
|
def graphql(&block)
|
|
66
105
|
if block
|
|
67
|
-
|
|
106
|
+
cfg = config.graphql_config
|
|
107
|
+
# Presence of a graphql block is an intentional opt-in when the namespace default is false.
|
|
108
|
+
cfg.enabled = true
|
|
109
|
+
ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(cfg).instance_exec(&block)
|
|
68
110
|
end
|
|
69
111
|
config.graphql_config
|
|
70
112
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Compatibility wrapper around {RunActionCapability} for member actions (RFC 0012).
|
|
6
|
+
module MemberActionCapability
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def resolve(namespace, aa_res, action_name = nil)
|
|
10
|
+
RunActionCapability.resolve(namespace, aa_res, :member, action_name)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Preview context and resolve helpers for +member_action_mutation dry_run:+ (RFC 0010).
|
|
6
|
+
class MemberActionDryRun
|
|
7
|
+
PreviewContext = RunActionPreview
|
|
8
|
+
|
|
9
|
+
def self.resolve(
|
|
10
|
+
aa_res,
|
|
11
|
+
proxy:,
|
|
12
|
+
context:,
|
|
13
|
+
action:,
|
|
14
|
+
id:,
|
|
15
|
+
params_h:,
|
|
16
|
+
extras:,
|
|
17
|
+
per:,
|
|
18
|
+
dry_run:
|
|
19
|
+
)
|
|
20
|
+
new(aa_res, proxy:, context:, action:, id:, params_h:, extras:, per:, dry_run:).call
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(aa_res, proxy:, context:, action:, id:, params_h:, extras:, per:, dry_run:)
|
|
24
|
+
@aa_res = aa_res
|
|
25
|
+
@proxy = proxy
|
|
26
|
+
@context = context
|
|
27
|
+
@action = action
|
|
28
|
+
@id = id
|
|
29
|
+
@params_h = params_h
|
|
30
|
+
@extras = extras
|
|
31
|
+
@per = per
|
|
32
|
+
@dry_run = dry_run
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
record = @proxy.find_member(@id)
|
|
37
|
+
preview_result = run_preview(record)
|
|
38
|
+
|
|
39
|
+
if @dry_run
|
|
40
|
+
result = preview_result || default_dry_run_result(record)
|
|
41
|
+
audit!(result, dry_run: true)
|
|
42
|
+
return result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if preview_result && !preview_result.ok
|
|
46
|
+
audit!(preview_result, dry_run: false)
|
|
47
|
+
return preview_result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
result = run_action
|
|
51
|
+
audit!(result, dry_run: false)
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def run_preview(record)
|
|
58
|
+
return nil unless @per.preview_proc
|
|
59
|
+
|
|
60
|
+
PreviewContext.new.instance_exec(
|
|
61
|
+
record,
|
|
62
|
+
proxy: @proxy,
|
|
63
|
+
context: @context,
|
|
64
|
+
aa_resource: @aa_res,
|
|
65
|
+
auth: @context[:auth],
|
|
66
|
+
action: @action,
|
|
67
|
+
id: @id,
|
|
68
|
+
params: @params_h,
|
|
69
|
+
**@extras,
|
|
70
|
+
&@per.preview_proc
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def default_dry_run_result(record)
|
|
75
|
+
body = if record
|
|
76
|
+
"would_run:#{@action}:#{record.id}"
|
|
77
|
+
else
|
|
78
|
+
"would_run:#{@action}"
|
|
79
|
+
end
|
|
80
|
+
RunActionPayload::Result.new(ok: true, status: 200, location: nil, body: body)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_action
|
|
84
|
+
resolve = @per.resolve_proc || @aa_res.graphql_config.member_run_action.resolve_proc
|
|
85
|
+
if resolve
|
|
86
|
+
resolve.call(
|
|
87
|
+
proxy: @proxy,
|
|
88
|
+
context: @context,
|
|
89
|
+
aa_resource: @aa_res,
|
|
90
|
+
auth: @context[:auth],
|
|
91
|
+
action: @action,
|
|
92
|
+
id: @id,
|
|
93
|
+
params: @params_h,
|
|
94
|
+
**@extras
|
|
95
|
+
)
|
|
96
|
+
else
|
|
97
|
+
@proxy.run_member_action(@action, @id, extra_params: @params_h)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def audit!(result, dry_run:)
|
|
102
|
+
hook = @context[:namespace]&.graphql_run_action_audit
|
|
103
|
+
return if hook.nil?
|
|
104
|
+
|
|
105
|
+
hook.call(
|
|
106
|
+
@context,
|
|
107
|
+
{
|
|
108
|
+
action: @action,
|
|
109
|
+
id: @id,
|
|
110
|
+
status: result.respond_to?(:status) ? result.status : nil,
|
|
111
|
+
dry_run: dry_run,
|
|
112
|
+
ok: result.respond_to?(:ok) ? result.ok : nil,
|
|
113
|
+
reason: result.respond_to?(:body) ? result.body : nil
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
rescue
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
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
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "capability_definition_dsl"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin
|
|
6
|
+
module GraphQL
|
|
7
|
+
# +capability+ entry on {ResourceDefinitionDSL} (RFC 0008, RFC 0009).
|
|
8
|
+
module ResourceCapabilityDSL
|
|
9
|
+
def capability(
|
|
10
|
+
name,
|
|
11
|
+
fields: nil,
|
|
12
|
+
queries: nil,
|
|
13
|
+
mutations: nil,
|
|
14
|
+
member_actions: nil,
|
|
15
|
+
batch_actions: nil,
|
|
16
|
+
collection_actions: nil,
|
|
17
|
+
mode: nil,
|
|
18
|
+
&block
|
|
19
|
+
)
|
|
20
|
+
body = CapabilityDefinitionDSL.new(@config, name, mode: mode)
|
|
21
|
+
body.fields(*fields, mode: mode) if fields
|
|
22
|
+
body.queries(*queries) if queries
|
|
23
|
+
body.mutations(*mutations) if mutations
|
|
24
|
+
body.member_actions(*member_actions) if member_actions
|
|
25
|
+
body.batch_actions(*batch_actions) if batch_actions
|
|
26
|
+
body.collection_actions(*collection_actions) if collection_actions
|
|
27
|
+
body.instance_exec(&block) if block
|
|
28
|
+
name.to_sym
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Capability → member maps on {ResourceConfig} (RFC 0008, RFC 0009).
|
|
6
|
+
module ResourceCapabilityMaps
|
|
7
|
+
def field_capabilities
|
|
8
|
+
@field_capabilities ||= {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def field_capability_modes
|
|
12
|
+
@field_capability_modes ||= {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def query_capabilities
|
|
16
|
+
@query_capabilities ||= {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def mutation_capabilities
|
|
20
|
+
@mutation_capabilities ||= {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def capability_for_field(name)
|
|
24
|
+
field_capabilities[name.to_sym]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def capability_mode_for_field(name)
|
|
28
|
+
field_capability_modes[name.to_sym]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def capability_for_query(kind)
|
|
32
|
+
query_capabilities[kind.to_sym]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def capability_for_mutation(kind)
|
|
36
|
+
mutation_capabilities[kind.to_sym]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def assign_field_capabilities!(capability_name, names, mode: nil)
|
|
40
|
+
assign_capabilities!(field_capabilities, capability_name, names, label: "field")
|
|
41
|
+
return if mode.nil?
|
|
42
|
+
|
|
43
|
+
resolved = mode.to_sym
|
|
44
|
+
unless %i[omit null].include?(resolved)
|
|
45
|
+
raise ArgumentError, "capability mode must be :omit or :null, got #{mode.inspect}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
names.flatten.each do |name|
|
|
49
|
+
field_capability_modes[name.to_sym] = resolved
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def assign_query_capabilities!(capability_name, names)
|
|
54
|
+
assign_capabilities!(query_capabilities, capability_name, names, label: "query")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def assign_mutation_capabilities!(capability_name, names)
|
|
58
|
+
assign_capabilities!(mutation_capabilities, capability_name, names, label: "mutation")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def assign_capabilities!(map, capability_name, names, label:)
|
|
64
|
+
capability = capability_name.to_sym
|
|
65
|
+
names.flatten.each do |name|
|
|
66
|
+
key = name.to_sym
|
|
67
|
+
existing = map[key]
|
|
68
|
+
if existing && existing != capability
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"GraphQL #{label} #{key.inspect} already requires capability #{existing.inspect}"
|
|
71
|
+
end
|
|
72
|
+
map[key] = capability
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "run_action_mutation_config"
|
|
4
4
|
require_relative "mutation_input_config"
|
|
5
|
+
require_relative "resource_capability_maps"
|
|
5
6
|
|
|
6
7
|
module ActiveAdmin
|
|
7
8
|
module GraphQL
|
|
8
9
|
# Per-resource GraphQL options set via +graphql+ in +ActiveAdmin.register+.
|
|
9
10
|
class ResourceConfig
|
|
11
|
+
include ResourceCapabilityMaps
|
|
12
|
+
|
|
10
13
|
attr_accessor :enabled
|
|
14
|
+
attr_accessor :queries_enabled
|
|
15
|
+
attr_accessor :mutations_enabled
|
|
16
|
+
attr_accessor :run_actions_enabled
|
|
17
|
+
attr_accessor :policies_enabled
|
|
11
18
|
attr_accessor :graphql_type_name
|
|
12
19
|
attr_accessor :only_attributes
|
|
13
20
|
attr_accessor :exclude_attributes
|
|
@@ -31,8 +38,18 @@ module ActiveAdmin
|
|
|
31
38
|
# {RunActionMutationConfig#payload_type} is not set. Falls back to {RunActionPayload}.
|
|
32
39
|
attr_accessor :run_action_payload_type
|
|
33
40
|
|
|
34
|
-
def initialize
|
|
35
|
-
|
|
41
|
+
def initialize(
|
|
42
|
+
enabled: true,
|
|
43
|
+
queries_enabled: true,
|
|
44
|
+
mutations_enabled: true,
|
|
45
|
+
run_actions_enabled: true,
|
|
46
|
+
policies_enabled: true
|
|
47
|
+
)
|
|
48
|
+
@enabled = enabled
|
|
49
|
+
@queries_enabled = queries_enabled
|
|
50
|
+
@mutations_enabled = mutations_enabled
|
|
51
|
+
@run_actions_enabled = run_actions_enabled
|
|
52
|
+
@policies_enabled = policies_enabled
|
|
36
53
|
@exclude_attributes = []
|
|
37
54
|
end
|
|
38
55
|
|