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
@@ -0,0 +1,31 @@
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).
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
+ &block
18
+ )
19
+ body = CapabilityDefinitionDSL.new(@config, name)
20
+ body.fields(*fields) if fields
21
+ body.queries(*queries) if queries
22
+ body.mutations(*mutations) if mutations
23
+ body.member_actions(*member_actions) if member_actions
24
+ body.batch_actions(*batch_actions) if batch_actions
25
+ body.collection_actions(*collection_actions) if collection_actions
26
+ body.instance_exec(&block) if block
27
+ name.to_sym
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Capability → member maps on {ResourceConfig} (RFC 0008).
6
+ module ResourceCapabilityMaps
7
+ def field_capabilities
8
+ @field_capabilities ||= {}
9
+ end
10
+
11
+ def query_capabilities
12
+ @query_capabilities ||= {}
13
+ end
14
+
15
+ def mutation_capabilities
16
+ @mutation_capabilities ||= {}
17
+ end
18
+
19
+ def capability_for_field(name)
20
+ field_capabilities[name.to_sym]
21
+ end
22
+
23
+ def capability_for_query(kind)
24
+ query_capabilities[kind.to_sym]
25
+ end
26
+
27
+ def capability_for_mutation(kind)
28
+ mutation_capabilities[kind.to_sym]
29
+ end
30
+
31
+ def assign_field_capabilities!(capability_name, names)
32
+ assign_capabilities!(field_capabilities, capability_name, names, label: "field")
33
+ end
34
+
35
+ def assign_query_capabilities!(capability_name, names)
36
+ assign_capabilities!(query_capabilities, capability_name, names, label: "query")
37
+ end
38
+
39
+ def assign_mutation_capabilities!(capability_name, names)
40
+ assign_capabilities!(mutation_capabilities, capability_name, names, label: "mutation")
41
+ end
42
+
43
+ private
44
+
45
+ def assign_capabilities!(map, capability_name, names, label:)
46
+ capability = capability_name.to_sym
47
+ names.flatten.each do |name|
48
+ key = name.to_sym
49
+ existing = map[key]
50
+ if existing && existing != capability
51
+ raise ArgumentError,
52
+ "GraphQL #{label} #{key.inspect} already requires capability #{existing.inspect}"
53
+ end
54
+ map[key] = capability
55
+ end
56
+ end
57
+ end
58
+ end
59
+ 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
- @enabled = true
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
 
@@ -3,19 +3,20 @@
3
3
  require_relative "run_action_mutation_config"
4
4
  require_relative "run_action_mutation_dsl"
5
5
  require_relative "mutation_input_definition_dsl"
6
+ require_relative "resource_surface_dsl"
7
+ require_relative "resource_capability_dsl"
6
8
 
7
9
  module ActiveAdmin
8
10
  module GraphQL
9
11
  # DSL for +graphql do ... end+ inside +ActiveAdmin.register+.
10
12
  class ResourceDefinitionDSL
13
+ include ResourceSurfaceDSL
14
+ include ResourceCapabilityDSL
15
+
11
16
  def initialize(config)
12
17
  @config = config
13
18
  end
14
19
 
15
- def disable!
16
- @config.enabled = false
17
- end
18
-
19
20
  # Declare GraphQL identity columns when ActiveRecord has no primary key / id column.
20
21
  # See RFC 0007.
21
22
  def identity(*columns, separator: nil, encode: nil, decode: nil)
@@ -7,7 +7,13 @@ module ActiveAdmin
7
7
  SERVER_OWNED_TIMESTAMP_COLUMNS = %w[created_at updated_at].freeze
8
8
 
9
9
  def graphql_config
10
- @graphql_config ||= ActiveAdmin::GraphQL::ResourceConfig.new
10
+ @graphql_config ||= ActiveAdmin::GraphQL::ResourceConfig.new(
11
+ enabled: namespace.graphql_resource_enabled_default,
12
+ queries_enabled: namespace.graphql_queries_enabled_default,
13
+ mutations_enabled: namespace.graphql_mutations_enabled_default,
14
+ run_actions_enabled: namespace.graphql_run_actions_enabled_default,
15
+ policies_enabled: namespace.graphql_policies_enabled_default
16
+ )
11
17
  end
12
18
 
