rails_admin_next 1.3.0 → 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 +8 -0
- data/app/views/layouts/rails_admin_next/application.html.erb +3 -0
- data/docs/UPGRADING.md +137 -9
- data/docs/security.md +4 -0
- data/lib/generators/rails_admin_next/templates/initializer.erb +7 -3
- data/lib/rails_admin_next/config/actions/base.rb +0 -1
- data/lib/rails_admin_next/config.rb +25 -5
- 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/pundit/authorization_adapter.rb +17 -1
- data/lib/rails_admin_next/version.rb +2 -2
- data/lib/rails_admin_next.rb +1 -1
- data/skills/rails-admin-next/SKILL.md +5 -4
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85cf357bfb48a568e28ee9d031d96f9d9d96546564579a17273d06ddd1643a45
|
|
4
|
+
data.tar.gz: a3850dcf1e9f5fb4bf6797a1175be2112d6f5ecc1d9f4588dfc561a412ce9072
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 950489886f285808c1d8688a3099361d63bc0f2bead9b15b3b752c3de41e696a48232f63bd6bdce8ec6835a9266b35f3b13a1faebc10572a0de1daf05b87a61b
|
|
7
|
+
data.tar.gz: 6dcc9aa9063071f2008888dff5c1c4e43042c6fd3026ce6c52bdd5844080f0276cbb719241427c84254d95d2b05160cf066aaf699ddd14ac03c5c9204a5d791d
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -33,7 +33,7 @@ multi-bundler, jQuery-era machinery. RailsAdminNext keeps the admin and drops th
|
|
|
33
33
|
- **🧩 Turbo + Stimulus** — the jQuery / jQuery-UI / Bootstrap-JS / `@rails/ujs` stack is gone, replaced by engine-shipped Stimulus controllers; styling is hand-maintained vanilla CSS.
|
|
34
34
|
- **🔒 Hardened & CDN-free** — a documented CSRF / clickjacking / mass-assignment / CSP posture ([SECURITY.md](SECURITY.md)) and **zero external CDN**, default or opt-in — every asset is self-hosted off a gem.
|
|
35
35
|
- **🎯 One modern target** — Ruby ≥ 4.0.9, Rails ~> 8.1, ActiveRecord only, latest Chrome + Safari. No Mongoid, no multi-bundler / multi-Rails matrix, no legacy-browser CSS.
|
|
36
|
-
- **↩️ Familiar** — `RailsAdminNext.config`, per-model `rails_admin_next do … end` blocks, and
|
|
36
|
+
- **↩️ Familiar** — `RailsAdminNext.config`, per-model `rails_admin_next do … end` blocks, and Pundit authorization all work as before; upgrading from RailsAdmin is a mechanical rename.
|
|
37
37
|
|
|
38
38
|
## Quick Start
|
|
39
39
|
|
|
@@ -113,12 +113,12 @@ always match the installed version.
|
|
|
113
113
|
- File uploads (ActiveStorage)
|
|
114
114
|
- Rich text fields (ActionText / Trix)
|
|
115
115
|
- Authentication (via [Devise][devise] or any other scheme)
|
|
116
|
-
- Authorization (via [
|
|
116
|
+
- Authorization (via [Pundit][pundit], [ActionPolicy][action_policy] or a custom adapter)
|
|
117
117
|
- User action history (via [PaperTrail][papertrail])
|
|
118
118
|
|
|
119
119
|
[devise]: https://github.com/heartcombo/devise
|
|
120
|
-
[cancancan]: https://github.com/CanCanCommunity/cancancan
|
|
121
120
|
[pundit]: https://github.com/varvet/pundit
|
|
121
|
+
[action_policy]: https://github.com/palkan/action_policy
|
|
122
122
|
[papertrail]: https://github.com/paper-trail-gem/paper_trail
|
|
123
123
|
|
|
124
124
|
## Configuration
|
|
@@ -143,8 +143,15 @@ RailsAdminNext.config do |config|
|
|
|
143
143
|
end
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
This is also where you wire up [Devise][devise], [
|
|
147
|
-
|
|
146
|
+
This is also where you wire up [Devise][devise], [Pundit][pundit], and [PaperTrail][papertrail].
|
|
147
|
+
|
|
148
|
+
Every admin page ends with a small "Made in Tokyo with ❤️ and 🤖" footer. To hide it:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
RailsAdminNext.config do |config|
|
|
152
|
+
config.show_footer = false
|
|
153
|
+
end
|
|
154
|
+
```
|
|
148
155
|
|
|
149
156
|
### Per model
|
|
150
157
|
|
|
@@ -170,19 +177,24 @@ The configuration DSL is unchanged from upstream RailsAdmin, so the upstream
|
|
|
170
177
|
|
|
171
178
|
### Authorization keys are stable
|
|
172
179
|
|
|
173
|
-
The authorization contract is held stable, so existing
|
|
174
|
-
|
|
175
|
-
`:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
```ruby
|
|
180
|
-
# Still grants access to the admin:
|
|
181
|
-
can :access, :rails_admin
|
|
182
|
-
```
|
|
180
|
+
The authorization contract is held stable, so existing Pundit policies keep working
|
|
181
|
+
**without edits**: every action's authorization key (`:dashboard`, `:index`, `:show`,
|
|
182
|
+
`:new`, `:edit`, `:export`, `:destroy`, `:history`, `:show_in_app`) is unchanged, and the
|
|
183
|
+
Pundit adapter calls it with a `?` suffix (`index?`, `edit?`). Pundit and ActionPolicy are
|
|
184
|
+
the built-in adapters; any other authorization library plugs in as a custom adapter, which by convention
|
|
185
|
+
checks the `:access, :rails_admin` subject. See [docs/UPGRADING.md](docs/UPGRADING.md).
|
|
183
186
|
|
|
184
187
|
The `admin.*` I18n namespace is likewise unchanged.
|
|
185
188
|
|
|
189
|
+
### ActionPolicy
|
|
190
|
+
|
|
191
|
+
`config.authorize_with :action_policy` checks each action against the record's policy with
|
|
192
|
+
the same `<key>?` rules the Pundit adapter calls (`index?`, `show?`, `new?`, `edit?`,
|
|
193
|
+
`destroy?`, `export?`, `history?`, `show_in_app?`, `dashboard?`), and scopes lists, export
|
|
194
|
+
and bulk delete through the policy's default `relation_scope`. A model with no policy, or a
|
|
195
|
+
policy with no default relation scope, lists nothing. Bundle `action_policy` yourself; see
|
|
196
|
+
the "ActionPolicy adapter" section of [docs/UPGRADING.md](docs/UPGRADING.md).
|
|
197
|
+
|
|
186
198
|
## Frontend and assets
|
|
187
199
|
|
|
188
200
|
RailsAdminNext ships a single, zero-build asset path:
|
|
@@ -294,16 +306,16 @@ action methods**, where each admin operation is an object.
|
|
|
294
306
|
- **One controller, many action objects** — to change how e.g. _edit_ behaves, edit `lib/rails_admin_next/config/actions/edit.rb`, not the controller.
|
|
295
307
|
- **ORM facade** — nothing outside `AbstractModel` references ActiveRecord directly; the adapter seam is intact even though Mongoid was removed.
|
|
296
308
|
- **Lazy, layered config** — `config/initializers` blocks run before in-model `rails_admin_next do … end` blocks, so per-model config overrides the initializer defaults.
|
|
297
|
-
- **A deliberately frozen `rails_admin` surface** — the Ruby layer is `RailsAdminNext`, but the
|
|
309
|
+
- **A deliberately frozen `rails_admin` surface** — the Ruby layer is `RailsAdminNext`, but the action authorization keys, the `admin.*` I18n namespace, and the `src/rails_admin/` asset tree stay `rails_admin`-era on purpose, so host policies, translations, and assets keep working across the rename.
|
|
298
310
|
|
|
299
311
|
## Upgrading from RailsAdmin
|
|
300
312
|
|
|
301
313
|
See [docs/UPGRADING.md](docs/UPGRADING.md) for the one-time changes a host app needs:
|
|
302
314
|
the Gemfile gem name, the importmap/Propshaft prerequisites, and a mechanical rename of the
|
|
303
315
|
`RailsAdmin` constant, initializer, mount, model macro, and route helpers to `RailsAdminNext`
|
|
304
|
-
(`rails_admin_next`). The
|
|
305
|
-
|
|
306
|
-
|
|
316
|
+
(`rails_admin_next`). The action authorization keys and the `admin.*` I18n namespace are
|
|
317
|
+
deliberately left unchanged, so Pundit policies and translations keep working without edits.
|
|
318
|
+
Its 2.0 section covers hosts whose authorization library is no longer a built-in adapter.
|
|
307
319
|
|
|
308
320
|
## Security
|
|
309
321
|
|
|
@@ -333,5 +345,5 @@ RailsAdminNext is released under the [MIT License](LICENSE.md).
|
|
|
333
345
|
---
|
|
334
346
|
|
|
335
347
|
<div align="center">
|
|
336
|
-
<sub>
|
|
348
|
+
<sub>Made in Tokyo with ❤️ and 🤖</sub>
|
|
337
349
|
</div>
|
|
@@ -460,6 +460,14 @@ body.rails_admin .pagination .page-item {
|
|
|
460
460
|
white-space: nowrap;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
/* === Admin skin: footer ================================================= */
|
|
464
|
+
body.rails_admin .admin-footer {
|
|
465
|
+
padding: 2rem 0 1rem;
|
|
466
|
+
font-size: 0.875em;
|
|
467
|
+
color: var(--ra-gray-600);
|
|
468
|
+
text-align: center;
|
|
469
|
+
}
|
|
470
|
+
|
|
463
471
|
/* === Admin skin: responsive adjustments ================================= */
|
|
464
472
|
@media screen and (min-width: 576px) {
|
|
465
473
|
body.rails_admin .sidebar.btn-toggle-nav {
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
<div class="container-fluid">
|
|
21
21
|
<%= render template: 'layouts/rails_admin_next/content' %>
|
|
22
22
|
</div>
|
|
23
|
+
<% if RailsAdminNext::Config.show_footer %>
|
|
24
|
+
<footer class="admin-footer">Made in Tokyo with ❤️ and 🤖</footer>
|
|
25
|
+
<% end %>
|
|
23
26
|
</div>
|
|
24
27
|
</div>
|
|
25
28
|
</div>
|
data/docs/UPGRADING.md
CHANGED
|
@@ -8,9 +8,85 @@ the one-time changes a host app needs.
|
|
|
8
8
|
|
|
9
9
|
The Ruby namespace is renamed **`RailsAdmin` → `RailsAdminNext`** (and `rails_admin` →
|
|
10
10
|
`rails_admin_next`), so your initializer, engine mount, model macros, and route helpers
|
|
11
|
-
need a mechanical rename. The **authorization contract is deliberately unchanged** —
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
need a mechanical rename. The **authorization contract is deliberately unchanged** — every
|
|
12
|
+
action's authorization key and the `admin.*` I18n namespace stay exactly as they were, so
|
|
13
|
+
your Pundit policies and translations need no edits. CanCanCan is no longer a built-in
|
|
14
|
+
adapter; see the 2.0 section below.
|
|
15
|
+
|
|
16
|
+
## 2.0: CanCanCan support removed
|
|
17
|
+
|
|
18
|
+
Pundit and ActionPolicy are the built-in authorization adapters.
|
|
19
|
+
`config.authorize_with :cancancan` now raises `ArgumentError` while the initializer loads,
|
|
20
|
+
listing the registered adapters. Any other unregistered key does the same. Move to Pundit,
|
|
21
|
+
move to ActionPolicy (see [ActionPolicy adapter](#actionpolicy-adapter)), or keep CanCanCan
|
|
22
|
+
with a custom adapter.
|
|
23
|
+
|
|
24
|
+
### Move to Pundit
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
RailsAdminNext.config do |config|
|
|
28
|
+
config.authorize_with :pundit
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **`ApplicationPolicy`.** The adapter falls back to `::ApplicationPolicy` for the dashboard
|
|
33
|
+
(record `nil`) and for any model without its own policy.
|
|
34
|
+
- **Action predicates.** The adapter calls each action's authorization key with a `?`
|
|
35
|
+
suffix: `dashboard?`, `index?`, `show?`, `new?`, `edit?`, `export?`, `destroy?`,
|
|
36
|
+
`history?` and `show_in_app?`. Saving a new record also calls `create?`, and saving an
|
|
37
|
+
edit calls `update?`. Member actions pass the record; list-level checks pass the class.
|
|
38
|
+
`edit?` and `destroy?` also get the class: the form's "Save and edit" button checks
|
|
39
|
+
`edit?`, and the index's bulk-delete menu checks `destroy?`. A predicate that reads record
|
|
40
|
+
attributes must handle a class, for example `!record.is_a?(Class) && record.active?`.
|
|
41
|
+
- **A `Scope` for every model the admin lists.** A model whose policy resolves no `Scope`
|
|
42
|
+
lists nothing (see
|
|
43
|
+
[Pundit: a model with no policy Scope lists nothing](#pundit-a-model-with-no-policy-scope-lists-nothing)).
|
|
44
|
+
A policy that subclasses `ApplicationPolicy` without its own `Scope` inherits
|
|
45
|
+
`ApplicationPolicy::Scope`, so that class's `resolve` decides what the model lists.
|
|
46
|
+
- **Optional `attributes_for(action)`** on a policy returns the attributes a new record
|
|
47
|
+
starts with, for example `{suspended: true}` for `:new`.
|
|
48
|
+
|
|
49
|
+
Three differences from CanCanCan to check before switching:
|
|
50
|
+
|
|
51
|
+
- **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.
|
|
56
|
+
- **Nothing checks `:access, :rails_admin`.** Admin access is decided by the same policies,
|
|
57
|
+
and the `ApplicationPolicy` fallback, that the rest of the app uses. If those admit
|
|
58
|
+
ordinary users, gate admin entry separately: in `config.authenticate_with`, or with
|
|
59
|
+
admin-specific `authorization_key` values per action.
|
|
60
|
+
- **The block form only gates entry.** `config.authorize_with { ... }` runs before every
|
|
61
|
+
action, but it checks no records and does not scope lists.
|
|
62
|
+
|
|
63
|
+
### Keep CanCanCan with a custom adapter
|
|
64
|
+
|
|
65
|
+
Register an adapter before the `RailsAdminNext.config` block that names its key, or the
|
|
66
|
+
`ArgumentError` above fires at boot:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
RailsAdminNext.add_extension(:cancancan, MyApp::CanCanCanAdmin, authorization: true)
|
|
70
|
+
|
|
71
|
+
RailsAdminNext.config do |config|
|
|
72
|
+
config.authorize_with :cancancan
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The module defines an `AuthorizationAdapter` class. `authorize_with` builds it with
|
|
77
|
+
`new(controller, *args, &block)` and it answers:
|
|
78
|
+
|
|
79
|
+
- `authorize(action, abstract_model, object)`: raises when the action is denied.
|
|
80
|
+
- `authorized?(action, abstract_model, object)`: returns a boolean, for links and buttons.
|
|
81
|
+
- `query(action, abstract_model)`: returns the scope lists, export and bulk actions read from.
|
|
82
|
+
- `attributes_for(action, abstract_model)`: returns the attributes a new record starts with.
|
|
83
|
+
- `self.setup` (optional): runs once when `authorize_with` names the key.
|
|
84
|
+
|
|
85
|
+
`lib/rails_admin_next/extensions/pundit/authorization_adapter.rb` in the gem is the template.
|
|
86
|
+
The removed CanCanCan adapter is a starting point you can copy:
|
|
87
|
+
[v1.3.0 `extensions/cancancan/authorization_adapter.rb`](https://github.com/6temes/rails_admin_next/blob/v1.3.0/lib/rails_admin_next/extensions/cancancan/authorization_adapter.rb),
|
|
88
|
+
with its module in `extensions/cancancan.rb` at the same tag. Abilities keep granting
|
|
89
|
+
`can :access, :rails_admin`.
|
|
14
90
|
|
|
15
91
|
## 1. Gemfile
|
|
16
92
|
|
|
@@ -119,19 +195,19 @@ engine no longer provides it.
|
|
|
119
195
|
|
|
120
196
|
## Authorization (no change needed)
|
|
121
197
|
|
|
122
|
-
The authorization contract is held stable — **do not touch your
|
|
198
|
+
The authorization contract is held stable — **do not touch your policies**:
|
|
123
199
|
|
|
124
|
-
- The access subject is `:rails_admin`.
|
|
125
200
|
- Every action's authorization key (`:dashboard`, `:index`, `:show`, `:new`, `:edit`,
|
|
126
201
|
`:export`, `:destroy`, `:history`, `:show_in_app`) is unchanged.
|
|
202
|
+
- A custom adapter keeps the `:access, :rails_admin` subject by convention.
|
|
127
203
|
|
|
128
204
|
```ruby
|
|
129
|
-
#
|
|
205
|
+
# A custom CanCanCan adapter's ability still grants access with:
|
|
130
206
|
can :access, :rails_admin
|
|
131
207
|
```
|
|
132
208
|
|
|
133
|
-
The
|
|
134
|
-
|
|
209
|
+
The authorization keys are locked by a spec in the gem — a renamed key can fail *open* (a
|
|
210
|
+
deny rule that no longer matches silently grants access).
|
|
135
211
|
|
|
136
212
|
## Asset prerequisites
|
|
137
213
|
|
|
@@ -460,6 +536,10 @@ A model whose policy defines no `Scope` used to list every record. It now lists
|
|
|
460
536
|
list, export, bulk delete and dashboard count all return an empty set. Bulk delete deletes
|
|
461
537
|
whatever the scope returns, with no per-record check, so "no Scope" can't mean "everything".
|
|
462
538
|
|
|
539
|
+
This holds only when no `Scope` constant resolves. A policy that subclasses `ApplicationPolicy`
|
|
540
|
+
without its own `Scope` inherits `ApplicationPolicy::Scope`: Pundit's generated `resolve` there
|
|
541
|
+
raises `NoMethodError`, and one that returns `scope.all` lists every record.
|
|
542
|
+
|
|
463
543
|
Define a `Scope` for each model the admin exposes:
|
|
464
544
|
|
|
465
545
|
```ruby
|
|
@@ -472,7 +552,49 @@ class PlayerPolicy < ApplicationPolicy
|
|
|
472
552
|
end
|
|
473
553
|
```
|
|
474
554
|
|
|
475
|
-
|
|
555
|
+
## ActionPolicy adapter
|
|
556
|
+
|
|
557
|
+
[ActionPolicy](https://github.com/palkan/action_policy) is a built-in adapter next to Pundit.
|
|
558
|
+
Bundle `action_policy` in your app, then:
|
|
559
|
+
|
|
560
|
+
```ruby
|
|
561
|
+
RailsAdminNext.config do |config|
|
|
562
|
+
config.authorize_with :action_policy
|
|
563
|
+
config.current_user_method(&:current_user)
|
|
564
|
+
end
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
The adapter authorizes as the user `current_user_method` returns. It checks the record's
|
|
568
|
+
policy with the rule names the Pundit adapter uses: `dashboard?`, `index?`, `show?`, `new?`,
|
|
569
|
+
`edit?`, `export?`, `destroy?`, `history?`, `show_in_app?`, plus `create?` and `update?` when a
|
|
570
|
+
form is submitted, and `<authorization_key>?` for custom actions. Two ActionPolicy defaults
|
|
571
|
+
change how a ported Pundit policy behaves:
|
|
572
|
+
|
|
573
|
+
- `new?` is an alias of `create?` unless the policy defines `new?` itself.
|
|
574
|
+
- A rule the policy does not define resolves to `manage?`, which is `false` on
|
|
575
|
+
`ActionPolicy::Base`, instead of raising.
|
|
576
|
+
|
|
577
|
+
A model with no policy, and the dashboard, are checked against `ApplicationPolicy`. Lists,
|
|
578
|
+
export, bulk delete and dashboard counts use the policy's default `relation_scope`; a model
|
|
579
|
+
with no policy, no relation scope, or only named ones lists nothing. To prefill new records,
|
|
580
|
+
define `attributes_for(action)` on the policy, as with Pundit.
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
class ApplicationPolicy < ActionPolicy::Base
|
|
584
|
+
def dashboard? = user.admin?
|
|
585
|
+
def index? = user.admin?
|
|
586
|
+
def show? = user.admin?
|
|
587
|
+
def create? = user.admin?
|
|
588
|
+
def update? = user.admin?
|
|
589
|
+
alias_rule :edit?, to: :update?
|
|
590
|
+
def destroy? = user.admin?
|
|
591
|
+
def export? = user.admin?
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
class PlayerPolicy < ApplicationPolicy
|
|
595
|
+
relation_scope { |relation| relation }
|
|
596
|
+
end
|
|
597
|
+
```
|
|
476
598
|
|
|
477
599
|
## An `:enum` field with no option source offers no options
|
|
478
600
|
|
|
@@ -572,6 +694,12 @@ the primary key descending, so an export with no sort chosen is now newest first
|
|
|
572
694
|
Rows are read in batches of 1000 instead of loading every record up front. An export with no
|
|
573
695
|
matching records now contains the header row instead of an empty line.
|
|
574
696
|
|
|
697
|
+
## Admin pages show a footer
|
|
698
|
+
|
|
699
|
+
Every admin page now ends with a small, muted "Made in Tokyo with ❤️ and 🤖" line under the page
|
|
700
|
+
content. It is not shown inside modals. To hide it, set `config.show_footer = false` in your
|
|
701
|
+
initializer.
|
|
702
|
+
|
|
575
703
|
## Removed: options deprecated before RailsAdmin 3.0
|
|
576
704
|
|
|
577
705
|
These six were deprecated upstream between 2019-07 and 2022-01 and had no effect for
|
data/docs/security.md
CHANGED
|
@@ -16,6 +16,10 @@ destructive-action controls itself; a few things are left for the host to opt in
|
|
|
16
16
|
can't name a hidden column, an association the export doesn't show, or a method to call.
|
|
17
17
|
- **Pundit scopes fail closed.** A model whose policy has no `Scope` lists no records, rather
|
|
18
18
|
than all of them.
|
|
19
|
+
- **ActionPolicy scopes fail closed.** A model with no policy, or whose policy has no default
|
|
20
|
+
`relation_scope`, lists no records. A rule the policy does not define (`history?`,
|
|
21
|
+
`show_in_app?`, `export?`, a custom authorization key) resolves to the policy's `manage?`, so a
|
|
22
|
+
permissive `manage?` grants every admin action the policy leaves undefined.
|
|
19
23
|
- **Destructive actions.** `delete` and `bulk_delete` render a confirmation page and additionally
|
|
20
24
|
carry `data-turbo-confirm` on the destructive submit (Turbo's confirmation, not `@rails/ujs`).
|
|
21
25
|
- **Mass assignment.** Submitted attributes are sliced to the model config's `visible_fields`
|
|
@@ -7,12 +7,12 @@ RailsAdminNext.config do |config|
|
|
|
7
7
|
# end
|
|
8
8
|
# config.current_user_method(&:current_user)
|
|
9
9
|
|
|
10
|
-
## == CancanCan ==
|
|
11
|
-
# config.authorize_with :cancancan
|
|
12
|
-
|
|
13
10
|
## == Pundit ==
|
|
14
11
|
# config.authorize_with :pundit
|
|
15
12
|
|
|
13
|
+
## == ActionPolicy ==
|
|
14
|
+
# config.authorize_with :action_policy
|
|
15
|
+
|
|
16
16
|
## == PaperTrail ==
|
|
17
17
|
# config.audit_with :paper_trail, 'User', 'PaperTrail::Version' # PaperTrail >= 3.0.0
|
|
18
18
|
|
|
@@ -22,6 +22,10 @@ RailsAdminNext.config do |config|
|
|
|
22
22
|
## To disable Gravatar integration in Navigation Bar set to false
|
|
23
23
|
# config.show_gravatar = true
|
|
24
24
|
|
|
25
|
+
## == Footer ==
|
|
26
|
+
## To hide the "Made in Tokyo" footer under admin pages set to false
|
|
27
|
+
# config.show_footer = true
|
|
28
|
+
|
|
25
29
|
config.actions do
|
|
26
30
|
dashboard # mandatory
|
|
27
31
|
index # mandatory
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -5,8 +5,8 @@ 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 =
|
|
8
|
+
MAJOR = 2
|
|
9
|
+
MINOR = 0
|
|
10
10
|
PATCH = 0
|
|
11
11
|
PRE = nil
|
|
12
12
|
|
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"
|
|
@@ -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
|
|
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
|