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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +27 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1009 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +37 -27
  18. data/docs/getting-started/index.md +22 -29
  19. data/docs/getting-started/installation.md +37 -80
  20. data/docs/getting-started/tutorial/index.md +4 -5
  21. data/docs/guides/adding-resources.md +66 -377
  22. data/docs/guides/authentication.md +94 -463
  23. data/docs/guides/authorization.md +124 -370
  24. data/docs/guides/creating-packages.md +94 -296
  25. data/docs/guides/custom-actions.md +121 -441
  26. data/docs/guides/index.md +22 -42
  27. data/docs/guides/multi-tenancy.md +116 -187
  28. data/docs/guides/nested-resources.md +103 -431
  29. data/docs/guides/search-filtering.md +123 -240
  30. data/docs/guides/testing.md +5 -4
  31. data/docs/guides/theming.md +157 -407
  32. data/docs/guides/troubleshooting.md +5 -3
  33. data/docs/guides/user-invites.md +106 -425
  34. data/docs/guides/user-profile.md +76 -243
  35. data/docs/index.md +1 -1
  36. data/docs/reference/app/generators.md +517 -0
  37. data/docs/reference/app/index.md +158 -0
  38. data/docs/reference/app/packages.md +146 -0
  39. data/docs/reference/app/portals.md +377 -0
  40. data/docs/reference/auth/accounts.md +230 -0
  41. data/docs/reference/auth/index.md +88 -0
  42. data/docs/reference/auth/profile.md +185 -0
  43. data/docs/reference/behavior/controllers.md +395 -0
  44. data/docs/reference/behavior/index.md +22 -0
  45. data/docs/reference/behavior/interactions.md +341 -0
  46. data/docs/reference/behavior/policies.md +417 -0
  47. data/docs/reference/index.md +56 -49
  48. data/docs/reference/resource/actions.md +423 -0
  49. data/docs/reference/resource/definition.md +508 -0
  50. data/docs/reference/resource/index.md +50 -0
  51. data/docs/reference/resource/model.md +348 -0
  52. data/docs/reference/resource/query.md +305 -0
  53. data/docs/reference/tenancy/entity-scoping.md +361 -0
  54. data/docs/reference/tenancy/index.md +36 -0
  55. data/docs/reference/tenancy/invites.md +393 -0
  56. data/docs/reference/tenancy/nested-resources.md +267 -0
  57. data/docs/reference/testing/index.md +287 -0
  58. data/docs/reference/ui/assets.md +400 -0
  59. data/docs/reference/ui/components.md +165 -0
  60. data/docs/reference/ui/displays.md +104 -0
  61. data/docs/reference/ui/forms.md +284 -0
  62. data/docs/reference/ui/index.md +30 -0
  63. data/docs/reference/ui/layouts.md +106 -0
  64. data/docs/reference/ui/pages.md +189 -0
  65. data/docs/reference/ui/tables.md +117 -0
  66. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  67. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  68. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  69. data/gemfiles/rails_7.gemfile.lock +1 -1
  70. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  72. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  73. data/lib/generators/pu/invites/install_generator.rb +1 -0
  74. data/lib/plutonium/definition/base.rb +1 -1
  75. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  76. data/lib/plutonium/helpers/turbo_helper.rb +11 -0
  77. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  78. data/lib/plutonium/resource/controller.rb +1 -0
  79. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  80. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  81. data/lib/plutonium/resource/policy.rb +7 -0
  82. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  83. data/lib/plutonium/ui/component/methods.rb +4 -0
  84. data/lib/plutonium/ui/form/base.rb +6 -2
  85. data/lib/plutonium/ui/form/components/json.rb +58 -0
  86. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  87. data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
  88. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  89. data/lib/plutonium/ui/form/resource.rb +0 -4
  90. data/lib/plutonium/ui/grid/resource.rb +1 -1
  91. data/lib/plutonium/ui/layout/base.rb +1 -0
  92. data/lib/plutonium/ui/page/base.rb +0 -7
  93. data/lib/plutonium/ui/page/index.rb +4 -4
  94. data/lib/plutonium/ui/table/resource.rb +1 -1
  95. data/lib/plutonium/version.rb +1 -1
  96. data/lib/plutonium.rb +8 -0
  97. data/lib/tasks/release.rake +15 -1
  98. data/package.json +10 -10
  99. data/src/css/slim_select.css +4 -0
  100. data/src/js/controllers/slim_select_controller.js +61 -0
  101. data/src/js/turbo/turbo_actions.js +33 -0
  102. data/yarn.lock +553 -543
  103. metadata +44 -33
  104. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  105. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  106. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  107. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  108. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  109. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  110. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  111. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  112. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  113. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  114. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  115. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  116. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  117. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  118. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  119. data/docs/reference/assets/index.md +0 -496
  120. data/docs/reference/controller/index.md +0 -412
  121. data/docs/reference/definition/actions.md +0 -462
  122. data/docs/reference/definition/fields.md +0 -383
  123. data/docs/reference/definition/index.md +0 -326
  124. data/docs/reference/definition/query.md +0 -351
  125. data/docs/reference/generators/index.md +0 -648
  126. data/docs/reference/interaction/index.md +0 -449
  127. data/docs/reference/model/features.md +0 -248
  128. data/docs/reference/model/index.md +0 -218
  129. data/docs/reference/policy/index.md +0 -456
  130. data/docs/reference/portal/index.md +0 -379
  131. data/docs/reference/views/forms.md +0 -411
  132. data/docs/reference/views/index.md +0 -544