13
19
  def attributes_for_graphql
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Inclusion and schema-surface toggles for +graphql do ... end+.
6
+ module ResourceSurfaceDSL
7
+ def enable!
8
+ @config.enabled = true
9
+ end
10
+
11
+ def disable!
12
+ @config.enabled = false
13
+ end
14
+
15
+ def enable_queries!
16
+ @config.queries_enabled = true
17
+ end
18
+
19
+ def disable_queries!
20
+ @config.queries_enabled = false
21
+ end
22
+
23
+ def enable_mutations!
24
+ @config.mutations_enabled = true
25
+ end
26
+
27
+ def disable_mutations!
28
+ @config.mutations_enabled = false
29
+ end
30
+
31
+ def enable_run_actions!
32
+ @config.run_actions_enabled = true
33
+ end
34
+
35
+ def disable_run_actions!
36
+ @config.run_actions_enabled = false
37
+ end
38
+
39
+ def enable_policies!
40
+ @config.policies_enabled = true
41
+ end
42
+
43
+ def disable_policies!
44
+ @config.policies_enabled = false
45
+ end
46
+ end
47
+ end
48
+ end
@@ -25,7 +25,8 @@ module ActiveAdmin
25
25
  @aa_by_graphql_type_name[graphql_type_name_for(aa_res)] = aa_res
26
26
  end
27
27
 
28
- wire_belongs_to_associations
28
+ wire_belongs_to_associations if @namespace.graphql_belongs_to_associations_enabled
29
+ verify_field_capabilities!
29
30
 
30
31
  active_resources.each do |aa_res|
31
32
  model = aa_res.resource_class
@@ -35,9 +36,10 @@ module ActiveAdmin
35
36
  @find_input_types[model] = build_find_input_type(aa_res)
36
37
  end
37
38
 
38
- union_members = @object_types.values.uniq
39
+ queryable = queryable_resources
40
+ union_members = queryable.filter_map { |aa_res| @object_types[aa_res.resource_class] }.uniq
39
41
  registered_resource_union =
40
- if union_members.any?
42
+ if @namespace.graphql_registered_resource_enabled && union_members.any?
41
43
  u = Class.new(::GraphQL::Schema::Union) do
42
44
  graphql_name "ActiveAdminRegisteredResource"
43
45
  description "Any resource object type registered for GraphQL in this namespace."
@@ -47,10 +49,12 @@ module ActiveAdmin
47
49
  u
48
50
  end
49
51
 
52
+ ensure_query_surface!(registered_resource_union: registered_resource_union)
50
53
  query_type = build_query_type(registered_resource_union: registered_resource_union)
51
54
  mutation_type = build_mutation_type
52
55
 
53
56
  model_to_type = @object_types
57
+ queryable_models = queryable.map(&:resource_class)
54
58
 
55
59
  schema = Class.new(::GraphQL::Schema)
56
60
  schema.query(query_type)
@@ -61,6 +65,11 @@ module ActiveAdmin
61
65
  if registered_resource_union
62
66
  schema.define_singleton_method(:resolve_type) do |abstract_type, obj, ctx|
63
67
  if abstract_type == registered_resource_union
68
+ unless queryable_models.include?(obj.class)
69
+ raise ::GraphQL::ExecutionError,
70
+ "ActiveAdminRegisteredResource could not resolve type for #{obj.class.name}"
71
+ end
72
+
64
73
  typ = model_to_type[obj.class]
65
74
  unless typ
66
75
  raise ::GraphQL::ExecutionError,
@@ -78,6 +87,46 @@ module ActiveAdmin
78
87
  hook.call(schema) if hook.respond_to?(:call)
79
88
  schema
80
89
  end
90
+
91
+ private
92
+
93
+ def ensure_query_surface!(registered_resource_union:)
94
+ return if registered_resource_union
95
+ return if queryable_resources.any?
96
+ return if policy_adapter
97
+ return if namespace_page_graphql_fields?
98
+
99
+ raise ActiveAdmin::DependencyError,
100
+ "GraphQL Query type has no fields for namespace #{@namespace.name.inspect}. " \
101
+ "Enable list/show (graphql_queries_enabled_default / enable_queries!), " \
102
+ "policies (graphql_policies_enabled), registered_resource, " \
103
+ "or a page graphql_field — or turn off graphql for this namespace."
104
+ end
105
+
106
+ def namespace_page_graphql_fields?
107
+ @namespace.resources.any? do |resource|
108
+ resource.is_a?(ActiveAdmin::Page) && resource.graphql_fields.any?
109
+ end
110
+ end
111
+
112
+ def verify_field_capabilities!
113
+ @object_types.each do |model, type_class|
114
+ aa_res = @aa_by_model[model]
115
+ next unless aa_res
116
+
117
+ declared = aa_res.graphql_config.field_capabilities.keys
118
+ next if declared.empty?
119
+
120
+ present = type_class.own_fields.keys.map { |name| name.to_s.to_sym }
121
+ unknown = declared - present
122
+ next if unknown.empty?
123
+
124
+ raise ActiveAdmin::DependencyError,
125
+ "GraphQL capability fields #{unknown.map(&:inspect).join(", ")} are not on " \
126
+ "#{type_class.graphql_name} (resource #{aa_res.resource_name}). " \
127
+ "Use a generated attribute, belongs_to association, or configure field name."
128
+ end
129
+ end
81
130
  end
