plutonium 0.33.1 → 0.34.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/# Plutonium: The pre-alpha demo.md +4 -2
- data/.claude/skills/assets/SKILL.md +416 -0
- data/.claude/skills/connect-resource/SKILL.md +112 -0
- data/.claude/skills/controller/SKILL.md +302 -0
- data/.claude/skills/create-resource/SKILL.md +240 -0
- data/.claude/skills/definition/SKILL.md +218 -0
- data/.claude/skills/definition-actions/SKILL.md +386 -0
- data/.claude/skills/definition-fields/SKILL.md +474 -0
- data/.claude/skills/definition-query/SKILL.md +334 -0
- data/.claude/skills/forms/SKILL.md +439 -0
- data/.claude/skills/installation/SKILL.md +300 -0
- data/.claude/skills/interaction/SKILL.md +382 -0
- data/.claude/skills/model/SKILL.md +267 -0
- data/.claude/skills/model-features/SKILL.md +286 -0
- data/.claude/skills/nested-resources/SKILL.md +274 -0
- data/.claude/skills/package/SKILL.md +191 -0
- data/.claude/skills/policy/SKILL.md +352 -0
- data/.claude/skills/portal/SKILL.md +400 -0
- data/.claude/skills/resource/SKILL.md +281 -0
- data/.claude/skills/rodauth/SKILL.md +452 -0
- data/.claude/skills/views/SKILL.md +563 -0
- data/Appraisals +46 -4
- data/CHANGELOG.md +32 -1
- data/app/assets/plutonium.css +2 -2
- data/config/brakeman.ignore +239 -0
- data/config/initializers/action_policy.rb +1 -1
- data/docs/.vitepress/config.ts +132 -47
- data/docs/concepts/architecture.md +226 -0
- data/docs/concepts/auto-detection.md +254 -0
- data/docs/concepts/index.md +61 -0
- data/docs/concepts/packages-portals.md +304 -0
- data/docs/concepts/resources.md +224 -0
- data/docs/cookbook/blog.md +412 -0
- data/docs/cookbook/index.md +289 -0
- data/docs/cookbook/saas.md +481 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +146 -0
- data/docs/getting-started/tutorial/01-setup.md +118 -0
- data/docs/getting-started/tutorial/02-first-resource.md +180 -0
- data/docs/getting-started/tutorial/03-authentication.md +246 -0
- data/docs/getting-started/tutorial/04-authorization.md +170 -0
- data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
- data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
- data/docs/getting-started/tutorial/index.md +64 -0
- data/docs/guides/adding-resources.md +420 -0
- data/docs/guides/authentication.md +551 -0
- data/docs/guides/authorization.md +468 -0
- data/docs/guides/creating-packages.md +380 -0
- data/docs/guides/custom-actions.md +523 -0
- data/docs/guides/index.md +45 -0
- data/docs/guides/multi-tenancy.md +302 -0
- data/docs/guides/nested-resources.md +411 -0
- data/docs/guides/search-filtering.md +266 -0
- data/docs/guides/theming.md +321 -0
- data/docs/index.md +67 -26
- data/docs/public/CLAUDE.md +64 -21
- data/docs/reference/assets/index.md +496 -0
- data/docs/reference/controller/index.md +363 -0
- data/docs/reference/definition/actions.md +400 -0
- data/docs/reference/definition/fields.md +350 -0
- data/docs/reference/definition/index.md +252 -0
- data/docs/reference/definition/query.md +342 -0
- data/docs/reference/generators/index.md +469 -0
- data/docs/reference/index.md +49 -0
- data/docs/reference/interaction/index.md +445 -0
- data/docs/reference/model/features.md +248 -0
- data/docs/reference/model/index.md +219 -0
- data/docs/reference/policy/index.md +385 -0
- data/docs/reference/portal/index.md +382 -0
- data/docs/reference/views/forms.md +396 -0
- data/docs/reference/views/index.md +479 -0
- data/gemfiles/rails_7.gemfile +9 -2
- data/gemfiles/rails_7.gemfile.lock +146 -111
- data/gemfiles/rails_8.0.gemfile +20 -0
- data/gemfiles/rails_8.0.gemfile.lock +417 -0
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/gemfiles/rails_8.1.gemfile.lock +419 -0
- data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
- data/lib/generators/pu/pkg/portal/USAGE +65 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
- data/lib/generators/pu/res/conn/USAGE +71 -0
- data/lib/generators/pu/res/model/USAGE +106 -110
- data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
- data/lib/generators/pu/res/scaffold/USAGE +85 -0
- data/lib/generators/pu/rodauth/install_generator.rb +2 -6
- data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
- data/lib/generators/pu/skills/sync/USAGE +14 -0
- data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
- data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
- data/lib/plutonium/core/controller.rb +2 -2
- data/lib/plutonium/interaction/base.rb +1 -0
- data/lib/plutonium/package/engine.rb +2 -2
- data/lib/plutonium/query/adhoc_block.rb +6 -2
- data/lib/plutonium/query/model_scope.rb +1 -1
- data/lib/plutonium/railtie.rb +4 -0
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
- data/lib/plutonium/resource/query_object.rb +38 -8
- data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
- data/lib/plutonium/version.rb +1 -1
- data/lib/tasks/release.rake +19 -4
- data/package.json +1 -1
- metadata +76 -39
- data/brakeman.ignore +0 -28
- data/docs/api-examples.md +0 -49
- data/docs/guide/claude-code-guide.md +0 -74
- data/docs/guide/deep-dive/authorization.md +0 -189
- data/docs/guide/deep-dive/multitenancy.md +0 -256
- data/docs/guide/deep-dive/resources.md +0 -390
- data/docs/guide/getting-started/01-installation.md +0 -165
- data/docs/guide/index.md +0 -28
- data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
- data/docs/guide/introduction/02-core-concepts.md +0 -440
- data/docs/guide/tutorial/01-project-setup.md +0 -75
- data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
- data/docs/guide/tutorial/03-defining-resources.md +0 -90
- data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
- data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
- data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
- data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
- data/docs/markdown-examples.md +0 -85
- data/docs/modules/action.md +0 -244
- data/docs/modules/authentication.md +0 -236
- data/docs/modules/configuration.md +0 -599
- data/docs/modules/controller.md +0 -443
- data/docs/modules/core.md +0 -316
- data/docs/modules/definition.md +0 -1308
- data/docs/modules/display.md +0 -759
- data/docs/modules/form.md +0 -495
- data/docs/modules/generator.md +0 -400
- data/docs/modules/index.md +0 -167
- data/docs/modules/interaction.md +0 -642
- data/docs/modules/package.md +0 -151
- data/docs/modules/policy.md +0 -176
- data/docs/modules/portal.md +0 -710
- data/docs/modules/query.md +0 -297
- data/docs/modules/resource_record.md +0 -618
- data/docs/modules/routing.md +0 -690
- data/docs/modules/table.md +0 -301
- data/docs/modules/ui.md +0 -631
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# Authorization
|
|
2
|
+
|
|
3
|
+
This guide covers implementing authorization policies to control access.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Plutonium authorization is built on [ActionPolicy](https://actionpolicy.evilmartians.io/) and works at three levels:
|
|
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?
|
|
12
|
+
|
|
13
|
+
## Policy Structure
|
|
14
|
+
|
|
15
|
+
Policies inherit from a base `ResourcePolicy` class:
|
|
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
|
|
38
|
+
|
|
39
|
+
def update?
|
|
40
|
+
owner?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def destroy?
|
|
44
|
+
owner? || user.admin?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def permitted_attributes_for_create
|
|
48
|
+
%i[title content]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def permitted_attributes_for_read
|
|
52
|
+
%i[title content author_id created_at updated_at]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def permitted_associations
|
|
56
|
+
%i[comments tags]
|
|
57
|
+
end
|
|
58
|
+
|
|
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) |
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
def update?
|
|
79
|
+
user # => Current user
|
|
80
|
+
record # => The specific Post instance
|
|
81
|
+
entity_scope # => Current parent/tenant entity
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Action Permissions
|
|
86
|
+
|
|
87
|
+
### Core Actions (Must Override)
|
|
88
|
+
|
|
89
|
+
The base `Plutonium::Resource::Policy` defaults `create?` and `read?` to `false`. You must override these:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
def create? # Default: false
|
|
93
|
+
user.present?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def read? # Default: false
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
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 |
|
|
114
|
+
|
|
115
|
+
```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
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Custom Actions
|
|
125
|
+
|
|
126
|
+
Define methods matching your action names:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
def publish?
|
|
130
|
+
update? && record.draft?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def archive?
|
|
134
|
+
update? && !record.archived?
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Actions are secure by default - undefined methods return `false`.
|
|
139
|
+
|
|
140
|
+
## Attribute Permissions
|
|
141
|
+
|
|
142
|
+
### Core Methods (Must Override for Production)
|
|
143
|
+
|
|
144
|
+
```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]
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
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` |
|
|
165
|
+
|
|
166
|
+
### Per-Action Attributes
|
|
167
|
+
|
|
168
|
+
Show different fields for different views:
|
|
169
|
+
|
|
170
|
+
```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
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Conditional Attributes
|
|
181
|
+
|
|
182
|
+
```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
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
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:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
def permitted_associations
|
|
201
|
+
%i[comments tags author]
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
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`:
|
|
210
|
+
|
|
211
|
+
```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
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### With Entity Scoping
|
|
226
|
+
|
|
227
|
+
Call `super` to preserve automatic entity scoping for multi-tenancy:
|
|
228
|
+
|
|
229
|
+
```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
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Portal-Specific Policies
|
|
242
|
+
|
|
243
|
+
Override policies for specific portals:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
# packages/admin_portal/app/policies/admin_portal/post_policy.rb
|
|
247
|
+
class AdminPortal::PostPolicy < ::PostPolicy
|
|
248
|
+
include AdminPortal::ResourcePolicy
|
|
249
|
+
|
|
250
|
+
# Admins can do everything
|
|
251
|
+
def destroy?
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def permitted_attributes_for_create
|
|
256
|
+
%i[title content featured internal_notes] # More fields
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
relation_scope do |relation|
|
|
260
|
+
relation # No restrictions
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
For restricted portals:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
# packages/public_portal/app/policies/public_portal/post_policy.rb
|
|
269
|
+
class PublicPortal::PostPolicy < ::PostPolicy
|
|
270
|
+
include PublicPortal::ResourcePolicy
|
|
271
|
+
|
|
272
|
+
def create?
|
|
273
|
+
false # No public creation
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
relation_scope do |relation|
|
|
277
|
+
relation.where(published: true) # Only published
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Plutonium automatically uses portal-specific policies when available.
|
|
283
|
+
|
|
284
|
+
## Policy Helpers
|
|
285
|
+
|
|
286
|
+
Extract common logic into concerns:
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
# app/policies/concerns/ownership.rb
|
|
290
|
+
module Ownership
|
|
291
|
+
extend ActiveSupport::Concern
|
|
292
|
+
|
|
293
|
+
def owner?
|
|
294
|
+
return false unless record.respond_to?(:user_id)
|
|
295
|
+
record.user_id == user.id
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Use in policies
|
|
300
|
+
class PostPolicy < ResourcePolicy
|
|
301
|
+
include Ownership
|
|
302
|
+
|
|
303
|
+
def update?
|
|
304
|
+
owner? || user.admin?
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Testing Policies
|
|
310
|
+
|
|
311
|
+
### Manual Testing
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
rails runner "
|
|
315
|
+
user = User.first
|
|
316
|
+
post = Post.first
|
|
317
|
+
policy = PostPolicy.new(user: user, record: post)
|
|
318
|
+
|
|
319
|
+
puts 'Can read: ' + policy.read?.to_s
|
|
320
|
+
puts 'Can update: ' + policy.update?.to_s
|
|
321
|
+
"
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### RSpec with ActionPolicy
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
# spec/policies/post_policy_spec.rb
|
|
328
|
+
RSpec.describe PostPolicy, type: :policy do
|
|
329
|
+
let(:user) { create(:user) }
|
|
330
|
+
let(:other_user) { create(:user) }
|
|
331
|
+
|
|
332
|
+
describe '#update?' do
|
|
333
|
+
context 'when user owns the post' do
|
|
334
|
+
let(:record) { create(:post, user: user) }
|
|
335
|
+
|
|
336
|
+
it { is_expected.to be_allowed_to(:update?) }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
context 'when user does not own the post' do
|
|
340
|
+
let(:record) { create(:post, user: other_user) }
|
|
341
|
+
|
|
342
|
+
it { is_expected.not_to be_allowed_to(:update?) }
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Common Patterns
|
|
349
|
+
|
|
350
|
+
### Role-Based Access
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
class PostPolicy < ResourcePolicy
|
|
354
|
+
def destroy?
|
|
355
|
+
case user.role
|
|
356
|
+
when 'admin'
|
|
357
|
+
true
|
|
358
|
+
when 'editor'
|
|
359
|
+
record.draft?
|
|
360
|
+
when 'author'
|
|
361
|
+
owner? && record.draft?
|
|
362
|
+
else
|
|
363
|
+
false
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Time-Based Permissions
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
def update?
|
|
373
|
+
owner? && record.created_at > 24.hours.ago
|
|
374
|
+
end
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Status-Based Permissions
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
def update?
|
|
381
|
+
return false if record.archived?
|
|
382
|
+
return true if user.admin?
|
|
383
|
+
owner? && record.draft?
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Check Model Capabilities
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
def archive?
|
|
391
|
+
return false unless record.respond_to?(:archived!)
|
|
392
|
+
return false if record.archived?
|
|
393
|
+
update?
|
|
394
|
+
end
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Prevent Actions on Archived Records
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
def update?
|
|
401
|
+
return false if record.try(:archived?)
|
|
402
|
+
super
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def destroy?
|
|
406
|
+
return false if record.try(:archived?)
|
|
407
|
+
super
|
|
408
|
+
end
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Handling Unauthorized Access
|
|
412
|
+
|
|
413
|
+
When authorization fails, ActionPolicy raises `ActionPolicy::Unauthorized`.
|
|
414
|
+
|
|
415
|
+
### Custom Error Handling
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
# app/controllers/application_controller.rb
|
|
419
|
+
class ApplicationController < ActionController::Base
|
|
420
|
+
rescue_from ActionPolicy::Unauthorized do |exception|
|
|
421
|
+
respond_to do |format|
|
|
422
|
+
format.html { redirect_to root_path, alert: "You are not authorized." }
|
|
423
|
+
format.json { render json: { error: "Unauthorized" }, status: :forbidden }
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Skip Verification (Custom Actions)
|
|
430
|
+
|
|
431
|
+
Built-in CRUD actions automatically verify authorization. For custom actions:
|
|
432
|
+
|
|
433
|
+
```ruby
|
|
434
|
+
class PostsController < ResourceController
|
|
435
|
+
skip_verify_authorize_current only: [:custom_action]
|
|
436
|
+
|
|
437
|
+
def custom_action
|
|
438
|
+
# Handle authorization manually or skip entirely
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Debugging Authorization
|
|
444
|
+
|
|
445
|
+
### Check Why Access Denied
|
|
446
|
+
|
|
447
|
+
Add logging to your policy:
|
|
448
|
+
|
|
449
|
+
```ruby
|
|
450
|
+
def update?
|
|
451
|
+
result = owner?
|
|
452
|
+
Rails.logger.debug { "PostPolicy#update? for user #{user.id} on post #{record.id}: #{result}" }
|
|
453
|
+
result
|
|
454
|
+
end
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Policy Inspection
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
policy = PostPolicy.new(user: current_user, record: @post)
|
|
461
|
+
puts policy.permitted_attributes_for_update.inspect
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Related
|
|
465
|
+
|
|
466
|
+
- [Authentication](./authentication)
|
|
467
|
+
- [Multi-tenancy](./multi-tenancy)
|
|
468
|
+
- [Custom Actions](./custom-actions)
|