@@ -1,129 +1,77 @@
1
1
  # Authorization
2
2
 
3
- This guide covers implementing authorization policies to control access.
3
+ Control what users can do once authenticated. Plutonium uses ActionPolicy with extensions for attribute permissions and tenant scoping.
4
4
 
5
- ## Overview
5
+ ## Goal
6
6
 
7
- Plutonium authorization is built on [ActionPolicy](https://actionpolicy.evilmartians.io/) and works at three levels:
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
- 1. **Action Permissions** - Can the user perform this action?
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
- ## Policy Structure
11
+ Every policy controls three things:
14
12
 
15
- Policies inherit from a base `ResourcePolicy` class:
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
- ```ruby
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
- def update?
40
- owner?
41
- end
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
- def destroy?
44
- owner? || user.admin?
45
- end
24
+ ## Steps
46
25
 
47
- def permitted_attributes_for_create
48
- %i[title content]
49
- end
26
+ ### 1. Open the generated policy
50
27
 
51
- def permitted_attributes_for_read
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
- def permitted_associations
56
- %i[comments tags]
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
- private
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
- def update?
79
- user # => Current user
80
- record # => The specific Post instance
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
- ## Action Permissions
42
+ These default to `false` — without an explicit override, nobody can create or read records.
86
43
 
87
- ### Core Actions (Must Override)
44
+ ### 3. Override derived methods only when rules differ
88
45
 
89
- The base `Plutonium::Resource::Policy` defaults `create?` and `read?` to `false`. You must override these:
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 create? # Default: false
93
- user.present?
49
+ def update?
50
+ user.admin? || record.author == user
94
51
  end
95
52
 
96
- def read? # Default: false
97
- true
53
+ def destroy?
54
+ user.admin?
98
55
  end
99
56
  ```
100
57
 
101
- ### Derived Actions
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
- class PostPolicy < ResourcePolicy
117
- # Only need to override when rules differ
118
- def destroy?
119
- owner? || user.admin? # Different from create?
120
- end
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
- ### Custom Actions
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
- Define methods matching your action names:
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
- update? && !record.archived?
82
+ user.admin?
135
83
  end
136
84
  ```
137
85
 
138
- Actions are secure by default - undefined methods return `false`.
139
-
140
- ## Attribute Permissions
86
+ The method name matches the action name plus `?`. Undefined methods return `false`.
141
87
 
142
- ### Core Methods (Must Override for Production)
88
+ ### 6. Optionally filter the collection — `relation_scope`
143
89
 
144
90
  ```ruby
145
- # What users can see (index, show)
146
- def permitted_attributes_for_read
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
- ### Derived Methods
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
- ### Per-Action Attributes
98
+ ## Common patterns
167
99
 
168
- Show different fields for different views:
100
+ ### Owner-based
169
101
 
170
102
  ```ruby
171
- def permitted_attributes_for_index
172
- %i[title author_id created_at] # Minimal for list
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
- ### Conditional Attributes
107
+ ### Role-based
181
108
 
182
109
  ```ruby
183
- def permitted_attributes_for_create
184
- attrs = %i[title content]
185
- attrs << :featured if user.admin?
186
- attrs << :author_id if user.admin? # Only admins can set author
187
- attrs
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
- ### Auto-Detection Warning
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 permitted_associations
201
- %i[comments tags author]
202
- end
121
+ def update? = !record.try(:archived?) && super
122
+ def destroy? = !record.try(:archived?) && super
203
123
  ```
204
124
 
205
- Used for nested forms, related data displays, and association fields in tables.
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
- class PostPolicy < ResourcePolicy
213
- relation_scope do |relation|
214
- if user.admin?
215
- relation
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
- ### With Entity Scoping
226
-
227
- Call `super` to preserve automatic entity scoping for multi-tenancy:
135
+ ### Time-based
228
136
 
229
137
  ```ruby
230
- relation_scope do |relation|
231
- relation = super(relation) # Apply entity scope first
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
- ## Controller & View Helpers
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
- # In a view or controller
251
- authorized_resource_scope(Post) # => Post.where(...)
252
- authorized_resource_scope(Post).count # => 42
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
- ### policy_for
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
- Get the policy instance for any record:
155
+ Records come from `current_authorized_scope` users can only select records they can access.
259
156
 
260
- ```ruby
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
- allowed_to?(:edit?, @post) # => true/false
271
- allowed_to?(:create?, Post) # => true/false
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
- ```ruby
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
- # Admins can do everything
284
- def destroy?
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
- For restricted portals:
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
- ## Testing Policies
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
- # spec/policies/post_policy_spec.rb
361
- RSpec.describe PostPolicy, type: :policy do
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
- ## Common Patterns
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
- ### Role-Based Access
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
- ```ruby
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
- ### Time-Based Permissions
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
- def update?
406
- owner? && record.created_at > 24.hours.ago
198
+ relation_scope do |relation|
199
+ default_relation_scope(relation).where(archived: false)
407
200
  end
408
201
  ```
409
202
 
410
- ### Status-Based Permissions
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
- ### Check Model Capabilities
205
+ ## Anti-pattern: nested-attributes hashes in policies
421
206
 
422
207
  ```ruby
423
- def archive?
424
- return false unless record.respond_to?(:archived!)
425
- return false if record.archived?
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
- ### Prevent Actions on Archived Records
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
- def update?
434
- return false if record.try(:archived?)
435
- super
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
- ## Handling Unauthorized Access
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
- ### Custom Error Handling
225
+ ## Custom authorization context
449
226
 
450
227
  ```ruby
451
- # app/controllers/application_controller.rb
452
- class ApplicationController < ActionController::Base
453
- rescue_from ActionPolicy::Unauthorized do |exception|
454
- respond_to do |format|
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
- Built-in CRUD actions automatically verify authorization. For custom actions:
465
-
466
- ```ruby
234
+ # Controller
467
235
  class PostsController < ResourceController
468
- skip_verify_authorize_current only: [:custom_action]
469
-
470
- def custom_action
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
- ### Policy Inspection
242
+ ## Common issues
491
243
 
492
- ```ruby
493
- policy = PostPolicy.new(user: current_user, record: @post)
494
- puts policy.permitted_attributes_for_update.inspect
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
- - [Authentication](./authentication)
500
- - [Multi-tenancy](./multi-tenancy)
501
- - [Custom Actions](./custom-actions)
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