plutonium 0.50.0 → 0.51.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/.claude/skills/plutonium/SKILL.md +85 -102
- data/.claude/skills/plutonium-app/SKILL.md +572 -0
- data/.claude/skills/plutonium-auth/SKILL.md +163 -300
- data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
- data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
- data/.claude/skills/plutonium-testing/SKILL.md +6 -5
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +27 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1009 -1214
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +52 -51
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +37 -27
- data/docs/getting-started/index.md +22 -29
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +94 -463
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +94 -296
- data/docs/guides/custom-actions.md +121 -441
- data/docs/guides/index.md +22 -42
- data/docs/guides/multi-tenancy.md +116 -187
- data/docs/guides/nested-resources.md +103 -431
- data/docs/guides/search-filtering.md +123 -240
- data/docs/guides/testing.md +5 -4
- data/docs/guides/theming.md +157 -407
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +106 -425
- data/docs/guides/user-profile.md +76 -243
- data/docs/index.md +1 -1
- data/docs/reference/app/generators.md +517 -0
- data/docs/reference/app/index.md +158 -0
- data/docs/reference/app/packages.md +146 -0
- data/docs/reference/app/portals.md +377 -0
- data/docs/reference/auth/accounts.md +230 -0
- data/docs/reference/auth/index.md +88 -0
- data/docs/reference/auth/profile.md +185 -0
- data/docs/reference/behavior/controllers.md +395 -0
- data/docs/reference/behavior/index.md +22 -0
- data/docs/reference/behavior/interactions.md +341 -0
- data/docs/reference/behavior/policies.md +417 -0
- data/docs/reference/index.md +56 -49
- data/docs/reference/resource/actions.md +423 -0
- data/docs/reference/resource/definition.md +508 -0
- data/docs/reference/resource/index.md +50 -0
- data/docs/reference/resource/model.md +348 -0
- data/docs/reference/resource/query.md +305 -0
- data/docs/reference/tenancy/entity-scoping.md +361 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +393 -0
- data/docs/reference/tenancy/nested-resources.md +267 -0
- data/docs/reference/testing/index.md +287 -0
- data/docs/reference/ui/assets.md +400 -0
- data/docs/reference/ui/components.md +165 -0
- data/docs/reference/ui/displays.md +104 -0
- data/docs/reference/ui/forms.md +284 -0
- data/docs/reference/ui/index.md +30 -0
- data/docs/reference/ui/layouts.md +106 -0
- data/docs/reference/ui/pages.md +189 -0
- data/docs/reference/ui/tables.md +117 -0
- data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
- data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
- data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +1 -0
- data/lib/plutonium/definition/base.rb +1 -1
- data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
- data/lib/plutonium/helpers/turbo_helper.rb +11 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
- data/lib/plutonium/resource/controller.rb +1 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
- data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
- data/lib/plutonium/resource/policy.rb +7 -0
- data/lib/plutonium/routing/mapper_extensions.rb +15 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/form/base.rb +6 -2
- data/lib/plutonium/ui/form/components/json.rb +58 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
- data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/grid/resource.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +0 -7
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +8 -0
- data/lib/tasks/release.rake +15 -1
- data/package.json +10 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +553 -543
- metadata +44 -33
- data/.claude/skills/plutonium-assets/SKILL.md +0 -512
- data/.claude/skills/plutonium-controller/SKILL.md +0 -396
- data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
- data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
- data/.claude/skills/plutonium-forms/SKILL.md +0 -465
- data/.claude/skills/plutonium-installation/SKILL.md +0 -331
- data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
- data/.claude/skills/plutonium-invites/SKILL.md +0 -408
- data/.claude/skills/plutonium-model/SKILL.md +0 -440
- data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
- data/.claude/skills/plutonium-package/SKILL.md +0 -198
- data/.claude/skills/plutonium-policy/SKILL.md +0 -456
- data/.claude/skills/plutonium-portal/SKILL.md +0 -410
- data/.claude/skills/plutonium-views/SKILL.md +0 -651
- data/docs/reference/assets/index.md +0 -496
- data/docs/reference/controller/index.md +0 -412
- data/docs/reference/definition/actions.md +0 -462
- data/docs/reference/definition/fields.md +0 -383
- data/docs/reference/definition/index.md +0 -326
- data/docs/reference/definition/query.md +0 -351
- data/docs/reference/generators/index.md +0 -648
- data/docs/reference/interaction/index.md +0 -449
- data/docs/reference/model/features.md +0 -248
- data/docs/reference/model/index.md +0 -218
- data/docs/reference/policy/index.md +0 -456
- data/docs/reference/portal/index.md +0 -379
- data/docs/reference/views/forms.md +0 -411
- data/docs/reference/views/index.md +0 -544
|
@@ -1,129 +1,77 @@
|
|
|
1
1
|
# Authorization
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Control what users can do once authenticated. Plutonium uses ActionPolicy with extensions for attribute permissions and tenant scoping.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Goal
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
For each resource, decide who can create / read / update / destroy / run custom actions, and which fields they can see and edit.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
2. **Attribute Permissions** - Which fields can the user see/modify?
|
|
11
|
-
3. **Scope Permissions** - Which records can the user access?
|
|
9
|
+
## The three layers
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
Every policy controls three things:
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
1. **Action permissions** — `create?`, `read?`, `update?`, `destroy?`, plus your custom action methods.
|
|
14
|
+
2. **Attribute permissions** — `permitted_attributes_for_create`, `_for_read`, etc.
|
|
15
|
+
3. **Collection scope** — `relation_scope` (which records show up in lists).
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
# app/policies/resource_policy.rb (generated during install)
|
|
19
|
-
class ResourcePolicy < Plutonium::Resource::Policy
|
|
20
|
-
def create?
|
|
21
|
-
true
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def read?
|
|
25
|
-
true
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# app/policies/post_policy.rb (per resource)
|
|
30
|
-
class PostPolicy < ResourcePolicy
|
|
31
|
-
def create?
|
|
32
|
-
user.present?
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def read?
|
|
36
|
-
true
|
|
37
|
-
end
|
|
17
|
+
## 🚨 Critical
|
|
38
18
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
19
|
+
- **`create?` and `read?` default to `false`.** Always override them explicitly. Derived methods (`update?`, `show?`, `index?`) inherit automatically.
|
|
20
|
+
- **`permitted_attributes_for_*` must be explicit in production.** Dev auto-detects; production raises.
|
|
21
|
+
- **`relation_scope` must call `default_relation_scope(relation)` explicitly** — never `super`. See [Reference › Behavior › Policies](/reference/behavior/policies).
|
|
22
|
+
- **Custom action ⇒ policy method.** `action :publish` needs `def publish?` on the policy. Undefined methods return `false` → action silently disappears.
|
|
42
23
|
|
|
43
|
-
|
|
44
|
-
owner? || user.admin?
|
|
45
|
-
end
|
|
24
|
+
## Steps
|
|
46
25
|
|
|
47
|
-
|
|
48
|
-
%i[title content]
|
|
49
|
-
end
|
|
26
|
+
### 1. Open the generated policy
|
|
50
27
|
|
|
51
|
-
|
|
52
|
-
%i[title content author_id created_at updated_at]
|
|
53
|
-
end
|
|
28
|
+
After `pu:res:scaffold` + `pu:res:conn`, you have:
|
|
54
29
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
end
|
|
30
|
+
- `app/policies/post_policy.rb` (base policy)
|
|
31
|
+
- `packages/admin_portal/app/policies/admin_portal/post_policy.rb` (per-portal override, seeded by `pu:res:conn`)
|
|
58
32
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def owner?
|
|
62
|
-
record.user_id == user.id
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Policy Context
|
|
68
|
-
|
|
69
|
-
Inside a policy, you have access to:
|
|
70
|
-
|
|
71
|
-
| Variable | Description |
|
|
72
|
-
|----------|-------------|
|
|
73
|
-
| `user` | Current authenticated user (required) |
|
|
74
|
-
| `record` | The resource being authorized |
|
|
75
|
-
| `entity_scope` | Current scoped entity (for multi-tenancy) |
|
|
33
|
+
### 2. Override `create?` and `read?` explicitly
|
|
76
34
|
|
|
77
35
|
```ruby
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
entity_scope # => Current parent/tenant entity
|
|
36
|
+
class PostPolicy < ResourcePolicy
|
|
37
|
+
def create? = user.present?
|
|
38
|
+
def read? = true
|
|
82
39
|
end
|
|
83
40
|
```
|
|
84
41
|
|
|
85
|
-
|
|
42
|
+
These default to `false` — without an explicit override, nobody can create or read records.
|
|
86
43
|
|
|
87
|
-
###
|
|
44
|
+
### 3. Override derived methods only when rules differ
|
|
88
45
|
|
|
89
|
-
|
|
46
|
+
`update?` inherits from `create?`. `index?`/`show?` inherit from `read?`. Only override when the rule is genuinely different:
|
|
90
47
|
|
|
91
48
|
```ruby
|
|
92
|
-
def
|
|
93
|
-
user.
|
|
49
|
+
def update?
|
|
50
|
+
user.admin? || record.author == user
|
|
94
51
|
end
|
|
95
52
|
|
|
96
|
-
def
|
|
97
|
-
|
|
53
|
+
def destroy?
|
|
54
|
+
user.admin?
|
|
98
55
|
end
|
|
99
56
|
```
|
|
100
57
|
|
|
101
|
-
###
|
|
102
|
-
|
|
103
|
-
Other actions inherit from core actions by default:
|
|
104
|
-
|
|
105
|
-
| Method | Inherits From | Override When |
|
|
106
|
-
|--------|---------------|---------------|
|
|
107
|
-
| `update?` | `create?` | Different update rules |
|
|
108
|
-
| `destroy?` | `create?` | Different delete rules |
|
|
109
|
-
| `index?` | `read?` | Custom listing rules |
|
|
110
|
-
| `show?` | `read?` | Record-specific read rules |
|
|
111
|
-
| `new?` | `create?` | Rarely needed |
|
|
112
|
-
| `edit?` | `update?` | Rarely needed |
|
|
113
|
-
| `search?` | `index?` | Search-specific rules |
|
|
58
|
+
### 4. Declare attribute permissions
|
|
114
59
|
|
|
115
60
|
```ruby
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
61
|
+
def permitted_attributes_for_create
|
|
62
|
+
%i[title content category]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def permitted_attributes_for_read
|
|
66
|
+
%i[title content category author published_at created_at]
|
|
121
67
|
end
|
|
122
68
|
```
|
|
123
69
|
|
|
124
|
-
|
|
70
|
+
::: warning Index has no `record`
|
|
71
|
+
`permitted_attributes_for_index` runs at collection level — `record` is `nil`. If you write a `record`-dependent `_for_read`, you MUST also declare an explicit `_for_index`. See [Reference › Behavior › Policies › Index has no record](/reference/behavior/policies#index-has-no-record).
|
|
72
|
+
:::
|
|
125
73
|
|
|
126
|
-
|
|
74
|
+
### 5. Custom action methods
|
|
127
75
|
|
|
128
76
|
```ruby
|
|
129
77
|
def publish?
|
|
@@ -131,371 +79,177 @@ def publish?
|
|
|
131
79
|
end
|
|
132
80
|
|
|
133
81
|
def archive?
|
|
134
|
-
|
|
82
|
+
user.admin?
|
|
135
83
|
end
|
|
136
84
|
```
|
|
137
85
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
## Attribute Permissions
|
|
86
|
+
The method name matches the action name plus `?`. Undefined methods return `false`.
|
|
141
87
|
|
|
142
|
-
###
|
|
88
|
+
### 6. Optionally filter the collection — `relation_scope`
|
|
143
89
|
|
|
144
90
|
```ruby
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
%i[title content author_id published_at created_at]
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# What users can set (create, update)
|
|
151
|
-
def permitted_attributes_for_create
|
|
152
|
-
%i[title content]
|
|
91
|
+
relation_scope do |relation|
|
|
92
|
+
default_relation_scope(relation).where(published: true)
|
|
153
93
|
end
|
|
154
94
|
```
|
|
155
95
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
| Method | Inherits From |
|
|
159
|
-
|--------|---------------|
|
|
160
|
-
| `permitted_attributes_for_update` | `permitted_attributes_for_create` |
|
|
161
|
-
| `permitted_attributes_for_index` | `permitted_attributes_for_read` |
|
|
162
|
-
| `permitted_attributes_for_show` | `permitted_attributes_for_read` |
|
|
163
|
-
| `permitted_attributes_for_new` | `permitted_attributes_for_create` |
|
|
164
|
-
| `permitted_attributes_for_edit` | `permitted_attributes_for_update` |
|
|
96
|
+
🚨 Always call `default_relation_scope(relation)` explicitly — not `super`. Bypassing it triggers `verify_default_relation_scope_applied!` at runtime.
|
|
165
97
|
|
|
166
|
-
|
|
98
|
+
## Common patterns
|
|
167
99
|
|
|
168
|
-
|
|
100
|
+
### Owner-based
|
|
169
101
|
|
|
170
102
|
```ruby
|
|
171
|
-
def
|
|
172
|
-
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def permitted_attributes_for_read
|
|
176
|
-
%i[title content author_id tags created_at updated_at] # Full for detail
|
|
177
|
-
end
|
|
103
|
+
def update? = record.author == user || user.admin?
|
|
104
|
+
def destroy? = update?
|
|
178
105
|
```
|
|
179
106
|
|
|
180
|
-
###
|
|
107
|
+
### Role-based
|
|
181
108
|
|
|
182
109
|
```ruby
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
110
|
+
def create? = user.admin? || user.editor?
|
|
111
|
+
|
|
112
|
+
def update?
|
|
113
|
+
return true if user.admin?
|
|
114
|
+
user.editor? && record.author == user
|
|
188
115
|
end
|
|
189
116
|
```
|
|
190
117
|
|
|
191
|
-
###
|
|
192
|
-
|
|
193
|
-
In development, undefined attribute methods auto-detect from the model. **This raises errors in production** - always define explicitly.
|
|
194
|
-
|
|
195
|
-
## Association Permissions
|
|
196
|
-
|
|
197
|
-
Control which associations can be rendered:
|
|
118
|
+
### Block archived records
|
|
198
119
|
|
|
199
120
|
```ruby
|
|
200
|
-
def
|
|
201
|
-
|
|
202
|
-
end
|
|
121
|
+
def update? = !record.try(:archived?) && super
|
|
122
|
+
def destroy? = !record.try(:archived?) && super
|
|
203
123
|
```
|
|
204
124
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
## Scope Permissions
|
|
208
|
-
|
|
209
|
-
Control which records appear in lists using ActionPolicy's `relation_scope`:
|
|
125
|
+
### Conditional attribute access
|
|
210
126
|
|
|
211
127
|
```ruby
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
else
|
|
217
|
-
relation.where(published: true).or(
|
|
218
|
-
relation.where(user_id: user.id)
|
|
219
|
-
)
|
|
220
|
-
end
|
|
221
|
-
end
|
|
128
|
+
def permitted_attributes_for_create
|
|
129
|
+
attrs = %i[title content]
|
|
130
|
+
attrs += %i[featured author_id] if user.admin?
|
|
131
|
+
attrs
|
|
222
132
|
end
|
|
223
133
|
```
|
|
224
134
|
|
|
225
|
-
###
|
|
226
|
-
|
|
227
|
-
Call `super` to preserve automatic entity scoping for multi-tenancy:
|
|
135
|
+
### Time-based
|
|
228
136
|
|
|
229
137
|
```ruby
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if user.admin?
|
|
234
|
-
relation
|
|
235
|
-
else
|
|
236
|
-
relation.where(published: true)
|
|
237
|
-
end
|
|
138
|
+
def update?
|
|
139
|
+
return false if record.created_at < 24.hours.ago
|
|
140
|
+
owner?
|
|
238
141
|
end
|
|
239
142
|
```
|
|
240
143
|
|
|
241
|
-
##
|
|
242
|
-
|
|
243
|
-
These helpers are available in controllers and views for authorization checks.
|
|
244
|
-
|
|
245
|
-
### authorized_resource_scope
|
|
246
|
-
|
|
247
|
-
Get an authorized scope for a resource other than the current controller's resource. Useful in dashboards and custom views:
|
|
144
|
+
## Bulk action authorization — per record
|
|
248
145
|
|
|
249
146
|
```ruby
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
authorized_resource_scope(Comment, relation: post.comments)
|
|
147
|
+
def bulk_archive?
|
|
148
|
+
create? && !record.locked? # checked PER record in the selection
|
|
149
|
+
end
|
|
254
150
|
```
|
|
255
151
|
|
|
256
|
-
|
|
152
|
+
- **Backend:** if any selected record fails, the entire request is rejected.
|
|
153
|
+
- **UI:** only actions ALL selected records support are shown (intersection).
|
|
257
154
|
|
|
258
|
-
|
|
155
|
+
Records come from `current_authorized_scope` — users can only select records they can access.
|
|
259
156
|
|
|
260
|
-
|
|
261
|
-
policy_for(@post) # => PostPolicy instance
|
|
262
|
-
policy_for(@post).update? # => true/false
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
### allowed_to?
|
|
266
|
-
|
|
267
|
-
Check if an action is permitted:
|
|
157
|
+
## Portal-specific policies
|
|
268
158
|
|
|
269
159
|
```ruby
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
## Portal-Specific Policies
|
|
275
|
-
|
|
276
|
-
Override policies for specific portals:
|
|
160
|
+
class PostPolicy < ResourcePolicy
|
|
161
|
+
def create? = user.present?
|
|
162
|
+
end
|
|
277
163
|
|
|
278
|
-
|
|
279
|
-
# packages/admin_portal/app/policies/admin_portal/post_policy.rb
|
|
164
|
+
# Admin — more permissive
|
|
280
165
|
class AdminPortal::PostPolicy < ::PostPolicy
|
|
281
166
|
include AdminPortal::ResourcePolicy
|
|
282
167
|
|
|
283
|
-
|
|
284
|
-
def
|
|
285
|
-
true
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
def permitted_attributes_for_create
|
|
289
|
-
%i[title content featured internal_notes] # More fields
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
relation_scope do |relation|
|
|
293
|
-
relation # No restrictions
|
|
294
|
-
end
|
|
168
|
+
def destroy? = true
|
|
169
|
+
def permitted_attributes_for_create = %i[title content featured internal_notes]
|
|
295
170
|
end
|
|
296
|
-
```
|
|
297
171
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
```ruby
|
|
301
|
-
# packages/public_portal/app/policies/public_portal/post_policy.rb
|
|
172
|
+
# Public — read-only
|
|
302
173
|
class PublicPortal::PostPolicy < ::PostPolicy
|
|
303
174
|
include PublicPortal::ResourcePolicy
|
|
304
|
-
|
|
305
|
-
def create?
|
|
306
|
-
false # No public creation
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
relation_scope do |relation|
|
|
310
|
-
relation.where(published: true) # Only published
|
|
311
|
-
end
|
|
312
|
-
end
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
Plutonium automatically uses portal-specific policies when available.
|
|
316
|
-
|
|
317
|
-
## Policy Helpers
|
|
318
|
-
|
|
319
|
-
Extract common logic into concerns:
|
|
320
|
-
|
|
321
|
-
```ruby
|
|
322
|
-
# app/policies/concerns/ownership.rb
|
|
323
|
-
module Ownership
|
|
324
|
-
extend ActiveSupport::Concern
|
|
325
|
-
|
|
326
|
-
def owner?
|
|
327
|
-
return false unless record.respond_to?(:user_id)
|
|
328
|
-
record.user_id == user.id
|
|
329
|
-
end
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
# Use in policies
|
|
333
|
-
class PostPolicy < ResourcePolicy
|
|
334
|
-
include Ownership
|
|
335
|
-
|
|
336
|
-
def update?
|
|
337
|
-
owner? || user.admin?
|
|
338
|
-
end
|
|
175
|
+
def create? = false
|
|
339
176
|
end
|
|
340
177
|
```
|
|
341
178
|
|
|
342
|
-
##
|
|
343
|
-
|
|
344
|
-
### Manual Testing
|
|
345
|
-
|
|
346
|
-
```bash
|
|
347
|
-
rails runner "
|
|
348
|
-
user = User.first
|
|
349
|
-
post = Post.first
|
|
350
|
-
policy = PostPolicy.new(user: user, record: post)
|
|
351
|
-
|
|
352
|
-
puts 'Can read: ' + policy.read?.to_s
|
|
353
|
-
puts 'Can update: ' + policy.update?.to_s
|
|
354
|
-
"
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
### RSpec with ActionPolicy
|
|
179
|
+
## Show-page association tabs
|
|
358
180
|
|
|
359
181
|
```ruby
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
let(:user) { create(:user) }
|
|
363
|
-
let(:other_user) { create(:user) }
|
|
364
|
-
|
|
365
|
-
describe '#update?' do
|
|
366
|
-
context 'when user owns the post' do
|
|
367
|
-
let(:record) { create(:post, user: user) }
|
|
368
|
-
|
|
369
|
-
it { is_expected.to be_allowed_to(:update?) }
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
context 'when user does not own the post' do
|
|
373
|
-
let(:record) { create(:post, user: other_user) }
|
|
374
|
-
|
|
375
|
-
it { is_expected.not_to be_allowed_to(:update?) }
|
|
376
|
-
end
|
|
377
|
-
end
|
|
182
|
+
def permitted_associations
|
|
183
|
+
%i[comments tags author]
|
|
378
184
|
end
|
|
379
185
|
```
|
|
380
186
|
|
|
381
|
-
|
|
187
|
+
Drives the show-page tablist. Each named association must exist on the model AND be a registered Plutonium resource. See [Reference › Behavior › Policies › Association permissions](/reference/behavior/policies#association-permissions).
|
|
382
188
|
|
|
383
|
-
|
|
189
|
+
::: warning Not for nested forms
|
|
190
|
+
`permitted_associations` is for show-page navigation tabs, NOT nested forms. Nested forms come from `nested_input :variants` in the definition. See [Reference › Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs).
|
|
191
|
+
:::
|
|
384
192
|
|
|
385
|
-
|
|
386
|
-
class PostPolicy < ResourcePolicy
|
|
387
|
-
def destroy?
|
|
388
|
-
case user.role
|
|
389
|
-
when 'admin'
|
|
390
|
-
true
|
|
391
|
-
when 'editor'
|
|
392
|
-
record.draft?
|
|
393
|
-
when 'author'
|
|
394
|
-
owner? && record.draft?
|
|
395
|
-
else
|
|
396
|
-
false
|
|
397
|
-
end
|
|
398
|
-
end
|
|
399
|
-
end
|
|
400
|
-
```
|
|
193
|
+
## Multi-tenant scoping
|
|
401
194
|
|
|
402
|
-
|
|
195
|
+
When the portal sets `scope_to_entity Organization`, the inherited `relation_scope` automatically filters everything to the current org — no work in the policy. To add filters on top:
|
|
403
196
|
|
|
404
197
|
```ruby
|
|
405
|
-
|
|
406
|
-
|
|
198
|
+
relation_scope do |relation|
|
|
199
|
+
default_relation_scope(relation).where(archived: false)
|
|
407
200
|
end
|
|
408
201
|
```
|
|
409
202
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
```ruby
|
|
413
|
-
def update?
|
|
414
|
-
return false if record.archived?
|
|
415
|
-
return true if user.admin?
|
|
416
|
-
owner? && record.draft?
|
|
417
|
-
end
|
|
418
|
-
```
|
|
203
|
+
See [Multi-tenancy](./multi-tenancy) and [Reference › Tenancy › Entity scoping](/reference/tenancy/entity-scoping).
|
|
419
204
|
|
|
420
|
-
|
|
205
|
+
## Anti-pattern: nested-attributes hashes in policies
|
|
421
206
|
|
|
422
207
|
```ruby
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
update?
|
|
208
|
+
# ❌ NEVER
|
|
209
|
+
def permitted_attributes_for_create
|
|
210
|
+
[:name, {variants_attributes: [:id, :name, :_destroy]}]
|
|
427
211
|
end
|
|
428
212
|
```
|
|
429
213
|
|
|
430
|
-
|
|
214
|
+
Nested params are extracted by the form definition, not the policy. The hash entry renders as a literal text input. Use just the association name:
|
|
431
215
|
|
|
432
216
|
```ruby
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
def destroy?
|
|
439
|
-
return false if record.try(:archived?)
|
|
440
|
-
super
|
|
217
|
+
# ✅ Policy permits just the association name
|
|
218
|
+
def permitted_attributes_for_create
|
|
219
|
+
[:name, :variants]
|
|
441
220
|
end
|
|
442
221
|
```
|
|
443
222
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
When authorization fails, ActionPolicy raises `ActionPolicy::Unauthorized`.
|
|
223
|
+
`nested_input :variants` in the definition handles the rest. See [Reference › Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs).
|
|
447
224
|
|
|
448
|
-
|
|
225
|
+
## Custom authorization context
|
|
449
226
|
|
|
450
227
|
```ruby
|
|
451
|
-
#
|
|
452
|
-
class
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
format.html { redirect_to root_path, alert: "You are not authorized." }
|
|
456
|
-
format.json { render json: { error: "Unauthorized" }, status: :forbidden }
|
|
457
|
-
end
|
|
458
|
-
end
|
|
228
|
+
# Policy
|
|
229
|
+
class PostPolicy < ResourcePolicy
|
|
230
|
+
authorize :department, allow_nil: true
|
|
231
|
+
def create? = department&.allows_posting?
|
|
459
232
|
end
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
### Skip Verification (Custom Actions)
|
|
463
233
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
```ruby
|
|
234
|
+
# Controller
|
|
467
235
|
class PostsController < ResourceController
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def
|
|
471
|
-
# Handle authorization manually or skip entirely
|
|
472
|
-
end
|
|
473
|
-
end
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
## Debugging Authorization
|
|
477
|
-
|
|
478
|
-
### Check Why Access Denied
|
|
479
|
-
|
|
480
|
-
Add logging to your policy:
|
|
481
|
-
|
|
482
|
-
```ruby
|
|
483
|
-
def update?
|
|
484
|
-
result = owner?
|
|
485
|
-
Rails.logger.debug { "PostPolicy#update? for user #{user.id} on post #{record.id}: #{result}" }
|
|
486
|
-
result
|
|
236
|
+
authorize :department, through: :current_department
|
|
237
|
+
private
|
|
238
|
+
def current_department = current_user.department
|
|
487
239
|
end
|
|
488
240
|
```
|
|
489
241
|
|
|
490
|
-
|
|
242
|
+
## Common issues
|
|
491
243
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
244
|
+
- **Undefined custom action policy method** — the button silently disappears (undefined returns `false`). Add `def my_action?` to the policy.
|
|
245
|
+
- **`record.X` crashes during index** — `record` is `nil` on index. Add an explicit `permitted_attributes_for_index` that doesn't depend on `record`.
|
|
246
|
+
- **`verify_default_relation_scope_applied!` raises** — your custom `relation_scope` doesn't call `default_relation_scope(relation)`. Fix by composing: `default_relation_scope(relation).where(...)`.
|
|
247
|
+
- **`super` in `relation_scope` doesn't behave as expected** — use `default_relation_scope(relation)` explicitly; `super`'s semantics depend on how ActionPolicy registered the scope.
|
|
496
248
|
|
|
497
249
|
## Related
|
|
498
250
|
|
|
499
|
-
- [
|
|
500
|
-
- [
|
|
501
|
-
- [
|
|
251
|
+
- [Reference › Behavior › Policies](/reference/behavior/policies) — full policy surface
|
|
252
|
+
- [Reference › Tenancy › Entity scoping](/reference/tenancy/entity-scoping) — `default_relation_scope`, multi-tenant patterns
|
|
253
|
+
- [Authentication](./authentication) — who's the user in the first place
|
|
254
|
+
- [Multi-tenancy](./multi-tenancy) — entity scoping setup
|
|
255
|
+
- [Custom actions](./custom-actions) — defining the actions that need policy methods
|