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
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# Policy
|
|
2
|
+
|
|
3
|
+
Authorization for resources. Built on [ActionPolicy](https://actionpolicy.evilmartians.io/). Plutonium adds:
|
|
4
|
+
|
|
5
|
+
- Attribute permissions (`permitted_attributes_for_*`)
|
|
6
|
+
- Association permissions (`permitted_associations`)
|
|
7
|
+
- Automatic entity scoping via `default_relation_scope`
|
|
8
|
+
- Derived action methods (`update?` inherits from `create?`, etc.)
|
|
9
|
+
|
|
10
|
+
## 🚨 Critical
|
|
11
|
+
|
|
12
|
+
- **`create?` and `read?` default to `false`.** You MUST override them explicitly. Everything else (`update?`, `destroy?`, `index?`, `show?`, …) derives from one of those.
|
|
13
|
+
- **`permitted_attributes_for_*` must be explicit in production.** Dev auto-detects; production raises.
|
|
14
|
+
- **`relation_scope` must call `default_relation_scope(relation)` explicitly** — never `super`. Bypassing it triggers `verify_default_relation_scope_applied!`.
|
|
15
|
+
- **For `has_cents` fields, use the virtual name** (`:price`), NEVER `:price_cents`.
|
|
16
|
+
- **Don't put `*_attributes` hashes in `permitted_attributes_for_*`.** Nested forms are extracted from the form definition, not the policy. List the association name (`:variants`) and the `nested_input` in the definition handles the rest.
|
|
17
|
+
- **Custom action ⇒ policy method.** `action :publish` needs `def publish?`. Undefined methods return `false` → action silently disappears.
|
|
18
|
+
- **Index has no `record`.** Record-dependent `_for_read` overrides need an explicit `_for_index` too (see [below](#index-has-no-record)).
|
|
19
|
+
|
|
20
|
+
## Base class
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# app/policies/resource_policy.rb — installed once
|
|
24
|
+
class ResourcePolicy < Plutonium::Resource::Policy
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# app/policies/post_policy.rb — per resource, generated
|
|
28
|
+
class PostPolicy < ResourcePolicy
|
|
29
|
+
def create? = user.present?
|
|
30
|
+
def read? = true
|
|
31
|
+
|
|
32
|
+
def permitted_attributes_for_create
|
|
33
|
+
%i[title content]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def permitted_attributes_for_read
|
|
37
|
+
%i[title content author created_at]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Authorization context
|
|
43
|
+
|
|
44
|
+
Inside a policy:
|
|
45
|
+
|
|
46
|
+
| Variable | Description |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `user` | Current authenticated user (required) |
|
|
49
|
+
| `record` | Resource being authorized |
|
|
50
|
+
| `entity_scope` | Current scoped entity (multi-tenancy) |
|
|
51
|
+
| `parent` | Parent record for nested resources (nil otherwise) |
|
|
52
|
+
| `parent_association` | Association name on parent (e.g. `:comments`) |
|
|
53
|
+
|
|
54
|
+
## Action permissions
|
|
55
|
+
|
|
56
|
+
### Must override
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
def create? # default: false
|
|
60
|
+
user.present?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def read? # default: false
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Derived (inherit automatically)
|
|
69
|
+
|
|
70
|
+
| Method | Inherits from | Override when |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `update?` | `create?` | Different update rules |
|
|
73
|
+
| `destroy?` | `create?` | Different delete rules |
|
|
74
|
+
| `index?` | `read?` | Custom listing rules |
|
|
75
|
+
| `show?` | `read?` | Record-specific read rules |
|
|
76
|
+
| `new?` | `create?` | Rarely needed |
|
|
77
|
+
| `edit?` | `update?` | Rarely needed |
|
|
78
|
+
| `search?` | `index?` | Search-specific rules |
|
|
79
|
+
| `typeahead?` | `index?` | Autocomplete on inputs/filters targeting this resource |
|
|
80
|
+
|
|
81
|
+
### Custom actions
|
|
82
|
+
|
|
83
|
+
Define `def <action>?` matching the definition's `action :<action>`. Undefined methods return `false`:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
def publish? = update? && record.draft?
|
|
87
|
+
def archive? = create? && !record.archived?
|
|
88
|
+
def invite_user? = user.admin?
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Bulk actions — per-record authorization
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
def bulk_archive?
|
|
95
|
+
create? && !record.locked? # checked per record in the selection
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
How it works:
|
|
100
|
+
|
|
101
|
+
- Policy is checked **per record** in the selected set.
|
|
102
|
+
- **Backend:** if any record fails, the entire request is rejected.
|
|
103
|
+
- **UI:** only actions ALL selected records support are shown (intersection).
|
|
104
|
+
- Records come from `current_authorized_scope` — users can only select records they're allowed to access.
|
|
105
|
+
|
|
106
|
+
## Attribute permissions
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# Must override for production
|
|
110
|
+
def permitted_attributes_for_read
|
|
111
|
+
%i[title content author published_at created_at]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def permitted_attributes_for_create
|
|
115
|
+
%i[title content]
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Derived
|
|
120
|
+
|
|
121
|
+
| Method | Inherits from |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `permitted_attributes_for_update` | `permitted_attributes_for_create` |
|
|
124
|
+
| `permitted_attributes_for_index` | `permitted_attributes_for_read` |
|
|
125
|
+
| `permitted_attributes_for_show` | `permitted_attributes_for_read` |
|
|
126
|
+
| `permitted_attributes_for_new` | `permitted_attributes_for_create` |
|
|
127
|
+
| `permitted_attributes_for_edit` | `permitted_attributes_for_update` |
|
|
128
|
+
|
|
129
|
+
### Per-action override
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
def permitted_attributes_for_index
|
|
133
|
+
%i[title author created_at] # minimal for the table
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def permitted_attributes_for_read
|
|
137
|
+
%i[title content author tags created_at] # fuller for the show page
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Index has no `record`
|
|
142
|
+
|
|
143
|
+
🚨 `permitted_attributes_for_index` is evaluated at the **collection level** — `record` is `nil`. `permitted_attributes_for_show` (and `_for_read`) ARE evaluated per record.
|
|
144
|
+
|
|
145
|
+
If you write a record-dependent `_for_read`:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
def permitted_attributes_for_read
|
|
149
|
+
attrs = %i[title content]
|
|
150
|
+
attrs << :archive_reason if record.archived? # uses record
|
|
151
|
+
attrs
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
…you MUST also define an explicit `permitted_attributes_for_index` — otherwise inheritance kicks in, runs the `_for_read` body during the table render, and `record.archived?` blows up on `NoMethodError: undefined method 'archived?' for nil`.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
def permitted_attributes_for_index
|
|
159
|
+
%i[title content] # no record-dependent fields
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Same rule for `permitted_attributes_for_create` vs `_for_new` (new has no persisted record).
|
|
164
|
+
|
|
165
|
+
### Conditional attribute access
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
def permitted_attributes_for_create
|
|
169
|
+
attrs = %i[title content]
|
|
170
|
+
attrs += %i[featured author_id] if user.admin?
|
|
171
|
+
attrs
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def permitted_attributes_for_update
|
|
175
|
+
case record.status
|
|
176
|
+
when 'draft' then %i[title content category_id]
|
|
177
|
+
when 'published' then %i[content] # only the body once published
|
|
178
|
+
else []
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Definition declares HOW, policy declares WHAT
|
|
184
|
+
|
|
185
|
+
`permitted_attributes_for_*` controls **which fields appear** on a view. The definition's `field`/`input`/`display`/`column` declarations only control **how** they render. A `field :name` in the definition does nothing unless `:name` is also in the relevant `permitted_attributes_for_*`.
|
|
186
|
+
|
|
187
|
+
Common mistake: adding a definition declaration and wondering why the field doesn't show — check the policy.
|
|
188
|
+
|
|
189
|
+
### Anti-pattern: nested-attributes hashes
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# ❌ NEVER
|
|
193
|
+
def permitted_attributes_for_create
|
|
194
|
+
[
|
|
195
|
+
:name,
|
|
196
|
+
{variants_attributes: [:id, :name, :_destroy]},
|
|
197
|
+
{comments_attributes: [:id, :body, :_destroy]}
|
|
198
|
+
]
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Plutonium extracts nested params via the form definition, not the policy. Hash entries here get iterated as field names by the form renderer and render as literal text inputs with names like `model[{:variants_attributes=>[...]}]`.
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
# ✅ Policy permits just the association name
|
|
206
|
+
def permitted_attributes_for_create
|
|
207
|
+
[:name, :variants]
|
|
208
|
+
end
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`nested_input :variants` in the definition handles the rest. See [Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs).
|
|
212
|
+
|
|
213
|
+
### Auto-detection (dev only)
|
|
214
|
+
|
|
215
|
+
In development, undefined `permitted_attributes_for_*` methods auto-detect from the model. **Production raises** with a clear error:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
🚨 Resource field auto-detection: PostPolicy#permitted_attributes_for_create
|
|
219
|
+
Auto-detected resource fields result in security holes and will fail outside of development.
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Always declare explicitly before deploying.
|
|
223
|
+
|
|
224
|
+
## Association permissions
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
def permitted_associations
|
|
228
|
+
%i[comments tags author]
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Declares which associations get their own **tab on the show page**. When non-empty, the show page renders a tablist: a "Details" tab (the main field card + metadata aside) plus one tab per association — each lazy-loaded via a frame navigator panel pointing at the associated `has_many` collection, `has_one` record, or `belongs_to` target. When empty, the show page renders without tabs.
|
|
233
|
+
|
|
234
|
+
Each named association must:
|
|
235
|
+
|
|
236
|
+
- Exist on the model (raises `ArgumentError: unknown association ...` otherwise).
|
|
237
|
+
- Point to a class that's itself a registered Plutonium resource (raises `... is not a registered resource` otherwise).
|
|
238
|
+
|
|
239
|
+
This is **NOT** the same as:
|
|
240
|
+
|
|
241
|
+
- **Nested forms** — declared with `nested_input :variants` in the definition, requires `accepts_nested_attributes_for` on the model. See [Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs).
|
|
242
|
+
- **Association fields on tables / show details** — controlled by `permitted_attributes_for_index` / `_for_show` listing the association name.
|
|
243
|
+
|
|
244
|
+
## Collection scoping (`relation_scope`)
|
|
245
|
+
|
|
246
|
+
Filter which records the user can see.
|
|
247
|
+
|
|
248
|
+
### Always compose with `default_relation_scope`
|
|
249
|
+
|
|
250
|
+
🚨 `relation_scope` MUST call `default_relation_scope(relation)` explicitly. Never `super` — the semantics depend on how ActionPolicy's DSL registered the scope. Plutonium enforces this at runtime via `verify_default_relation_scope_applied!`.
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
# ✅ Best — don't override at all. The inherited scope already calls default_relation_scope.
|
|
254
|
+
|
|
255
|
+
# ✅ Extra filters on top
|
|
256
|
+
relation_scope do |relation|
|
|
257
|
+
default_relation_scope(relation).where(archived: false)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# ✅ Role-based
|
|
261
|
+
relation_scope do |relation|
|
|
262
|
+
relation = default_relation_scope(relation)
|
|
263
|
+
user.admin? ? relation : relation.where(author: user)
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Wrong patterns
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
# ❌ Manually filtering by entity — bypasses default_relation_scope
|
|
271
|
+
relation_scope { |r| r.where(organization: current_scoped_entity) }
|
|
272
|
+
|
|
273
|
+
# ❌ Manual joins — same problem
|
|
274
|
+
relation_scope { |r| r.joins(:project).where(projects: {organization_id: current_scoped_entity.id}) }
|
|
275
|
+
|
|
276
|
+
# ❌ Missing default_relation_scope entirely — raises at runtime
|
|
277
|
+
relation_scope { |r| r.where(published: true) }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### What `default_relation_scope` does
|
|
281
|
+
|
|
282
|
+
1. If a **parent** is present (nested resource), scopes via the parent association.
|
|
283
|
+
2. Otherwise, applies `relation.associated_with(entity_scope)` for multi-tenancy.
|
|
284
|
+
|
|
285
|
+
Parent scoping takes precedence over entity scoping — the parent was already authorized and entity-scoped during its own authorization, so double-scoping isn't needed.
|
|
286
|
+
|
|
287
|
+
Full mechanics in [Tenancy › Entity scoping](/reference/tenancy/entity-scoping).
|
|
288
|
+
|
|
289
|
+
### Intentionally skipping
|
|
290
|
+
|
|
291
|
+
Rare. Use `skip_default_relation_scope!` explicitly — never silently bypass:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
relation_scope do |relation|
|
|
295
|
+
skip_default_relation_scope!
|
|
296
|
+
relation
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Before reaching for this, consider a separate, unscoped portal.
|
|
301
|
+
|
|
302
|
+
## Portal-specific policies
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
class PostPolicy < ResourcePolicy
|
|
306
|
+
def create? = user.present?
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Admin — more permissive
|
|
310
|
+
class AdminPortal::PostPolicy < ::PostPolicy
|
|
311
|
+
include AdminPortal::ResourcePolicy
|
|
312
|
+
|
|
313
|
+
def destroy? = true
|
|
314
|
+
def permitted_attributes_for_create = %i[title content featured internal_notes]
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Public — read-only
|
|
318
|
+
class PublicPortal::PostPolicy < ::PostPolicy
|
|
319
|
+
include PublicPortal::ResourcePolicy
|
|
320
|
+
def create? = false
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Custom authorization context
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
# Policy
|
|
328
|
+
class PostPolicy < ResourcePolicy
|
|
329
|
+
authorize :department, allow_nil: true
|
|
330
|
+
|
|
331
|
+
def create? = department&.allows_posting?
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Controller
|
|
335
|
+
class PostsController < ResourceController
|
|
336
|
+
authorize :department, through: :current_department
|
|
337
|
+
|
|
338
|
+
private
|
|
339
|
+
def current_department = current_user.department
|
|
340
|
+
end
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Authorization errors
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
# Failed authorization raises ActionPolicy::Unauthorized
|
|
347
|
+
|
|
348
|
+
# Handle globally
|
|
349
|
+
rescue_from ActionPolicy::Unauthorized do
|
|
350
|
+
redirect_to root_path, alert: "Not authorized"
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Common patterns
|
|
355
|
+
|
|
356
|
+
### Block archived records
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
def update? = !record.try(:archived?) && super
|
|
360
|
+
def destroy? = !record.try(:archived?) && super
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Owner-based
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
def update? = record.author == user || user.admin?
|
|
367
|
+
def destroy? = update?
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Role-based
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
def create? = user.admin? || user.editor?
|
|
374
|
+
|
|
375
|
+
def update?
|
|
376
|
+
return true if user.admin?
|
|
377
|
+
user.editor? && record.author == user
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Status-based
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
def update?
|
|
385
|
+
return false if record.archived?
|
|
386
|
+
owner? || admin?
|
|
387
|
+
end
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Time-based
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
def update?
|
|
394
|
+
return false if record.created_at < 24.hours.ago
|
|
395
|
+
owner?
|
|
396
|
+
end
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Debugging
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
# Console
|
|
403
|
+
user = User.find(1)
|
|
404
|
+
post = Post.find(1)
|
|
405
|
+
|
|
406
|
+
policy = PostPolicy.new(post, user: user)
|
|
407
|
+
policy.update?
|
|
408
|
+
policy.permitted_attributes_for_update
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Related
|
|
412
|
+
|
|
413
|
+
- [Controllers](./controllers) — call policies via `authorize_current!` and `authorized_resource_scope`
|
|
414
|
+
- [Interactions](./interactions) — custom actions whose policy methods you define
|
|
415
|
+
- [Resource › Actions](/reference/resource/actions) — registering actions that need policy methods
|
|
416
|
+
- [Tenancy › Entity scoping](/reference/tenancy/entity-scoping) — `default_relation_scope`, three model shapes, custom scopes
|
|
417
|
+
- [ActionPolicy docs](https://actionpolicy.evilmartians.io/) — the underlying library
|
data/docs/reference/index.md
CHANGED
|
@@ -1,49 +1,56 @@
|
|
|
1
|
-
# Reference
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
### [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
### [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
### [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
### [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
### [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
1
|
+
# Reference
|
|
2
|
+
|
|
3
|
+
Concept-by-concept API documentation. For task-oriented walkthroughs, see [Guides](/guides/).
|
|
4
|
+
|
|
5
|
+
## The seven areas
|
|
6
|
+
|
|
7
|
+
### [App](/reference/app/)
|
|
8
|
+
Installation, packages (feature + portal), portal engines, mounting, route registration (including singular and custom routes), connecting resources via `pu:res:conn`, full generator catalog.
|
|
9
|
+
|
|
10
|
+
### [Resource](/reference/resource/)
|
|
11
|
+
The four-layer resource — model, definition, query, actions. `pu:res:scaffold` field-type syntax, `has_cents`, SGID, URL routing, definition DSL (fields, inputs, displays, columns), page chrome, metadata panel, index views (table & grid), search, filters, scopes, sorting, custom + bulk actions.
|
|
12
|
+
|
|
13
|
+
### [Behavior](/reference/behavior/)
|
|
14
|
+
Controllers, policies, interactions. Controller hooks (redirect, params, presentation), policy action methods and `permitted_attributes_for_*`, `permitted_associations`, `relation_scope`, interaction structure, outcomes, chaining, URL generation.
|
|
15
|
+
|
|
16
|
+
### [UI](/reference/ui/)
|
|
17
|
+
Pages, forms, displays, tables, components, layouts, assets. Custom page classes, form field builders, association inputs (typeahead + inline `+`), built-in component kit, custom Phlex components, the shell, design tokens, `.pu-*` component classes, Phlexi themes.
|
|
18
|
+
|
|
19
|
+
### [Auth](/reference/auth/)
|
|
20
|
+
Rodauth installation, account types (basic / admin / SaaS), profile resource with the SecuritySection component.
|
|
21
|
+
|
|
22
|
+
### [Tenancy](/reference/tenancy/)
|
|
23
|
+
Multi-tenant entity scoping (`associated_with`, `default_relation_scope`, three model shapes), nested resources (parent/child routes, scoping), user invitations.
|
|
24
|
+
|
|
25
|
+
### [Testing](/reference/testing/)
|
|
26
|
+
The `Plutonium::Testing::*` concerns — CRUD, policy matrix, definition smoke tests, model concerns, nested resources, portal access, interaction outcomes.
|
|
27
|
+
|
|
28
|
+
## Quick reference
|
|
29
|
+
|
|
30
|
+
| I need to… | See |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Install Plutonium | [App › Index](/reference/app/) |
|
|
33
|
+
| Run a generator | [App › Generators](/reference/app/generators) |
|
|
34
|
+
| Create a portal | [App › Portals](/reference/app/portals) |
|
|
35
|
+
| Scaffold a resource | [App › Generators › `pu:res:scaffold`](/reference/app/generators#pu-res-scaffold) |
|
|
36
|
+
| Configure form fields | [Resource › Definition](/reference/resource/definition) |
|
|
37
|
+
| Add search / filters | [Resource › Query](/reference/resource/query) |
|
|
38
|
+
| Add custom buttons / bulk actions | [Resource › Actions](/reference/resource/actions) |
|
|
39
|
+
| Override CRUD redirects / params | [Behavior › Controllers](/reference/behavior/controllers) |
|
|
40
|
+
| Control who can see what | [Behavior › Policies](/reference/behavior/policies) |
|
|
41
|
+
| Write business logic | [Behavior › Interactions](/reference/behavior/interactions) |
|
|
42
|
+
| Customize a page | [UI › Pages](/reference/ui/pages) |
|
|
43
|
+
| Customize a form | [UI › Forms](/reference/ui/forms) |
|
|
44
|
+
| Style the UI | [UI › Assets](/reference/ui/assets) |
|
|
45
|
+
| Set up Rodauth | [Auth › Accounts](/reference/auth/accounts) |
|
|
46
|
+
| Add a profile page | [Auth › Profile](/reference/auth/profile) |
|
|
47
|
+
| Scope to a tenant | [Tenancy › Entity scoping](/reference/tenancy/entity-scoping) |
|
|
48
|
+
| Wire user invitations | [Tenancy › Invites](/reference/tenancy/invites) |
|
|
49
|
+
| Test a resource | [Testing](/reference/testing/) |
|
|
50
|
+
|
|
51
|
+
## Reading this reference
|
|
52
|
+
|
|
53
|
+
- **🚨 Critical blocks** at the top of each page surface the "you'll regret this" rules. Skim them even if you're skimming the rest.
|
|
54
|
+
- **Option / DSL tables** are designed for scanning — find your option name without reading prose.
|
|
55
|
+
- **Cross-references** use VitePress relative paths. If a link points somewhere that doesn't exist yet, it's a known gap.
|
|
56
|
+
- **Concrete decision rules** ("use X when…, Y when…") sit alongside the option references. Reach for them when in doubt.
|