rails_admin_next 1.2.1 → 2.0.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/Gemfile +1 -1
- data/README.md +32 -20
- data/app/assets/stylesheets/rails_admin/skin.css +12 -0
- data/app/controllers/rails_admin_next/main_controller.rb +53 -7
- data/app/helpers/rails_admin_next/form_builder.rb +23 -4
- data/app/helpers/rails_admin_next/main_helper.rb +12 -1
- data/app/views/layouts/rails_admin_next/_head.html.erb +1 -1
- data/app/views/layouts/rails_admin_next/_navigation.html.erb +1 -1
- data/app/views/layouts/rails_admin_next/application.html.erb +3 -0
- data/app/views/rails_admin_next/main/_form_nested_many.html.erb +9 -4
- data/app/views/rails_admin_next/main/_form_nested_one.html.erb +8 -3
- data/app/views/rails_admin_next/main/bulk_delete.html.erb +1 -1
- data/app/views/rails_admin_next/main/dashboard.html.erb +52 -50
- data/app/views/rails_admin_next/main/index.html.erb +3 -2
- data/docs/UPGRADING.md +263 -9
- data/docs/security.md +4 -0
- data/lib/generators/rails_admin_next/templates/initializer.erb +7 -3
- data/lib/rails_admin_next/abstract_model.rb +12 -4
- data/lib/rails_admin_next/adapters/active_record.rb +47 -3
- data/lib/rails_admin_next/config/actions/base.rb +0 -1
- data/lib/rails_admin_next/config/actions/bulk_delete.rb +4 -4
- data/lib/rails_admin_next/config/actions/index.rb +3 -19
- data/lib/rails_admin_next/config/actions.rb +5 -12
- data/lib/rails_admin_next/config/fields/association.rb +1 -1
- data/lib/rails_admin_next/config/fields/types/enum.rb +8 -2
- data/lib/rails_admin_next/config/fields/types/serialized.rb +17 -2
- data/lib/rails_admin_next/config/fields/types/timestamp.rb +1 -0
- data/lib/rails_admin_next/config/has_fields.rb +1 -1
- data/lib/rails_admin_next/config.rb +29 -9
- data/lib/rails_admin_next/extensions/action_policy/authorization_adapter.rb +60 -0
- data/lib/rails_admin_next/extensions/action_policy.rb +5 -0
- data/lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb +1 -1
- data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +17 -1
- data/lib/rails_admin_next/support/composite_keys_serializer.rb +8 -2
- data/lib/rails_admin_next/support/csv_converter.rb +45 -5
- data/lib/rails_admin_next/version.rb +3 -3
- data/lib/rails_admin_next.rb +5 -17
- data/skills/rails-admin-next/SKILL.md +5 -4
- data/src/rails_admin/controllers/nested_form_controller.js +1 -1
- metadata +3 -3
- data/lib/rails_admin_next/extensions/cancancan/authorization_adapter.rb +0 -90
- data/lib/rails_admin_next/extensions/cancancan.rb +0 -5
|
@@ -9,6 +9,7 @@ module RailsAdminNext
|
|
|
9
9
|
class Timestamp < RailsAdminNext::Config::Fields::Types::Datetime
|
|
10
10
|
# Register field type for the type loader
|
|
11
11
|
RailsAdminNext::Config::Fields::Types.register(self)
|
|
12
|
+
RailsAdminNext::Config::Fields::Types.register(:timestamptz, self)
|
|
12
13
|
end
|
|
13
14
|
end
|
|
14
15
|
end
|
|
@@ -10,7 +10,7 @@ module RailsAdminNext
|
|
|
10
10
|
|
|
11
11
|
# some fields are hidden by default (belongs_to keys, has_many associations in list views.)
|
|
12
12
|
# unhide them if config specifically defines them
|
|
13
|
-
field.show if field && !field.instance_variable_get(
|
|
13
|
+
field.show if field && !field.instance_variable_get(:@visible_registered).is_a?(Proc)
|
|
14
14
|
# Specify field as virtual if type is not specifically set and field was not
|
|
15
15
|
# found in default stack
|
|
16
16
|
if field.nil? && type.nil?
|
|
@@ -73,6 +73,9 @@ module RailsAdminNext
|
|
|
73
73
|
# show Gravatar in Navigation bar
|
|
74
74
|
attr_accessor :show_gravatar
|
|
75
75
|
|
|
76
|
+
# show the "Made in Tokyo" footer under every admin page
|
|
77
|
+
attr_accessor :show_footer
|
|
78
|
+
|
|
76
79
|
# accepts a hash of static links to be shown below the main navigation
|
|
77
80
|
attr_accessor :navigation_static_links
|
|
78
81
|
attr_accessor :navigation_static_label
|
|
@@ -171,21 +174,37 @@ module RailsAdminNext
|
|
|
171
174
|
# end
|
|
172
175
|
# end
|
|
173
176
|
#
|
|
177
|
+
# The block form only gates entry: it runs before every action, but nothing checks
|
|
178
|
+
# individual records or scopes the list.
|
|
179
|
+
#
|
|
174
180
|
# To use an authorization adapter, pass the name of the adapter. For example,
|
|
175
|
-
# to use with
|
|
181
|
+
# to use with Pundit[https://github.com/varvet/pundit], pass it like this.
|
|
182
|
+
#
|
|
183
|
+
# @example Pundit
|
|
184
|
+
# RailsAdminNext.config do |config|
|
|
185
|
+
# config.authorize_with :pundit
|
|
186
|
+
# end
|
|
176
187
|
#
|
|
177
|
-
# @example
|
|
188
|
+
# @example ActionPolicy
|
|
178
189
|
# RailsAdminNext.config do |config|
|
|
179
|
-
# config.authorize_with :
|
|
190
|
+
# config.authorize_with :action_policy
|
|
180
191
|
# end
|
|
181
192
|
#
|
|
182
|
-
#
|
|
193
|
+
# A custom adapter is registered with
|
|
194
|
+
# RailsAdminNext.add_extension(key, mod, authorization: true) before it is named here.
|
|
195
|
+
# See docs/UPGRADING.md for the adapter contract.
|
|
183
196
|
#
|
|
184
197
|
# @see RailsAdminNext::Config::DEFAULT_AUTHORIZE
|
|
185
198
|
def authorize_with(*args, &block)
|
|
186
199
|
extension = args.shift
|
|
187
200
|
if extension
|
|
188
|
-
klass = RailsAdminNext::AUTHORIZATION_ADAPTERS
|
|
201
|
+
klass = RailsAdminNext::AUTHORIZATION_ADAPTERS.fetch(extension) do
|
|
202
|
+
raise ArgumentError, "Unknown authorization adapter #{extension.inspect}. " \
|
|
203
|
+
"Registered adapters: #{RailsAdminNext::AUTHORIZATION_ADAPTERS.keys.map(&:inspect).join(", ")}. " \
|
|
204
|
+
"Register a custom adapter with RailsAdminNext.add_extension(key, mod, authorization: true) " \
|
|
205
|
+
"before this config block, or pass a block to authorize_with to gate admin entry only " \
|
|
206
|
+
"(no per-record checks or list scoping). See docs/UPGRADING.md."
|
|
207
|
+
end
|
|
189
208
|
klass.setup if klass.respond_to? :setup
|
|
190
209
|
@authorize = proc do
|
|
191
210
|
@authorization_adapter = klass.new(*([self] + args).compact, &block)
|
|
@@ -249,7 +268,7 @@ module RailsAdminNext
|
|
|
249
268
|
|
|
250
269
|
# pool of all found model names from the whole application
|
|
251
270
|
def models_pool
|
|
252
|
-
(viable_models - excluded_models.collect(
|
|
271
|
+
(viable_models - excluded_models.collect { it.to_s.delete_prefix("::") }).uniq.sort
|
|
253
272
|
end
|
|
254
273
|
|
|
255
274
|
# Loads a model configuration instance from the registry or registers
|
|
@@ -272,7 +291,7 @@ module RailsAdminNext
|
|
|
272
291
|
when Class, ConstLoadSuppressor::ConstProxy
|
|
273
292
|
entity.name.to_sym
|
|
274
293
|
when String, Symbol
|
|
275
|
-
entity.to_sym
|
|
294
|
+
entity.to_s.delete_prefix("::").to_sym
|
|
276
295
|
else
|
|
277
296
|
entity.class.name.to_sym
|
|
278
297
|
end
|
|
@@ -355,6 +374,7 @@ module RailsAdminNext
|
|
|
355
374
|
@registry = {}
|
|
356
375
|
@navbar_css_classes = %w[navbar-dark bg-primary border-bottom]
|
|
357
376
|
@show_gravatar = true
|
|
377
|
+
@show_footer = true
|
|
358
378
|
@navigation_static_links = {}
|
|
359
379
|
@navigation_static_label = nil
|
|
360
380
|
@composite_keys_serializer = RailsAdminNext::Support::CompositeKeysSerializer
|
|
@@ -369,7 +389,7 @@ module RailsAdminNext
|
|
|
369
389
|
#
|
|
370
390
|
# @see RailsAdminNext::Config.registry
|
|
371
391
|
def reset_model(model)
|
|
372
|
-
key = model.is_a?(Class) ? model.name.to_sym : model.to_sym
|
|
392
|
+
key = model.is_a?(Class) ? model.name.to_sym : model.to_s.delete_prefix("::").to_sym
|
|
373
393
|
@registry.delete(key)
|
|
374
394
|
end
|
|
375
395
|
|
|
@@ -395,7 +415,7 @@ module RailsAdminNext
|
|
|
395
415
|
private
|
|
396
416
|
|
|
397
417
|
def viable_models
|
|
398
|
-
included_models.collect(
|
|
418
|
+
included_models.collect { it.to_s.delete_prefix("::") }.presence || begin
|
|
399
419
|
@@system_models ||= # memoization for tests
|
|
400
420
|
([Rails.application] + Rails::Engine.subclasses.collect(&:instance)).flat_map do |app|
|
|
401
421
|
(app.paths["app/models"].to_a + app.config.eager_load_paths).collect do |load_path|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAdminNext
|
|
4
|
+
module Extensions
|
|
5
|
+
module ActionPolicy
|
|
6
|
+
class AuthorizationAdapter
|
|
7
|
+
# ActionPolicy is referenced only inside methods, so hosts without the gem still boot.
|
|
8
|
+
def self.setup
|
|
9
|
+
include ::ActionPolicy::Behaviour
|
|
10
|
+
|
|
11
|
+
authorize :user, through: -> { @controller._current_user }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(controller)
|
|
15
|
+
@controller = controller
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authorize(action, abstract_model = nil, model_object = nil)
|
|
19
|
+
return unless action
|
|
20
|
+
|
|
21
|
+
record = model_object || abstract_model&.model
|
|
22
|
+
authorize!(record, to: rule_for(action), with: policy_class_for(record))
|
|
23
|
+
@controller.skip_verify_authorized! if @controller.respond_to?(:skip_verify_authorized!)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def authorized?(action, abstract_model = nil, model_object = nil)
|
|
27
|
+
return unless action
|
|
28
|
+
|
|
29
|
+
record = model_object || abstract_model&.model
|
|
30
|
+
allowed_to?(rule_for(action), record, with: policy_class_for(record))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def query(_action, abstract_model)
|
|
34
|
+
@controller.skip_verify_authorized_scoped! if @controller.respond_to?(:skip_verify_authorized_scoped!)
|
|
35
|
+
model = abstract_model.model
|
|
36
|
+
authorized_scope(model.all, type: :active_record_relation, with: ::ActionPolicy.lookup(model))
|
|
37
|
+
rescue ::ActionPolicy::NotFound, ::ActionPolicy::UnknownScopeType, ::ActionPolicy::UnknownNamedScope
|
|
38
|
+
abstract_model.model.none
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def attributes_for(action, abstract_model)
|
|
42
|
+
record = abstract_model&.model
|
|
43
|
+
policy_for(record:, with: policy_class_for(record)).try(:attributes_for, action) || {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def policy_class_for(record)
|
|
49
|
+
::ActionPolicy.lookup(record, default: ::ApplicationPolicy)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A Symbol, because ActionPolicy keys its rule aliases (new? -> create?) by Symbol and a
|
|
53
|
+
# String would silently skip them.
|
|
54
|
+
def rule_for(action)
|
|
55
|
+
action.to_s.end_with?("?") ? action.to_sym : :"#{action}?"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -143,7 +143,7 @@ module RailsAdminNext
|
|
|
143
143
|
|
|
144
144
|
versions = object.nil? ? versions_for_model(model) : object.public_send(model.model.versions_association_name)
|
|
145
145
|
versions = versions.where("event LIKE ?", "%#{query}%") if query.present?
|
|
146
|
-
versions = versions.
|
|
146
|
+
versions = versions.reorder(sort)
|
|
147
147
|
|
|
148
148
|
paginate_versions(versions, all, (page.presence || 1).to_i, per_page.to_i)
|
|
149
149
|
end
|
|
@@ -46,7 +46,7 @@ module RailsAdminNext
|
|
|
46
46
|
def query(_action, abstract_model)
|
|
47
47
|
@controller.send(:policy_scope, abstract_model.model.all)
|
|
48
48
|
rescue ::Pundit::NotDefinedError
|
|
49
|
-
abstract_model.model.none
|
|
49
|
+
host_policy_scope? ? abstract_model.model.none : model_scope(abstract_model.model)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# This is called in the new/create actions to determine the initial attributes for new
|
|
@@ -65,6 +65,22 @@ module RailsAdminNext
|
|
|
65
65
|
::ApplicationPolicy.new(@controller.send(:pundit_user), record)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# action_policy defines ActiveRecord::Relation#policy_class as nil, which hides every
|
|
69
|
+
# Scope from Pundit's relation lookup when both gems are bundled; the model still finds it.
|
|
70
|
+
# A host override of policy_scope (e.g. namespacing) chose its own Scope, so it is never
|
|
71
|
+
# swapped for the model's.
|
|
72
|
+
def host_policy_scope?
|
|
73
|
+
@controller.method(:policy_scope).owner != (defined?(::Pundit::Authorization) ? ::Pundit::Authorization : ::Pundit)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def model_scope(model)
|
|
77
|
+
scope_class = ::Pundit::PolicyFinder.new(model).scope
|
|
78
|
+
return model.none unless scope_class
|
|
79
|
+
|
|
80
|
+
@controller.instance_variable_set(:@_pundit_policy_scoped, true)
|
|
81
|
+
scope_class.new(@controller.send(:pundit_user), model.all).resolve
|
|
82
|
+
end
|
|
83
|
+
|
|
68
84
|
def action_for_pundit(action)
|
|
69
85
|
(action[-1, 1] == "?") ? action : "#{action}?"
|
|
70
86
|
end
|
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
module RailsAdminNext
|
|
4
4
|
module Support
|
|
5
5
|
module CompositeKeysSerializer
|
|
6
|
+
# "_" joins the keys, so it is escaped as "~" plus its hex byte. "~" is escaped too so the
|
|
7
|
+
# escape cannot be forged, and both are URL-unreserved. Every other value serializes as-is,
|
|
8
|
+
# keeping existing links valid.
|
|
9
|
+
ESCAPED = /[_~]/
|
|
10
|
+
ESCAPE = /~(5F|7E)/
|
|
11
|
+
|
|
6
12
|
def self.serialize(keys)
|
|
7
|
-
keys.map { |key| key&.to_s&.gsub("
|
|
13
|
+
keys.map { |key| key&.to_s&.gsub(ESCAPED) { format("~%02X", it.ord) } }.join("_")
|
|
8
14
|
end
|
|
9
15
|
|
|
10
16
|
def self.deserialize(string)
|
|
11
|
-
string.split("_").map { |key| key
|
|
17
|
+
string.split("_").map { |key| key.gsub(ESCAPE) { $1.hex.chr } }
|
|
12
18
|
end
|
|
13
19
|
end
|
|
14
20
|
end
|
|
@@ -4,14 +4,17 @@ require "csv"
|
|
|
4
4
|
|
|
5
5
|
module RailsAdminNext
|
|
6
6
|
class CSVConverter
|
|
7
|
+
BATCH_SIZE = 1000
|
|
8
|
+
|
|
7
9
|
def initialize(objects = [], schema = nil)
|
|
8
10
|
@fields = []
|
|
9
11
|
@associations = []
|
|
10
12
|
schema ||= {}
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
@objects = objects
|
|
15
|
+
@model = objects.is_a?(ActiveRecord::Relation) ? objects.klass : objects.first&.class
|
|
16
|
+
return unless @model
|
|
13
17
|
|
|
14
|
-
@model = objects.dup.first.class
|
|
15
18
|
@abstract_model = RailsAdminNext::AbstractModel.new(@model)
|
|
16
19
|
@model_config = @abstract_model.config
|
|
17
20
|
@methods = [(schema[:only] || []) + (schema[:methods] || [])].flatten.compact
|
|
@@ -63,17 +66,54 @@ module RailsAdminNext
|
|
|
63
66
|
|
|
64
67
|
def generate_csv_string(options)
|
|
65
68
|
generator_options = (options[:generator] || {}).symbolize_keys.delete_if { |_, value| value.blank? }
|
|
66
|
-
method = @objects.respond_to?(:find_each) ? :find_each : :each
|
|
67
|
-
|
|
68
69
|
CSV.generate(**generator_options) do |csv|
|
|
69
70
|
csv << generate_csv_header unless options[:skip_header]
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
each_object do |object|
|
|
72
73
|
csv << generate_csv_row(object)
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
end
|
|
76
77
|
|
|
78
|
+
def each_object(&)
|
|
79
|
+
return @objects.each(&) unless @objects.is_a?(ActiveRecord::Relation)
|
|
80
|
+
# Batching sets its own limit and offset, which would drop the relation's.
|
|
81
|
+
return @objects.each(&) if @objects.limit_value || @objects.offset_value
|
|
82
|
+
|
|
83
|
+
if (direction = primary_key_direction)
|
|
84
|
+
@objects.unscope(:order).find_each(order: direction, batch_size: BATCH_SIZE, &)
|
|
85
|
+
else
|
|
86
|
+
each_in_offset_batches(&)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# sort_scope writes the list sort as a single "<table>.<column> <direction>" string.
|
|
91
|
+
def primary_key_direction
|
|
92
|
+
primary_key = @model.primary_key
|
|
93
|
+
return unless primary_key.is_a?(String)
|
|
94
|
+
|
|
95
|
+
orders = @objects.order_values
|
|
96
|
+
return :asc if orders.empty?
|
|
97
|
+
return unless orders.one? && orders.first.is_a?(String)
|
|
98
|
+
|
|
99
|
+
table = Regexp.escape(@model.table_name)
|
|
100
|
+
column = Regexp.escape(primary_key)
|
|
101
|
+
orders.first[/\A(?:\W?#{table}\W?\.)?\W?#{column}\W? (asc|desc)\z/i, 1]&.downcase&.to_sym
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The primary-key tie-break makes the order total, so rows that tie on the sort
|
|
105
|
+
# column cannot shift between batches. A short batch is not the last one: sorting
|
|
106
|
+
# on an eager-loaded has-many column returns fewer records than the limit.
|
|
107
|
+
def each_in_offset_batches(&)
|
|
108
|
+
ordered = @objects.order(Array(@model.primary_key).index_with(:asc))
|
|
109
|
+
0.step(by: BATCH_SIZE) do |offset|
|
|
110
|
+
batch = ordered.limit(BATCH_SIZE).offset(offset).to_a
|
|
111
|
+
break if batch.empty?
|
|
112
|
+
|
|
113
|
+
batch.each(&)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
77
117
|
def generate_csv_header
|
|
78
118
|
@fields.collect do |field|
|
|
79
119
|
::I18n.t("admin.export.csv.header_for_root_methods", name: field.label, model: @abstract_model.pretty_name)
|
|
@@ -5,9 +5,9 @@ module RailsAdminNext
|
|
|
5
5
|
# Releases are cut by publishing a Release-Drafter draft: the gem-push workflow stamps
|
|
6
6
|
# the tag's version over these constants at build time, so the in-repo values are only
|
|
7
7
|
# the floor a local `gem build` produces.
|
|
8
|
-
MAJOR =
|
|
9
|
-
MINOR =
|
|
10
|
-
PATCH =
|
|
8
|
+
MAJOR = 2
|
|
9
|
+
MINOR = 0
|
|
10
|
+
PATCH = 0
|
|
11
11
|
PRE = nil
|
|
12
12
|
|
|
13
13
|
class << self
|
data/lib/rails_admin_next.rb
CHANGED
|
@@ -6,8 +6,8 @@ require "rails_admin_next/config"
|
|
|
6
6
|
require "rails_admin_next/config/const_load_suppressor"
|
|
7
7
|
require "rails_admin_next/icons"
|
|
8
8
|
require "rails_admin_next/extension"
|
|
9
|
-
require "rails_admin_next/extensions/cancancan"
|
|
10
9
|
require "rails_admin_next/extensions/pundit"
|
|
10
|
+
require "rails_admin_next/extensions/action_policy"
|
|
11
11
|
require "rails_admin_next/extensions/paper_trail"
|
|
12
12
|
require "rails_admin_next/support/csv_converter"
|
|
13
13
|
require "rails_admin_next/support/hash_helper"
|
|
@@ -49,25 +49,13 @@ module RailsAdminNext
|
|
|
49
49
|
@deprecator ||= ActiveSupport::Deprecation.new(RailsAdminNext::Version.to_s, "RailsAdminNext")
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
begin
|
|
56
|
-
require "safe_yaml/load"
|
|
57
|
-
def self.yaml_load(yaml)
|
|
58
|
-
SafeYAML.load(yaml)
|
|
59
|
-
end
|
|
60
|
-
rescue LoadError
|
|
61
|
-
if YAML.respond_to?(:safe_load)
|
|
62
|
-
def self.yaml_load(yaml)
|
|
63
|
-
YAML.safe_load(yaml)
|
|
64
|
-
end
|
|
65
|
-
else
|
|
66
|
-
raise LoadError.new "Safe-loading of YAML is not available. Please install 'safe_yaml' or install Psych 2.0+"
|
|
67
|
-
end
|
|
52
|
+
def self.yaml_load(yaml)
|
|
53
|
+
deprecator.warn("RailsAdminNext.yaml_load is deprecated and will be removed. Call YAML.safe_load directly.")
|
|
54
|
+
YAML.safe_load(yaml)
|
|
68
55
|
end
|
|
69
56
|
|
|
70
57
|
def self.yaml_dump(object)
|
|
58
|
+
deprecator.warn("RailsAdminNext.yaml_dump is deprecated and will be removed. Call YAML.dump directly.")
|
|
71
59
|
YAML.dump(object)
|
|
72
60
|
end
|
|
73
61
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rails-admin-next
|
|
3
|
-
description: Configure or extend the rails_admin_next admin in a host Rails app (models, fields, custom actions and field types,
|
|
3
|
+
description: Configure or extend the rails_admin_next admin in a host Rails app (models, fields, custom actions and field types, Pundit, ActionPolicy or custom-adapter authorization, PaperTrail history, theming, CSP). Use instead of upstream RailsAdmin knowledge, which is wrong for this fork.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# rails_admin_next
|
|
@@ -35,7 +35,7 @@ Renamed (write the new form):
|
|
|
35
35
|
|
|
36
36
|
Not renamed (leave as is):
|
|
37
37
|
|
|
38
|
-
- The authorization subject is still `:rails_admin`. CanCanCan
|
|
38
|
+
- The authorization subject is still `:rails_admin`. A custom adapter that keeps CanCanCan grants `can :access, :rails_admin`. Never rewrite it to `:rails_admin_next`, which fails authorization.
|
|
39
39
|
- Action authorization keys (`:dashboard`, `:index`, `:show`, `:new`, `:edit`, `:export`, `:destroy`, `:history`, `:show_in_app`).
|
|
40
40
|
- The `admin.*` I18n namespace.
|
|
41
41
|
|
|
@@ -75,9 +75,10 @@ Icons take logical names, never Font Awesome classes: `link_icon :show_in_app` o
|
|
|
75
75
|
|
|
76
76
|
## Authorization and history
|
|
77
77
|
|
|
78
|
-
- `
|
|
78
|
+
- Pundit and ActionPolicy are the built-in adapters. `RailsAdminNext.add_extension(key, mod, authorization: true)` registers a custom one before the config block names its key; copy `<gem>/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb` as the template. An unregistered key raises `ArgumentError` at boot.
|
|
79
79
|
- `config.authorize_with :pundit` calls the record's policy with `<action>?` methods (`index?`, `edit?`, `dashboard?`) and scopes lists with `policy_scope`.
|
|
80
|
-
- `config.authorize_with
|
|
80
|
+
- `config.authorize_with :action_policy` checks the same `<action>?` rules with `allowed_to?` and scopes lists with the policy's default `relation_scope`. `new?` resolves to `create?` and undefined rules to `manage?`. A model with no policy is checked against `ApplicationPolicy`, and one with no default relation scope lists nothing.
|
|
81
|
+
- `config.authorize_with do ... end` runs a custom block as a before filter. It gates admin entry only: no per-record checks or list scoping.
|
|
81
82
|
- `config.authenticate_with do ... end` and `config.current_user_method do ... end` wire in the app's authentication, such as Devise or Warden.
|
|
82
83
|
- `config.audit_with :paper_trail` turns on the history action. Read the "History / audit table" section of `<gem>/docs/UPGRADING.md` first.
|
|
83
84
|
|
|
@@ -7,7 +7,7 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
7
7
|
//
|
|
8
8
|
// - add: clones a blank subform from the <template>. For a has_many
|
|
9
9
|
// association the template carries a child-index placeholder
|
|
10
|
-
// (e.g. "
|
|
10
|
+
// (e.g. "new_1_nested_field_tests") which is swapped for a unique value so
|
|
11
11
|
// each new row submits under its own index; has_one/belongs_to subforms
|
|
12
12
|
// have no placeholder. Focus then moves to the first visible field.
|
|
13
13
|
// - remove on an unsaved subform deletes it from the DOM.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_admin_next
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel López Prat
|
|
@@ -285,8 +285,8 @@ files:
|
|
|
285
285
|
- lib/rails_admin_next/config/sections/update.rb
|
|
286
286
|
- lib/rails_admin_next/engine.rb
|
|
287
287
|
- lib/rails_admin_next/extension.rb
|
|
288
|
-
- lib/rails_admin_next/extensions/
|
|
289
|
-
- lib/rails_admin_next/extensions/
|
|
288
|
+
- lib/rails_admin_next/extensions/action_policy.rb
|
|
289
|
+
- lib/rails_admin_next/extensions/action_policy/authorization_adapter.rb
|
|
290
290
|
- lib/rails_admin_next/extensions/controller_extension.rb
|
|
291
291
|
- lib/rails_admin_next/extensions/paper_trail.rb
|
|
292
292
|
- lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RailsAdminNext
|
|
4
|
-
module Extensions
|
|
5
|
-
module CanCanCan
|
|
6
|
-
# This adapter is for the CanCanCan[https://github.com/CanCanCommunity/cancancan] authorization library.
|
|
7
|
-
class AuthorizationAdapter
|
|
8
|
-
module ControllerExtension
|
|
9
|
-
def current_ability
|
|
10
|
-
# use _current_user instead of default current_user so it works with
|
|
11
|
-
# whatever current user method is defined with RailsAdminNext
|
|
12
|
-
@current_ability ||= ability_class.new(_current_user)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
include RailsAdminNext::Config::Configurable
|
|
17
|
-
|
|
18
|
-
def self.setup
|
|
19
|
-
RailsAdminNext::Extensions::ControllerExtension.include ControllerExtension
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# See the +authorize_with+ config method for where the initialization happens.
|
|
23
|
-
def initialize(controller, ability = nil, &block)
|
|
24
|
-
@controller = controller
|
|
25
|
-
ability_class { ability } if ability
|
|
26
|
-
instance_eval(&block) if block
|
|
27
|
-
|
|
28
|
-
adapter = self
|
|
29
|
-
ControllerExtension.define_method(:ability_class) do
|
|
30
|
-
adapter.ability_class
|
|
31
|
-
end
|
|
32
|
-
@controller.current_ability.authorize! :access, :rails_admin
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
register_instance_option :ability_class do
|
|
36
|
-
Ability
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# This method is called in every controller action and should raise an exception
|
|
40
|
-
# when the authorization fails. The first argument is the name of the controller
|
|
41
|
-
# action as a symbol (:create, :bulk_delete, etc.). The second argument is the
|
|
42
|
-
# AbstractModel instance that applies. The third argument is the actual model
|
|
43
|
-
# instance if it is available.
|
|
44
|
-
def authorize(action, abstract_model = nil, model_object = nil)
|
|
45
|
-
return unless action
|
|
46
|
-
|
|
47
|
-
action, subject = resolve_action_and_subject(action, abstract_model, model_object)
|
|
48
|
-
@controller.current_ability.authorize!(action, subject)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# This method is called primarily from the view to determine whether the given user
|
|
52
|
-
# has access to perform the action on a given model. It should return true when authorized.
|
|
53
|
-
# This takes the same arguments as +authorize+. The difference is that this will
|
|
54
|
-
# return a boolean whereas +authorize+ will raise an exception when not authorized.
|
|
55
|
-
def authorized?(action, abstract_model = nil, model_object = nil)
|
|
56
|
-
return unless action
|
|
57
|
-
|
|
58
|
-
action, subject = resolve_action_and_subject(action, abstract_model, model_object)
|
|
59
|
-
@controller.current_ability.can?(action, subject)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# This is called when needing to scope a database query. It is called within the list
|
|
63
|
-
# and bulk_delete/destroy actions and should return a scope which limits the records
|
|
64
|
-
# to those which the user can perform the given action on.
|
|
65
|
-
def query(action, abstract_model)
|
|
66
|
-
abstract_model.model.accessible_by(@controller.current_ability, action)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# This is called in the new/create actions to determine the initial attributes for new
|
|
70
|
-
# records. It should return a hash of attributes which match what the user
|
|
71
|
-
# is authorized to create.
|
|
72
|
-
def attributes_for(action, abstract_model)
|
|
73
|
-
@controller.current_ability.attributes_for(action, abstract_model&.model)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private
|
|
77
|
-
|
|
78
|
-
def resolve_action_and_subject(action, abstract_model, model_object)
|
|
79
|
-
subject = model_object || abstract_model&.model
|
|
80
|
-
if subject
|
|
81
|
-
[action, subject]
|
|
82
|
-
else
|
|
83
|
-
# For :dashboard compatibility
|
|
84
|
-
[:read, action]
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|