82
131
  end
83
132
  end
@@ -15,6 +15,7 @@ module ActiveAdmin
15
15
  proc do
16
16
  field fname.to_sym, batch_payload_t, null: false, camelize: false,
17
17
  visibility: {kind: :mutation_batch_action, graphql_type_name: sb.send(:graphql_type_name_for, aa_res), resource: aa_res, field_name: fname},
18
+ capability: aa_res.graphql_config.capability_for_mutation(:batch_action),
18
19
  description: "Run a batch action registered on #{aa_res.resource_name} (+batch_action+ DSL), " \
19
20
  "same as the index batch UI (+collection_selection+ / +batch_action_inputs+)." do
20
21
  argument :batch_action, ::GraphQL::Types::String, required: true, camelize: false
@@ -39,6 +40,12 @@ module ActiveAdmin
39
40
  unless allowed.include?(ba)
40
41
  raise ::GraphQL::ExecutionError, "Unknown batch_action #{ba.inspect} for #{plural}"
41
42
  end
43
+ unless Capability.granted?(
44
+ context,
45
+ aa_res.graphql_config.capability_for_mutation(:"batch_#{ba.tr("-", "_")}")
46
+ )
47
+ raise ::GraphQL::ExecutionError, "not authorized for batch action #{ba}"
48
+ end
42
49
 
43
50
  max_ids = ns.graphql_batch_action_max_ids
44
51
  if max_ids.is_a?(Integer) && max_ids.positive? && ids.size > max_ids
@@ -21,6 +21,7 @@ module ActiveAdmin
21
21
  proc do
22
22
  field fname.to_sym, collection_payload_t, null: false, camelize: false,
23
23
  visibility: {kind: :mutation_collection_action, graphql_type_name: sb.send(:graphql_type_name_for, aa_res), resource: aa_res, field_name: fname, aggregate: true},
24
+ capability: aa_res.graphql_config.capability_for_mutation(:collection_action),
24
25
  description: "Invoke a +collection_action+ on #{aa_res.resource_name} by name (same scoping as REST +index+). Prefer per-action fields +#{plural}_collection_<action>+ for distinct inputs or return types." do
25
26
  argument :action, ::GraphQL::Types::String, required: true, camelize: false
26
27
  argument :params, [KeyValuePairInput], required: false, camelize: false,
@@ -43,6 +44,12 @@ module ActiveAdmin
43
44
  unless allowed.include?(act)
44
45
  raise ::GraphQL::ExecutionError, "Unknown collection action #{act.inspect} for #{plural}"
45
46
  end
47
+ unless Capability.granted?(
48
+ context,
49
+ aa_res.graphql_config.capability_for_mutation(:"collection_#{act.tr("-", "_")}")
50
+ )
51
+ raise ::GraphQL::ExecutionError, "not authorized for collection action #{act}"
52
+ end
46
53
 
47
54
  proxy = ResourceQueryProxy.new(
48
55
  aa_resource: aa_res,
@@ -79,6 +86,7 @@ module ActiveAdmin
79
86
  field_name: field_name,
80
87
  collection_action: action_name
81
88
  },
89
+ capability: aa_res.graphql_config.capability_for_mutation(:"collection_#{action_name.tr("-", "_")}"),
82
90
  description: "Invoke +collection_action+ #{action_name} on #{aa_res.resource_name} (same scoping as REST +index+)." do
83
91
  argument :params, [KeyValuePairInput], required: false, camelize: false,
84
92
  description: "Extra request params as flat key/value pairs."
@@ -13,6 +13,7 @@ module ActiveAdmin
13
13
  proc do
14
14
  field fname.to_sym, type_c, null: true, camelize: false,
