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,456 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plutonium-policy
|
|
3
|
-
description: Use BEFORE writing relation_scope, permitted_attributes, permitted_associations, or any policy override. For tenant-scoped relation_scope, also load plutonium-entity-scoping.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Plutonium Policies
|
|
7
|
-
|
|
8
|
-
## 🚨 Critical (read first)
|
|
9
|
-
- **Use generators.** `pu:res:scaffold` and `pu:res:conn` create policies — never hand-write policy files.
|
|
10
|
-
- **Never bypass `default_relation_scope`.** Overriding `relation_scope` with a raw `where(organization: …)` or manual joins skips entity scoping and triggers `verify_default_relation_scope_applied!` at runtime. Compose like this: `relation_scope { |r| default_relation_scope(r).where(archived: false) }`. Call `default_relation_scope(r)` **explicitly** — `super` is unreliable inside the DSL block. Full rules in `plutonium-entity-scoping`.
|
|
11
|
-
- **Derived actions inherit.** `update?` falls back to `create?`, `show?` falls back to `read?` — don't duplicate unless the rules genuinely differ. Override `create?` and `read?` explicitly; they default to `false`.
|
|
12
|
-
- **Define `permitted_attributes_for_*` explicitly.** Auto-detection works in development but raises in production.
|
|
13
|
-
- **For `has_cents` fields, list the virtual name (`:price`), not the column (`:price_cents`).** Generators occasionally emit the wrong one — fix it (and verify the model has `has_cents`). See `plutonium-model` › Monetary Handling.
|
|
14
|
-
- **Related skills:** `plutonium-entity-scoping` (tenant-scoped overrides — required for `relation_scope`), `plutonium-model` (`associated_with`), `plutonium-definition` (`permitted_attributes` usage), `plutonium-controller` (how controllers use policies).
|
|
15
|
-
|
|
16
|
-
## Quick checklist
|
|
17
|
-
|
|
18
|
-
Writing / editing a policy:
|
|
19
|
-
|
|
20
|
-
1. Confirm the policy was created by `pu:res:scaffold` or `pu:res:conn`.
|
|
21
|
-
2. Override `create?` and `read?` explicitly — they default to `false`.
|
|
22
|
-
3. Define `permitted_attributes_for_read` and `permitted_attributes_for_create` (derived methods inherit).
|
|
23
|
-
4. For custom actions, add `def <action>?` matching the definition's `action :<action>`.
|
|
24
|
-
5. If you need `relation_scope`, compose with `default_relation_scope(relation).where(...)` — never bypass it.
|
|
25
|
-
6. For tenant scoping, load `plutonium-entity-scoping` and fix the **model**, not the policy.
|
|
26
|
-
7. Per-portal overrides go in the portal's policy file (created by `pu:res:conn`).
|
|
27
|
-
8. Test: log in as a user who should NOT see a record, verify it's filtered out.
|
|
28
|
-
|
|
29
|
-
**Policies are generated automatically** - never create them manually:
|
|
30
|
-
- `rails g pu:res:scaffold` creates the base policy
|
|
31
|
-
- `rails g pu:res:conn` creates portal-specific policies with attribute permissions
|
|
32
|
-
|
|
33
|
-
Policies control WHO can do WHAT with resources. Built on [ActionPolicy](https://actionpolicy.evilmartians.io/).
|
|
34
|
-
|
|
35
|
-
Plutonium extends ActionPolicy with:
|
|
36
|
-
- Attribute permissions (`permitted_attributes_for_*`)
|
|
37
|
-
- Association permissions (`permitted_associations`)
|
|
38
|
-
- Automatic entity scoping for multi-tenancy
|
|
39
|
-
- Derived action methods (e.g., `update?` inherits from `create?`)
|
|
40
|
-
|
|
41
|
-
## Base Class
|
|
42
|
-
|
|
43
|
-
```ruby
|
|
44
|
-
# app/policies/resource_policy.rb (generated during install)
|
|
45
|
-
class ResourcePolicy < Plutonium::Resource::Policy
|
|
46
|
-
# App-wide authorization defaults
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# app/policies/post_policy.rb (per resource)
|
|
50
|
-
class PostPolicy < ResourcePolicy
|
|
51
|
-
def create?
|
|
52
|
-
user.present?
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def read?
|
|
56
|
-
true
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def permitted_attributes_for_create
|
|
60
|
-
%i[title content]
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def permitted_attributes_for_read
|
|
64
|
-
%i[title content author created_at]
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Action Permissions
|
|
70
|
-
|
|
71
|
-
### Core Actions (Must Override)
|
|
72
|
-
|
|
73
|
-
```ruby
|
|
74
|
-
def create? # Default: false - MUST override
|
|
75
|
-
user.present?
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def read? # Default: false - MUST override
|
|
79
|
-
true
|
|
80
|
-
end
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Derived Actions (Inherit by Default)
|
|
84
|
-
|
|
85
|
-
| Method | Inherits From | Override When |
|
|
86
|
-
|--------|---------------|---------------|
|
|
87
|
-
| `update?` | `create?` | Different update rules |
|
|
88
|
-
| `destroy?` | `create?` | Different delete rules |
|
|
89
|
-
| `index?` | `read?` | Custom listing rules |
|
|
90
|
-
| `show?` | `read?` | Record-specific read rules |
|
|
91
|
-
| `new?` | `create?` | Rarely needed |
|
|
92
|
-
| `edit?` | `update?` | Rarely needed |
|
|
93
|
-
| `search?` | `index?` | Search-specific rules |
|
|
94
|
-
|
|
95
|
-
### Custom Actions
|
|
96
|
-
|
|
97
|
-
Define methods matching your action names:
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
def publish?
|
|
101
|
-
update? && record.draft?
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def archive?
|
|
105
|
-
create? && !record.archived?
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def invite_user?
|
|
109
|
-
user.admin?
|
|
110
|
-
end
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Actions are secure by default - undefined methods return `false`.
|
|
114
|
-
|
|
115
|
-
### Bulk Action Authorization
|
|
116
|
-
|
|
117
|
-
Bulk actions (operating on multiple selected records) support **per-record authorization**:
|
|
118
|
-
|
|
119
|
-
```ruby
|
|
120
|
-
def bulk_archive?
|
|
121
|
-
create? && !record.locked? # Per-record check
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def bulk_publish?
|
|
125
|
-
user.admin? || record.author == user
|
|
126
|
-
end
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
**How bulk authorization works:**
|
|
130
|
-
1. Policy method (e.g., `bulk_archive?`) is checked **per record** in the selection
|
|
131
|
-
2. **Backend:** If any selected record fails authorization, the entire request is rejected
|
|
132
|
-
3. **UI:** Only actions that **all** selected records support are shown (intersection)
|
|
133
|
-
4. Records are fetched via `current_authorized_scope` - only accessible records can be selected
|
|
134
|
-
|
|
135
|
-
This provides full per-record authorization while keeping the UI clean - users only see actions they can actually perform on their entire selection.
|
|
136
|
-
|
|
137
|
-
## Attribute Permissions
|
|
138
|
-
|
|
139
|
-
### Core Methods (Must Override for Production)
|
|
140
|
-
|
|
141
|
-
```ruby
|
|
142
|
-
# What users can see (index, show)
|
|
143
|
-
def permitted_attributes_for_read
|
|
144
|
-
%i[title content author published_at created_at]
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# What users can set (create, update)
|
|
148
|
-
def permitted_attributes_for_create
|
|
149
|
-
%i[title content]
|
|
150
|
-
end
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### Derived Methods (Inherit by Default)
|
|
154
|
-
|
|
155
|
-
| Method | Inherits From |
|
|
156
|
-
|--------|---------------|
|
|
157
|
-
| `permitted_attributes_for_update` | `permitted_attributes_for_create` |
|
|
158
|
-
| `permitted_attributes_for_index` | `permitted_attributes_for_read` |
|
|
159
|
-
| `permitted_attributes_for_show` | `permitted_attributes_for_read` |
|
|
160
|
-
| `permitted_attributes_for_new` | `permitted_attributes_for_create` |
|
|
161
|
-
| `permitted_attributes_for_edit` | `permitted_attributes_for_update` |
|
|
162
|
-
|
|
163
|
-
### Per-Action Attributes
|
|
164
|
-
|
|
165
|
-
Show different fields for different views:
|
|
166
|
-
|
|
167
|
-
```ruby
|
|
168
|
-
def permitted_attributes_for_index
|
|
169
|
-
%i[title author created_at] # Minimal for list
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def permitted_attributes_for_read
|
|
173
|
-
%i[title content author tags created_at updated_at] # Full for detail
|
|
174
|
-
end
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
**Key insight:** `permitted_attributes_for_*` controls *which fields appear* on each view (form, show, index). The `column`/`field`/`input`/`display` declarations in the **definition** only control *how those fields render* — they do NOT add or remove fields from the page. If your index page is showing fields you didn't want, override `permitted_attributes_for_index` (it does NOT inherit from `_for_read` automatically when you want a different shape). The same applies to forms: a `field :name` in the definition won't be rendered unless `:name` is in `permitted_attributes_for_create`/`_update`.
|
|
178
|
-
|
|
179
|
-
### Anti-pattern: nested_attributes hashes in permitted_attributes
|
|
180
|
-
|
|
181
|
-
```ruby
|
|
182
|
-
# ❌ DO NOT DO THIS
|
|
183
|
-
def permitted_attributes_for_create
|
|
184
|
-
[
|
|
185
|
-
:name,
|
|
186
|
-
{variants_attributes: [:id, :name, :_destroy]},
|
|
187
|
-
{comments_attributes: [:id, :body, :_destroy]}
|
|
188
|
-
]
|
|
189
|
-
end
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
Plutonium's form pipeline extracts nested params via the **form definition** (`build_form(...).extract_input(...)`), not the policy. Hash entries in `permitted_attributes_for_*` get iterated as field names by the form renderer and end up as literal text inputs with names like `model[{:variants_attributes=>[...]}]`.
|
|
193
|
-
|
|
194
|
-
The correct pattern:
|
|
195
|
-
|
|
196
|
-
```ruby
|
|
197
|
-
# ✅ Policy permits just the association name
|
|
198
|
-
def permitted_attributes_for_create
|
|
199
|
-
[:name, :variants, :comments]
|
|
200
|
-
end
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
```ruby
|
|
204
|
-
# ✅ Definition declares the nested input (this drives both rendering AND param extraction)
|
|
205
|
-
class PostDefinition < ResourceDefinition
|
|
206
|
-
nested_input :variants do |n|
|
|
207
|
-
n.input :name
|
|
208
|
-
n.input :is_default, as: :boolean
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
```ruby
|
|
214
|
-
# ✅ Model declares accepts_nested_attributes_for + inverse_of on the back-reference
|
|
215
|
-
class Post < ApplicationRecord
|
|
216
|
-
has_many :variants, inverse_of: :post, dependent: :destroy
|
|
217
|
-
accepts_nested_attributes_for :variants, allow_destroy: true, reject_if: :all_blank
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
class Variant < ApplicationRecord
|
|
221
|
-
belongs_to :post, inverse_of: :variants # ← required for nested validation
|
|
222
|
-
end
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
See `plutonium-definition` for the full `nested_input` API.
|
|
226
|
-
|
|
227
|
-
### Auto-Detection (Development Only)
|
|
228
|
-
|
|
229
|
-
In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly.
|
|
230
|
-
|
|
231
|
-
## Association Permissions
|
|
232
|
-
|
|
233
|
-
Control which associations can be rendered:
|
|
234
|
-
|
|
235
|
-
```ruby
|
|
236
|
-
def permitted_associations
|
|
237
|
-
%i[comments tags author]
|
|
238
|
-
end
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Used for:
|
|
242
|
-
- Nested forms
|
|
243
|
-
- Related data displays
|
|
244
|
-
- Association fields in tables
|
|
245
|
-
|
|
246
|
-
## Collection Scoping (relation_scope)
|
|
247
|
-
|
|
248
|
-
Filter which records users can see:
|
|
249
|
-
|
|
250
|
-
```ruby
|
|
251
|
-
relation_scope do |relation|
|
|
252
|
-
relation = default_relation_scope(relation)
|
|
253
|
-
user.admin? ? relation : relation.where(author: user)
|
|
254
|
-
end
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
**Always compose with `default_relation_scope(relation)` explicitly** — not `super`. Plutonium enforces this via `verify_default_relation_scope_applied!`. Anything else (a raw `where(organization: ...)`, manual joins) bypasses Plutonium's tenancy handling and will raise.
|
|
258
|
-
|
|
259
|
-
> **For the full rules — why `default_relation_scope` is required, how parent vs entity scoping interact, safe override patterns, `skip_default_relation_scope!`, and how `associated_with` resolution works — see the [plutonium-entity-scoping](../plutonium-entity-scoping/SKILL.md) skill. It is the single source of truth for Plutonium tenant scoping.**
|
|
260
|
-
|
|
261
|
-
## Portal-Specific Policies
|
|
262
|
-
|
|
263
|
-
Override policies per portal:
|
|
264
|
-
|
|
265
|
-
```ruby
|
|
266
|
-
# Base policy
|
|
267
|
-
class PostPolicy < ResourcePolicy
|
|
268
|
-
def create?
|
|
269
|
-
user.present?
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# Admin portal - more permissive
|
|
274
|
-
class AdminPortal::PostPolicy < ::PostPolicy
|
|
275
|
-
include AdminPortal::ResourcePolicy
|
|
276
|
-
|
|
277
|
-
def destroy?
|
|
278
|
-
true # Admins can always delete
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def permitted_attributes_for_create
|
|
282
|
-
%i[title content featured internal_notes] # More fields
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
# Public portal - restricted
|
|
287
|
-
class PublicPortal::PostPolicy < ::PostPolicy
|
|
288
|
-
include PublicPortal::ResourcePolicy
|
|
289
|
-
|
|
290
|
-
def create?
|
|
291
|
-
false # No public creation
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
## Common Patterns
|
|
297
|
-
|
|
298
|
-
### Check Model Capabilities
|
|
299
|
-
|
|
300
|
-
```ruby
|
|
301
|
-
def archive?
|
|
302
|
-
return false unless record.respond_to?(:archived!)
|
|
303
|
-
return false if record.archived?
|
|
304
|
-
|
|
305
|
-
user.admin?
|
|
306
|
-
end
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Prevent Actions on Archived Records
|
|
310
|
-
|
|
311
|
-
```ruby
|
|
312
|
-
def update?
|
|
313
|
-
return false if record.try(:archived?)
|
|
314
|
-
super
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def destroy?
|
|
318
|
-
return false if record.try(:archived?)
|
|
319
|
-
super
|
|
320
|
-
end
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
### Owner-Based Permissions
|
|
324
|
-
|
|
325
|
-
```ruby
|
|
326
|
-
def update?
|
|
327
|
-
record.author == user || user.admin?
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def destroy?
|
|
331
|
-
update? # Same rules as update
|
|
332
|
-
end
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### Role-Based Permissions
|
|
336
|
-
|
|
337
|
-
```ruby
|
|
338
|
-
def create?
|
|
339
|
-
user.admin? || user.editor?
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
def read?
|
|
343
|
-
true # Everyone can read
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
def update?
|
|
347
|
-
return true if user.admin?
|
|
348
|
-
return true if user.editor? && record.author == user
|
|
349
|
-
false
|
|
350
|
-
end
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
### Conditional Attribute Access
|
|
354
|
-
|
|
355
|
-
```ruby
|
|
356
|
-
def permitted_attributes_for_create
|
|
357
|
-
attrs = %i[title content]
|
|
358
|
-
attrs << :featured if user.admin?
|
|
359
|
-
attrs << :author_id if user.admin? # Only admins can set author
|
|
360
|
-
attrs
|
|
361
|
-
end
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
## Authorization Context
|
|
365
|
-
|
|
366
|
-
Policies have access to:
|
|
367
|
-
|
|
368
|
-
```ruby
|
|
369
|
-
user # Current user (required)
|
|
370
|
-
record # The resource being authorized
|
|
371
|
-
entity_scope # Current scoped entity (for multi-tenancy)
|
|
372
|
-
parent # Parent record for nested resources (nil if not nested)
|
|
373
|
-
parent_association # Association name on parent (e.g., :comments)
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
### Nested Resource Context
|
|
377
|
-
|
|
378
|
-
For nested resources (e.g., `/posts/123/nested_comments`), the policy receives:
|
|
379
|
-
|
|
380
|
-
```ruby
|
|
381
|
-
class CommentPolicy < ResourcePolicy
|
|
382
|
-
def create?
|
|
383
|
-
# parent is the Post instance
|
|
384
|
-
# parent_association is :comments
|
|
385
|
-
parent.present? && user.can_comment_on?(parent)
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
relation_scope do |relation|
|
|
389
|
-
# super() uses parent and parent_association for scoping
|
|
390
|
-
relation = super(relation)
|
|
391
|
-
relation
|
|
392
|
-
end
|
|
393
|
-
end
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
### Custom Context
|
|
397
|
-
|
|
398
|
-
Add custom context in controllers:
|
|
399
|
-
|
|
400
|
-
```ruby
|
|
401
|
-
# In policy
|
|
402
|
-
class PostPolicy < ResourcePolicy
|
|
403
|
-
authorize :department, allow_nil: true
|
|
404
|
-
|
|
405
|
-
def create?
|
|
406
|
-
department&.allows_posting?
|
|
407
|
-
end
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
# In controller
|
|
411
|
-
class PostsController < ResourceController
|
|
412
|
-
authorize :department, through: :current_department
|
|
413
|
-
|
|
414
|
-
private
|
|
415
|
-
|
|
416
|
-
def current_department
|
|
417
|
-
current_user.department
|
|
418
|
-
end
|
|
419
|
-
end
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
## Controller Integration
|
|
423
|
-
|
|
424
|
-
Built-in CRUD actions automatically:
|
|
425
|
-
- Call `authorize_current!` at the start of each action
|
|
426
|
-
- Apply `relation_scope` for index/listings
|
|
427
|
-
- Filter params through `permitted_attributes`
|
|
428
|
-
|
|
429
|
-
After-action callbacks verify authorization was performed - if you add custom actions, you must call `authorize_current!` yourself or skip verification.
|
|
430
|
-
|
|
431
|
-
### Skip Verification (When Needed)
|
|
432
|
-
|
|
433
|
-
```ruby
|
|
434
|
-
class PostsController < ResourceController
|
|
435
|
-
skip_verify_authorize_current only: [:custom_action]
|
|
436
|
-
|
|
437
|
-
def custom_action
|
|
438
|
-
# Handle authorization manually
|
|
439
|
-
end
|
|
440
|
-
end
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
## Best Practices
|
|
444
|
-
|
|
445
|
-
1. **Always override `create?` and `read?`** - They default to `false`
|
|
446
|
-
2. **Define attributes explicitly** - Auto-detection only works in development
|
|
447
|
-
3. **Call `default_relation_scope(relation)` in `relation_scope`** - Preserves parent/entity scoping (do not rely on `super` from inside the block)
|
|
448
|
-
4. **Use derived methods** - Let `update?` inherit from `create?` when appropriate
|
|
449
|
-
5. **Keep policies focused** - Authorization logic only, no business logic
|
|
450
|
-
6. **Test edge cases** - Archived records, nil associations, role combinations
|
|
451
|
-
|
|
452
|
-
## Related Skills
|
|
453
|
-
|
|
454
|
-
- `plutonium` - How policies fit in the resource architecture
|
|
455
|
-
- `plutonium-definition` - Actions that need policy methods
|
|
456
|
-
- `plutonium-controller` - How controllers use policies
|