rails_admin_next 2.1.0 → 2.2.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/docs/UPGRADING.md +37 -9
- data/lib/rails_admin_next/config/actions/bulk_delete.rb +5 -1
- data/lib/rails_admin_next/extensions/action_policy/authorization_adapter.rb +12 -3
- data/lib/rails_admin_next/extensions/model_lineage.rb +14 -0
- data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +15 -4
- data/lib/rails_admin_next/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 373f306f26c44449a2adeabf854452a3592fdef08eb87ffb31c818f7ce662ddb
|
|
4
|
+
data.tar.gz: 435fbec5b7817f3f1d7b53eacf233f04208c27de4a87e5eb15fdd6585f395328
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea6a58ab5f4276088b29bde851ef2802316f370a208d50c0619dc3c6a2218e981f690b959f1776d8b6d74687a703595887f798b1ef11866b398fd4ec5e4d90e0
|
|
7
|
+
data.tar.gz: 5e43c1a785d4bbc4bcfa06aea22a24989d2e5832ad7de1cef45e388f7a2f002823da4f50421dd1fbe9d4121b0324f05409194ea6f7f75f0751881c713253ca7e
|
data/docs/UPGRADING.md
CHANGED
|
@@ -30,7 +30,8 @@ end
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
- **`ApplicationPolicy`.** The adapter falls back to `::ApplicationPolicy` for the dashboard
|
|
33
|
-
(record `nil`) and for any model without its own
|
|
33
|
+
(record `nil`) and for any model without a policy of its own or a parent model's (see
|
|
34
|
+
[STI subclasses use their parent model's policy](#sti-subclasses-use-their-parent-models-policy)).
|
|
34
35
|
- **Action predicates.** The adapter calls each action's authorization key with a `?`
|
|
35
36
|
suffix: `dashboard?`, `index?`, `show?`, `new?`, `edit?`, `export?`, `destroy?`,
|
|
36
37
|
`history?` and `show_in_app?`. Saving a new record also calls `create?`, and saving an
|
|
@@ -49,10 +50,10 @@ end
|
|
|
49
50
|
Three differences from CanCanCan to check before switching:
|
|
50
51
|
|
|
51
52
|
- **One `Scope` bounds every list.** The adapter passes no action to the `Scope`, so the same
|
|
52
|
-
scope bounds index, export, bulk delete and the dashboard count. Bulk
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
scope bounds index, export, bulk delete and the dashboard count. Bulk delete also asks
|
|
54
|
+
`destroy?` of each selected record, but export does not. If an ability allowed export on
|
|
55
|
+
fewer records than read, resolve the `Scope` to the narrowest of them, or stay on a custom
|
|
56
|
+
adapter whose `query` honours the action.
|
|
56
57
|
- **Nothing checks `:access, :rails_admin`.** Admin access is decided by the same policies,
|
|
57
58
|
and the `ApplicationPolicy` fallback, that the rest of the app uses. If those admit
|
|
58
59
|
ordinary users, gate admin entry separately: in `config.authenticate_with`, or with
|
|
@@ -537,8 +538,8 @@ erroring.
|
|
|
537
538
|
## Pundit: a model with no policy Scope lists nothing
|
|
538
539
|
|
|
539
540
|
A model whose policy defines no `Scope` used to list every record. It now lists none: the
|
|
540
|
-
list, export, bulk delete and dashboard count all return an empty set
|
|
541
|
-
|
|
541
|
+
list, export, bulk delete and dashboard count all return an empty set, so "no Scope" can't
|
|
542
|
+
mean "everything".
|
|
542
543
|
|
|
543
544
|
This holds only when no `Scope` constant resolves. A policy that subclasses `ApplicationPolicy`
|
|
544
545
|
without its own `Scope` inherits `ApplicationPolicy::Scope`: Pundit's generated `resolve` there
|
|
@@ -576,9 +577,11 @@ change how a ported Pundit policy behaves:
|
|
|
576
577
|
|
|
577
578
|
- `new?` is an alias of `create?` unless the policy defines `new?` itself.
|
|
578
579
|
- A rule the policy does not define resolves to `manage?`, which is `false` on
|
|
579
|
-
`ActionPolicy::Base`, instead of raising.
|
|
580
|
+
`ActionPolicy::Base`, instead of raising. That includes a mistyped `authorization_key`,
|
|
581
|
+
so a policy whose `manage?` is true lets that action through.
|
|
580
582
|
|
|
581
|
-
A model with no policy, and the dashboard, are checked against
|
|
583
|
+
A model with no policy of its own or a parent model's, and the dashboard, are checked against
|
|
584
|
+
`ApplicationPolicy`. Lists,
|
|
582
585
|
export, bulk delete and dashboard counts use the policy's default `relation_scope`; a model
|
|
583
586
|
with no policy, no relation scope, or only named ones lists nothing. To prefill new records,
|
|
584
587
|
define `attributes_for(action)` on the policy, as with Pundit.
|
|
@@ -713,6 +716,15 @@ own model, its `additional_scope` defaults to `nil`, and it no longer reads the
|
|
|
713
716
|
`associated_collection` picker params. Custom actions that listed picker records through
|
|
714
717
|
it should call `association_entries(model_config, collection_scope)` instead.
|
|
715
718
|
|
|
719
|
+
## Bulk delete checks `destroy?` on each record
|
|
720
|
+
|
|
721
|
+
Bulk delete now asks the policy's `destroy?` (or the rule named by the action's
|
|
722
|
+
`authorization_key`) of every selected record, as the single delete does, with both Pundit
|
|
723
|
+
and ActionPolicy. A record it refuses is left out of the confirmation
|
|
724
|
+
page, is not destroyed, and counts in the "failed to be deleted" message. Before, bulk
|
|
725
|
+
delete honoured only the scope, so a `destroy?` rule that reads the record, such as
|
|
726
|
+
`!record.locked?`, stopped the single delete but not the bulk one.
|
|
727
|
+
|
|
716
728
|
## `associated_collection_scope` runs after the list sort
|
|
717
729
|
|
|
718
730
|
Association pickers apply `associated_collection_scope` after the picked model's list sort,
|
|
@@ -737,6 +749,22 @@ Every admin page now ends with a small, muted "Made in Tokyo with ❤️ and
|
|
|
737
749
|
content. It is not shown inside modals. To hide it, set `config.show_footer = false` in your
|
|
738
750
|
initializer.
|
|
739
751
|
|
|
752
|
+
## STI subclasses use their parent model's policy
|
|
753
|
+
|
|
754
|
+
Both the Pundit and the ActionPolicy adapter used to check an STI subclass with no policy of its
|
|
755
|
+
own against `ApplicationPolicy`, and to list none of its records. Such a subclass now takes the
|
|
756
|
+
nearest parent model's policy, for its rules and its scope alike, before `ApplicationPolicy`:
|
|
757
|
+
a `Hardball < Ball` with no `HardballPolicy` uses `BallPolicy`. A subclass that defines its own
|
|
758
|
+
policy still uses it, and lists nothing if that policy has no scope.
|
|
759
|
+
|
|
760
|
+
If `ApplicationPolicy` granted a subclass what its parent's policy denies, it is now denied. If
|
|
761
|
+
`ApplicationPolicy` denied a subclass what its parent's policy grants, it is now granted, and the
|
|
762
|
+
subclass lists what the parent's scope returns. Either way, give the subclass its own policy to
|
|
763
|
+
keep the old access.
|
|
764
|
+
|
|
765
|
+
The walk follows Ruby class inheritance, not the `type` column: a concrete subclass of a model
|
|
766
|
+
takes that model's policy even when it is not STI.
|
|
767
|
+
|
|
740
768
|
## Removed: options deprecated before RailsAdmin 3.0
|
|
741
769
|
|
|
742
770
|
These six were deprecated upstream between 2019-07 and 2022-01 and had no effect for
|
|
@@ -16,8 +16,10 @@ module RailsAdminNext
|
|
|
16
16
|
|
|
17
17
|
register_instance_option :controller do
|
|
18
18
|
proc do
|
|
19
|
+
destroyable = ->(object) { @authorization_adapter.nil? || @authorization_adapter.authorized?(@action.authorization_key, @abstract_model, object) }
|
|
20
|
+
|
|
19
21
|
if request.post? # BULK DELETE
|
|
20
|
-
@objects = list_entries(@model_config, :destroy)
|
|
22
|
+
@objects = list_entries(@model_config, :destroy).select(&destroyable)
|
|
21
23
|
|
|
22
24
|
if @objects.blank?
|
|
23
25
|
flash[:error] = t("admin.flash.error", name: pluralize(0, @model_config.label, plural: @model_config.label_plural), action: t("admin.actions.delete.done"))
|
|
@@ -37,6 +39,8 @@ module RailsAdminNext
|
|
|
37
39
|
# Rescue per record: dependent: :restrict_with_exception raises, and an
|
|
38
40
|
# escaping raise would abandon every record behind it in the batch.
|
|
39
41
|
destroyed, not_destroyed = Array.wrap(@objects).partition do |object|
|
|
42
|
+
next false unless destroyable.call(object)
|
|
43
|
+
|
|
40
44
|
object.destroy
|
|
41
45
|
object.destroyed?
|
|
42
46
|
rescue ActiveRecord::DeleteRestrictionError
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rails_admin_next/extensions/model_lineage"
|
|
4
|
+
|
|
3
5
|
module RailsAdminNext
|
|
4
6
|
module Extensions
|
|
5
7
|
module ActionPolicy
|
|
@@ -44,8 +46,11 @@ module RailsAdminNext
|
|
|
44
46
|
def query(_action, abstract_model)
|
|
45
47
|
@controller.skip_verify_authorized_scoped! if @controller.respond_to?(:skip_verify_authorized_scoped!)
|
|
46
48
|
model = abstract_model.model
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
policy_class = model_policy_class(model)
|
|
50
|
+
return model.none unless policy_class
|
|
51
|
+
|
|
52
|
+
authorized_scope(model.all, type: :active_record_relation, with: policy_class)
|
|
53
|
+
rescue ::ActionPolicy::UnknownScopeType, ::ActionPolicy::UnknownNamedScope
|
|
49
54
|
abstract_model.model.none
|
|
50
55
|
end
|
|
51
56
|
|
|
@@ -61,7 +66,11 @@ module RailsAdminNext
|
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
def policy_class_for(record)
|
|
64
|
-
|
|
69
|
+
model_policy_class(record) || ::ApplicationPolicy
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def model_policy_class(record)
|
|
73
|
+
[record, *ModelLineage.superclasses(record)].lazy.filter_map { ::ActionPolicy.lookup(it, allow_nil: true) }.first
|
|
65
74
|
end
|
|
66
75
|
|
|
67
76
|
# A Symbol, because ActionPolicy keys its rule aliases (new? -> create?) by Symbol and a
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAdminNext
|
|
4
|
+
module Extensions
|
|
5
|
+
# Pundit and ActionPolicy look a policy up by the record's own class name only, so without
|
|
6
|
+
# walking these an STI subclass with no policy skips its parent model's for ApplicationPolicy.
|
|
7
|
+
module ModelLineage
|
|
8
|
+
def self.superclasses(record)
|
|
9
|
+
model = record.is_a?(Class) ? record : record.class
|
|
10
|
+
model.ancestors.grep(Class).drop(1).take_while { it < ::ActiveRecord::Base && !it.abstract_class? }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rails_admin_next/extensions/model_lineage"
|
|
4
|
+
|
|
3
5
|
module RailsAdminNext
|
|
4
6
|
module Extensions
|
|
5
7
|
module Pundit
|
|
@@ -45,8 +47,7 @@ module RailsAdminNext
|
|
|
45
47
|
# This is called when needing to scope a database query. It is called within the list
|
|
46
48
|
# and bulk_delete/destroy actions and should return a scope which limits the records
|
|
47
49
|
# to those which the user can perform the given action on. A model with no policy
|
|
48
|
-
# Scope gets no records
|
|
49
|
-
# per-record check, so a forgotten Scope must not mean "everything".
|
|
50
|
+
# Scope gets no records, so a forgotten Scope must not mean "everything".
|
|
50
51
|
def query(_action, abstract_model)
|
|
51
52
|
@controller.send(:policy_scope, abstract_model.model.all)
|
|
52
53
|
rescue ::Pundit::NotDefinedError
|
|
@@ -66,7 +67,16 @@ module RailsAdminNext
|
|
|
66
67
|
def policy(record)
|
|
67
68
|
@controller.send(:policy, record)
|
|
68
69
|
rescue ::Pundit::NotDefinedError
|
|
69
|
-
|
|
70
|
+
policy_class = ModelLineage.superclasses(record).lazy.filter_map { host_policy(it)&.class }.first
|
|
71
|
+
(policy_class || ::ApplicationPolicy).new(@controller.send(:pundit_user), record)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Through the controller, so a host override of policy (e.g. namespacing) also picks the
|
|
75
|
+
# parent model's policy.
|
|
76
|
+
def host_policy(model)
|
|
77
|
+
@controller.send(:policy, model)
|
|
78
|
+
rescue ::Pundit::NotDefinedError
|
|
79
|
+
nil
|
|
70
80
|
end
|
|
71
81
|
|
|
72
82
|
# action_policy defines ActiveRecord::Relation#policy_class as nil, which hides every
|
|
@@ -78,7 +88,8 @@ module RailsAdminNext
|
|
|
78
88
|
end
|
|
79
89
|
|
|
80
90
|
def model_scope(model)
|
|
81
|
-
|
|
91
|
+
finder = [model, *ModelLineage.superclasses(model)].lazy.map { ::Pundit::PolicyFinder.new(it) }.find(&:policy)
|
|
92
|
+
scope_class = finder&.scope
|
|
82
93
|
return model.none unless scope_class
|
|
83
94
|
|
|
84
95
|
@controller.instance_variable_set(:@_pundit_policy_scoped, true)
|
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: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel López Prat
|
|
@@ -288,6 +288,7 @@ files:
|
|
|
288
288
|
- lib/rails_admin_next/extensions/action_policy.rb
|
|
289
289
|
- lib/rails_admin_next/extensions/action_policy/authorization_adapter.rb
|
|
290
290
|
- lib/rails_admin_next/extensions/controller_extension.rb
|
|
291
|
+
- lib/rails_admin_next/extensions/model_lineage.rb
|
|
291
292
|
- lib/rails_admin_next/extensions/paper_trail.rb
|
|
292
293
|
- lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb
|
|
293
294
|
- lib/rails_admin_next/extensions/pundit.rb
|