15
15
  visibility: {kind: :mutation_create, graphql_type_name: sb.send(:graphql_type_name_for, aa_res), resource: aa_res},
16
+ capability: aa_res.graphql_config.capability_for_mutation(:create),
16
17
  description: "Create #{model.name}" do
17
18
  argument :input, create_input, required: true, camelize: false
18
19
  end
@@ -21,6 +21,7 @@ module ActiveAdmin
21
21
  proc do
22
22
  field fname.to_sym, member_payload_t, null: false, camelize: false,
23
23
  visibility: {kind: :mutation_member_action, graphql_type_name: sb.send(:graphql_type_name_for, aa_res), resource: aa_res, field_name: fname, aggregate: true},
24
+ capability: aa_res.graphql_config.capability_for_mutation(:member_action),
24
25
  description: "Invoke a +member_action+ on #{aa_res.resource_name} by name (same scoping as REST +show+). Prefer per-action fields +#{plural}_member_<action>+ for distinct inputs or return types." do
25
26
  argument :action, ::GraphQL::Types::String, required: true, camelize: false
26
27
  argument :id, ::GraphQL::Types::ID, required: true, camelize: false
@@ -44,6 +45,12 @@ module ActiveAdmin
44
45
  unless allowed.include?(act)
45
46
  raise ::GraphQL::ExecutionError, "Unknown member action #{act.inspect} for #{plural}"
46
47
  end
48
+ unless Capability.granted?(
49
+ context,
50
+ aa_res.graphql_config.capability_for_mutation(:"member_#{act.tr("-", "_")}")
51
+ )
52
+ raise ::GraphQL::ExecutionError, "not authorized for member action #{act}"
53
+ end
47
54
 
48
55
  proxy = ResourceQueryProxy.new(
49
56
  aa_resource: aa_res,
@@ -81,6 +88,7 @@ module ActiveAdmin
81
88
  field_name: field_name,
82
89
  member_action: action_name
83
90
  },
91
+ capability: aa_res.graphql_config.capability_for_mutation(:"member_#{action_name.tr("-", "_")}"),
84
92
  description: "Invoke +member_action+ #{action_name} on #{aa_res.resource_name} (same scoping as REST +show+)." do
85
93
  argument :id, ::GraphQL::Types::ID, required: true, camelize: false
86
94
  argument :params, [KeyValuePairInput], required: false, camelize: false,
@@ -16,18 +16,23 @@ module ActiveAdmin
16
16
  type_c = @object_types[model]
17
17
  next unless type_c
18
18
 
19
- actions = defined_actions_for(aa_res)
20
-
21
- if actions.include?(:create)
22
- mutations << mutation_create_field(sb, ns, aa_res, model, type_c)
23
- end
24
- if actions.include?(:update)
25
- mutations << mutation_update_field(sb, ns, aa_res, model, type_c)
26
- end
27
- if actions.include?(:destroy)
28
- mutations << mutation_destroy_field(sb, ns, aa_res, model)
19
+ cfg = aa_res.graphql_config
20
+ if cfg.mutations_enabled
21
+ actions = defined_actions_for(aa_res)
22
+
23
+ if actions.include?(:create)
24
+ mutations << mutation_create_field(sb, ns, aa_res, model, type_c)
25
+ end
26
+ if actions.include?(:update)
27
+ mutations << mutation_update_field(sb, ns, aa_res, model, type_c)
28
+ end
29
+ if actions.include?(:destroy)
30
+ mutations << mutation_destroy_field(sb, ns, aa_res, model)
31
+ end
29
32
  end
30
33
 
34
+ next unless cfg.run_actions_enabled
35
+
31
36
  if aa_res.batch_actions_enabled? && aa_res.batch_actions.any?
32
37
  mutations << mutation_batch_field(sb, ns, aa_res)
33
38
  end
@@ -14,6 +14,7 @@ module ActiveAdmin
14
14
  proc do
15
15
  field fname.to_sym, type_c, null: true, camelize: false,
16
16
  visibility: {kind: :mutation_update, graphql_type_name: sb.send(:graphql_type_name_for, aa_res), resource: aa_res},
17
+ capability: aa_res.graphql_config.capability_for_mutation(:update),
17
18
  description: "Update #{model.name}" do
18
19
  argument :where, find_type, required: true, camelize: false
19
20
  argument :input, update_input, required: true, camelize: false
@@ -80,6 +81,7 @@ module ActiveAdmin
80
81
  [delete_name, destroy_name].each do |fname|
