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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 821ddc2c14943ce137265d8c99d9784492cde1b2041dab27a1b5651795ce3cd4
4
- data.tar.gz: f3c9d9906d117bb55e0eb4c2fcfc585d129b98c1bb2b32689fdd2a734f656d90
3
+ metadata.gz: 373f306f26c44449a2adeabf854452a3592fdef08eb87ffb31c818f7ce662ddb
4
+ data.tar.gz: 435fbec5b7817f3f1d7b53eacf233f04208c27de4a87e5eb15fdd6585f395328
5
5
  SHA512:
6
- metadata.gz: 993ba29f1ae5ce41e276c0637660c791c0f3a6a47379082143b15079052c4f43ebc7a24a0d00f7433b4c0c1c37359154e0ef4ae3bf1c5a95340ac78472fca376
7
- data.tar.gz: fa8ef20e12e1a670877e0d8f8719d87967e0398a11063d30e58a70a5bdff77984dc04e70695b7f12dfd7f632e04606388e4fd08c027b4e57dcc1121f2eec458c
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 policy.
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 destroy deletes every
53
- selected record that `Scope` returns, with no per-record `destroy?` check. If an ability
54
- allowed destroy or export on fewer records than read, resolve the `Scope` to the narrowest
55
- of them, or stay on a custom adapter whose `query` honours the action.
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. Bulk delete deletes
541
- whatever the scope returns, with no per-record check, so "no Scope" can't mean "everything".
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 `ApplicationPolicy`. Lists,
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
- authorized_scope(model.all, type: :active_record_relation, with: ::ActionPolicy.lookup(model))
48
- rescue ::ActionPolicy::NotFound, ::ActionPolicy::UnknownScopeType, ::ActionPolicy::UnknownNamedScope
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
- ::ActionPolicy.lookup(record, default: ::ApplicationPolicy)
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: bulk_delete deletes whatever this returns without a
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
- ::ApplicationPolicy.new(@controller.send(:pundit_user), record)
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
- scope_class = ::Pundit::PolicyFinder.new(model).scope
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)
@@ -6,7 +6,7 @@ module RailsAdminNext
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
8
  MAJOR = 2
9
- MINOR = 1
9
+ MINOR = 2
10
10
  PATCH = 0
11
11
  PRE = nil
12
12
 
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.1.0
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