81
82
  field fname.to_sym, ::GraphQL::Types::Boolean, null: false, camelize: false,
82
83
  visibility: {kind: :mutation_delete, graphql_type_name: type_basename, resource: aa_res},
84
+ capability: aa_res.graphql_config.capability_for_mutation(:destroy),
83
85
  description: "Destroy #{model.name}" do
84
86
  argument :where, find_type, required: true, camelize: false
85
87
  end
@@ -16,6 +16,7 @@ module ActiveAdmin
16
16
  aa_by_model = @aa_by_model
17
17
  object_types = @object_types
18
18
  aa_by_graphql_type_name = @aa_by_graphql_type_name
19
+ query_type_name_map = aa_by_graphql_type_name_for_queries
19
20
  list_filter_input_types = @list_filter_input_types
20
21
  find_input_types = @find_input_types
21
22
 
@@ -31,7 +32,7 @@ module ActiveAdmin
31
32
  builder: builder,
32
33
  ns: ns,
33
34
  registered_resource_union: registered_resource_union,
34
- aa_by_graphql_type_name: aa_by_graphql_type_name
35
+ aa_by_graphql_type_name: query_type_name_map
35
36
  )
36
37
  end
37
38
 
@@ -7,6 +7,8 @@ module ActiveAdmin
7
7
  def add_model_query_fields!(query_class, builder:, ns:, aa_by_model:, object_types:,
8
8
  list_filter_input_types:, find_input_types:)
9
9
  aa_by_model.each do |model, aa_res|
10
+ next unless aa_res.graphql_config.queries_enabled
11
+
10
12
  builder.define_query_collection_field!(
11
13
  query_class, model, aa_res, builder, ns, object_types, list_filter_input_types
12
14
  )
@@ -32,6 +34,7 @@ module ActiveAdmin
32
34
  field_name: plural_route,
33
35
  resource: aa_res
34
36
  },
37
+ capability: aa_res.graphql_config.capability_for_query(:collection),
35
38
  description: "Paginated list of #{model.name} (#{aa_res.resource_name.human}); " \
36
39
  "pass +filter+ or legacy +q+ / +scope+ / +order+ (Ransack +q+ matches the JSON index param). " \
37
40
  "Nested resources require the parent foreign key (same as REST route params)." do
@@ -19,6 +19,7 @@ module ActiveAdmin
19
19
  field_name: singular,
20
20
  resource: aa_res
21
21
  },
22
+ capability: aa_res.graphql_config.capability_for_query(:member),
22
23
  description: "Find #{model.name} by id (same scoping chain as REST +show+). Use +where+ or legacy +id+." do
23
24
  argument :where, find_type, required: false, camelize: false
24
25
  if ActiveAdmin::PrimaryKey.composite?(model)
@@ -4,14 +4,10 @@ module ActiveAdmin
4
4
  module GraphQL
5
5
  class SchemaBuilder
6
6
  module QueryTypePolicies
7
- POLICY_ACTIONS = {
8
- read: ActiveAdmin::Authorization::READ,
9
- create: ActiveAdmin::Authorization::CREATE,
10
- update: ActiveAdmin::Authorization::UPDATE,
11
- destroy: ActiveAdmin::Authorization::DESTROY
12
- }.freeze
13
-
14
7
  def add_activeadmin_policies_query_field!(query_class, builder:, ns:, aa_by_graphql_type_name:)
8
+ adapter = builder.send(:policy_adapter)
9
+ return unless adapter
10
+
15
11
  resource_policy_type = Class.new(::GraphQL::Schema::Object) do
16
12
  graphql_name "ActiveAdminResourcePolicies"
17
13
  field :type_name, ::GraphQL::Types::String, null: false, camelize: false
@@ -48,7 +44,9 @@ module ActiveAdmin
48
44
  end
49
45
 
50
46
  define_method(:activeadmin_policies) do
51
- resources = builder.send(:active_resources).map do |aa_res|
47
+ resources = builder.send(:active_resources).filter_map do |aa_res|
48
+ next unless adapter.include_resource?(aa_res)
49
+
52
50
  {
53
51
  type_name: builder.send(:graphql_type_name_for, aa_res),
54
52
  activeadmin_policies: ActiveAdmin::GraphQL::PolicySetCache.fetch(
@@ -74,6 +72,9 @@ module ActiveAdmin
74
72
  define_method(:activeadmin_policies_for) do |type_name:, ids:, path: nil|
75
73
  aa_res = aa_by_graphql_type_name[type_name.to_s]
76
74
  raise ::GraphQL::ExecutionError, "unknown resource type_name #{type_name.inspect}" unless aa_res
75
+ unless adapter.include_resource?(aa_res)
76
+ raise ::GraphQL::ExecutionError, "policies disabled for #{type_name.inspect}"
77
+ end
77
78
 
78
79
  proxy = ResourceQueryProxy.new(
79
80
  aa_resource: aa_res,
@@ -99,61 +100,6 @@ module ActiveAdmin
99
100
  end
100
101
  end
101
102
  end
102
-
103
- private
104
-
105
- def policy_action_pairs
106
- hook = @namespace.graphql_policy_actions
107
- if hook.respond_to?(:call)
108
- raw = hook.call(@namespace)
109
- return raw.to_h.transform_keys(&:to_sym) if raw.respond_to?(:to_h)
110
- end
111
- POLICY_ACTIONS
112
- end
113
-
114
- def allowed_base_actions(auth:, subject_owner:, subject:, context:)
115
- action_mapper = @namespace.graphql_policy_action_mapper
116
- policy_action_pairs.each_with_object([]) do |(name, default_action), out|
117
- allowed = if action_mapper.respond_to?(:call)
118
- action_mapper.call(context, subject_owner, subject, name, default_action)
119
- else
120
- auth.authorized?(subject_owner, default_action, subject)
121
- end
122
- out << name.to_s if allowed
123
- end
124
- end
125
-
126
- def build_policy_set(auth:, subject_owner:, subject:, context:)
127
- base = {
128
- allowed_actions: allowed_base_actions(auth: auth, subject_owner: subject_owner, subject: subject, context: context),
129
- allowed_member_actions: [],
130
- allowed_collection_actions: [],
131
- allowed_batch_actions: []
132
- }
133
-
134
- if subject_owner.is_a?(ActiveAdmin::Resource)
135
- class_subject = subject_owner.resource_class
136
- if subject.is_a?(Class)
137
- if auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, class_subject)
138
- base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
139
- base[:allowed_collection_actions] = subject_owner.collection_actions.map { |a| a.name.to_s }
140
- base[:allowed_batch_actions] = subject_owner.batch_actions.map { |a| a.sym.to_s }
141
- end
142
- elsif auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, subject)
143
- base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
144
- end
145
- end
146
-
147
- if (extra = @namespace.graphql_policy_extra).respond_to?(:call)
148
- out = extra.call(context, subject_owner, subject, base)
149
- base = out if out.is_a?(Hash)
150
- end
151
- if (transform = @namespace.graphql_policy_transform).respond_to?(:call)
152
- out = transform.call(context, subject_owner, subject, base)
153
- base = out if out.is_a?(Hash)
154
- end
155
- base
156
- end
157
103
  end
158
104
  end
159
105
  end
@@ -28,6 +28,40 @@ module ActiveAdmin
28
28
  aa_res.attributes_for_graphql
29
29
  end
30
30
 
31
+ # +nil+ when policy fields are off (+graphql_policies_enabled+ false).
32
+ def policy_adapter
33
+ return @policy_adapter if defined?(@policy_adapter)
34
+
35
+ unless @namespace.graphql_policies_enabled
36
+ @policy_adapter = nil
37
+ return @policy_adapter
38
+ end
39
+
40
+ raw = @namespace.graphql_policy_adapter
41
+ klass = case raw
42
+ when nil, true, :default
43
+ PolicyAdapter
44
+ when String
45
+ raw.constantize
46
+ when Class
47
+ raw
48
+ else
49
+ raise ActiveAdmin::DependencyError,
50
+ "graphql_policy_adapter must be a Class, String, or nil (got #{raw.inspect})"
51
+ end
52
+ @policy_adapter = klass.new(namespace: @namespace)
53
+ end
54
+
55
+ def queryable_resources
56
+ active_resources.select { |r| r.graphql_config.queries_enabled }
57
+ end
58
+
59
+ def aa_by_graphql_type_name_for_queries
60
+ queryable_resources.each_with_object({}) do |aa_res, map|
61
+ map[graphql_type_name_for(aa_res)] = aa_res
62
+ end
63
+ end
64
+
31
65
  def configure_schema_plugins(schema)
32
66
  if (c = @namespace.graphql_max_complexity)
33
67
  schema.max_complexity(c)