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,218 +0,0 @@
|
|
|
1
|
-
# Model Reference
|
|
2
|
-
|
|
3
|
-
Complete reference for Plutonium resource models.
|
|
4
|
-
|
|
5
|
-
## Base Class
|
|
6
|
-
|
|
7
|
-
All resource models inherit from `ResourceRecord`:
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
class Post < ResourceRecord
|
|
11
|
-
# Your model code
|
|
12
|
-
end
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
In packages, models inherit from the package's ResourceRecord:
|
|
16
|
-
|
|
17
|
-
```ruby
|
|
18
|
-
module Blogging
|
|
19
|
-
class Post < Blogging::ResourceRecord
|
|
20
|
-
# Your model code
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
`ResourceRecord` is an abstract class that inherits from `ApplicationRecord` and is created by the Plutonium installer.
|
|
26
|
-
|
|
27
|
-
## Standard ActiveRecord Features
|
|
28
|
-
|
|
29
|
-
All standard ActiveRecord features work:
|
|
30
|
-
|
|
31
|
-
```ruby
|
|
32
|
-
class Post < ResourceRecord
|
|
33
|
-
# Associations
|
|
34
|
-
belongs_to :user
|
|
35
|
-
has_many :comments, dependent: :destroy
|
|
36
|
-
has_one :featured_image
|
|
37
|
-
has_many :tags, through: :post_tags
|
|
38
|
-
|
|
39
|
-
# Validations
|
|
40
|
-
validates :title, presence: true, length: { maximum: 200 }
|
|
41
|
-
validates :slug, uniqueness: true
|
|
42
|
-
validates :status, inclusion: { in: %w[draft published] }
|
|
43
|
-
|
|
44
|
-
# Scopes
|
|
45
|
-
scope :published, -> { where(status: 'published') }
|
|
46
|
-
scope :recent, -> { order(created_at: :desc) }
|
|
47
|
-
scope :by_author, ->(user) { where(user: user) }
|
|
48
|
-
|
|
49
|
-
# Callbacks
|
|
50
|
-
before_save :generate_slug
|
|
51
|
-
after_create :notify_subscribers
|
|
52
|
-
|
|
53
|
-
# Methods
|
|
54
|
-
def publish!
|
|
55
|
-
update!(status: 'published', published_at: Time.current)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Nested Resources
|
|
61
|
-
|
|
62
|
-
Nesting is automatic via `belongs_to` associations:
|
|
63
|
-
|
|
64
|
-
```ruby
|
|
65
|
-
class Comment < ResourceRecord
|
|
66
|
-
belongs_to :post
|
|
67
|
-
end
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
When both `Post` and `Comment` are registered in a portal, Plutonium automatically creates nested routes (`/posts/:post_id/nested_comments`). Queries are automatically scoped to the parent via the association.
|
|
71
|
-
|
|
72
|
-
See the [Nested Resources Guide](/guides/nested-resources) for details.
|
|
73
|
-
|
|
74
|
-
## Entity Scoping (Multi-tenancy)
|
|
75
|
-
|
|
76
|
-
Entity scoping is configured on the **portal engine**, not the model:
|
|
77
|
-
|
|
78
|
-
```ruby
|
|
79
|
-
# packages/customer_portal/lib/engine.rb
|
|
80
|
-
module CustomerPortal
|
|
81
|
-
class Engine < Rails::Engine
|
|
82
|
-
include Plutonium::Portal::Engine
|
|
83
|
-
|
|
84
|
-
config.after_initialize do
|
|
85
|
-
scope_to_entity Organization
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
See the [Multi-tenancy Guide](/guides/multi-tenancy) for details.
|
|
92
|
-
|
|
93
|
-
## Plutonium Features
|
|
94
|
-
|
|
95
|
-
See [Model Features](./features) for:
|
|
96
|
-
- `has_cents` - Store monetary values as integers, expose as decimals
|
|
97
|
-
- `to_label` - Human-readable record labels
|
|
98
|
-
- `path_parameter` / `dynamic_path_parameter` - Custom URL parameters
|
|
99
|
-
- Secure association SGIDs - Auto-generated SGID accessors for associations
|
|
100
|
-
- `associated_with` - Scope for nested resource queries
|
|
101
|
-
- Field introspection methods
|
|
102
|
-
|
|
103
|
-
## Field Introspection
|
|
104
|
-
|
|
105
|
-
Plutonium introspects models to detect:
|
|
106
|
-
|
|
107
|
-
### Column Types
|
|
108
|
-
|
|
109
|
-
| Database Type | Detected As |
|
|
110
|
-
|--------------|-------------|
|
|
111
|
-
| `string` | `:string` |
|
|
112
|
-
| `text` | `:text` |
|
|
113
|
-
| `integer` | `:integer` |
|
|
114
|
-
| `bigint` | `:integer` |
|
|
115
|
-
| `float` | `:float` |
|
|
116
|
-
| `decimal` | `:decimal` |
|
|
117
|
-
| `boolean` | `:boolean` |
|
|
118
|
-
| `date` | `:date` |
|
|
119
|
-
| `datetime` | `:datetime` |
|
|
120
|
-
| `time` | `:time` |
|
|
121
|
-
| `json`/`jsonb` | `:json` |
|
|
122
|
-
|
|
123
|
-
### Constraints
|
|
124
|
-
|
|
125
|
-
```ruby
|
|
126
|
-
# NULL constraint detected
|
|
127
|
-
t.string :title, null: false # Required field
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Associations
|
|
131
|
-
|
|
132
|
-
```ruby
|
|
133
|
-
belongs_to :user # Detected as association field
|
|
134
|
-
has_many :comments # Available for association panels
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Validations
|
|
138
|
-
|
|
139
|
-
```ruby
|
|
140
|
-
validates :title, presence: true # Required
|
|
141
|
-
validates :email, format: { ... } # Format hint
|
|
142
|
-
validates :role, inclusion: { in: [...] } # Select options
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
## Model Organization
|
|
146
|
-
|
|
147
|
-
### Feature Package Models
|
|
148
|
-
|
|
149
|
-
```ruby
|
|
150
|
-
# packages/blogging/app/models/blogging/post.rb
|
|
151
|
-
module Blogging
|
|
152
|
-
class Post < ResourceRecord
|
|
153
|
-
# Namespaced model
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Table Naming
|
|
159
|
-
|
|
160
|
-
Namespaced models use prefixed tables:
|
|
161
|
-
|
|
162
|
-
```ruby
|
|
163
|
-
module Blogging
|
|
164
|
-
class Post < ResourceRecord
|
|
165
|
-
# Table: blogging_posts
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Override if needed:
|
|
171
|
-
|
|
172
|
-
```ruby
|
|
173
|
-
self.table_name = "posts"
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
## Best Practices
|
|
177
|
-
|
|
178
|
-
### Keep Models Thin
|
|
179
|
-
|
|
180
|
-
Put complex logic in Interactions:
|
|
181
|
-
|
|
182
|
-
```ruby
|
|
183
|
-
# Model: simple validations and associations
|
|
184
|
-
class Post < ResourceRecord
|
|
185
|
-
validates :title, presence: true
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# Interaction: complex logic
|
|
189
|
-
class PublishPost < ResourceInteraction
|
|
190
|
-
def execute
|
|
191
|
-
resource.update!(published: true)
|
|
192
|
-
notify_subscribers
|
|
193
|
-
update_search_index
|
|
194
|
-
succeed(resource)
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### Use Meaningful Scopes
|
|
200
|
-
|
|
201
|
-
```ruby
|
|
202
|
-
# Good: intention-revealing names
|
|
203
|
-
scope :visible_to, ->(user) { where(user: user).or(where(published: true)) }
|
|
204
|
-
|
|
205
|
-
# Avoid: generic names
|
|
206
|
-
scope :filtered, -> { where(status: 'active') }
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
### Validate at the Right Level
|
|
210
|
-
|
|
211
|
-
- **Model**: Data integrity (presence, format, uniqueness)
|
|
212
|
-
- **Interaction**: Business rules (can only publish once)
|
|
213
|
-
- **Policy**: Authorization (user must own the record)
|
|
214
|
-
|
|
215
|
-
## Related
|
|
216
|
-
|
|
217
|
-
- [Model Features](./features)
|
|
218
|
-
- [Definition Reference](/reference/definition/)
|
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
# Policy Reference
|
|
2
|
-
|
|
3
|
-
Complete reference for authorization policies. Built on [ActionPolicy](https://actionpolicy.evilmartians.io/).
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
Policies control authorization at three levels:
|
|
8
|
-
1. **Action Permissions** - Can user perform this action?
|
|
9
|
-
2. **Attribute Permissions** - Which fields can user access?
|
|
10
|
-
3. **Scope Permissions** - Which records can user see?
|
|
11
|
-
|
|
12
|
-
## Base Class
|
|
13
|
-
|
|
14
|
-
```ruby
|
|
15
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
16
|
-
# Policy code
|
|
17
|
-
end
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
In packages, inherit from the package's ResourcePolicy:
|
|
21
|
-
|
|
22
|
-
```ruby
|
|
23
|
-
module AdminPortal
|
|
24
|
-
class PostPolicy < ::PostPolicy
|
|
25
|
-
# Portal-specific overrides
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Authorization Context
|
|
31
|
-
|
|
32
|
-
Inside a policy, you have access to:
|
|
33
|
-
|
|
34
|
-
| Variable | Description |
|
|
35
|
-
|----------|-------------|
|
|
36
|
-
| `user` | Current authenticated user (required) |
|
|
37
|
-
| `record` | Resource being authorized |
|
|
38
|
-
| `entity_scope` | Current scoped entity (for multi-tenancy) |
|
|
39
|
-
| `parent` | Parent record for nested resources (nil if not nested) |
|
|
40
|
-
| `parent_association` | Association name on parent (e.g., `:comments`) |
|
|
41
|
-
|
|
42
|
-
```ruby
|
|
43
|
-
def update?
|
|
44
|
-
user # => Current user
|
|
45
|
-
record # => The Post instance
|
|
46
|
-
entity_scope # => Organization for multi-tenant portals
|
|
47
|
-
parent # => Parent record (for nested routes)
|
|
48
|
-
parent_association # => :comments (association name)
|
|
49
|
-
end
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Action Permissions
|
|
53
|
-
|
|
54
|
-
### Core Actions (Must Override)
|
|
55
|
-
|
|
56
|
-
These default to `false` - you must override them:
|
|
57
|
-
|
|
58
|
-
```ruby
|
|
59
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
60
|
-
def create?
|
|
61
|
-
user.present?
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def read?
|
|
65
|
-
true
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Derived Actions
|
|
71
|
-
|
|
72
|
-
These inherit from core actions by default:
|
|
73
|
-
|
|
74
|
-
| Method | Inherits From | Override When |
|
|
75
|
-
|--------|---------------|---------------|
|
|
76
|
-
| `update?` | `create?` | Different update rules |
|
|
77
|
-
| `destroy?` | `create?` | Different delete rules |
|
|
78
|
-
| `index?` | `read?` | Custom listing rules |
|
|
79
|
-
| `show?` | `read?` | Record-specific read rules |
|
|
80
|
-
| `new?` | `create?` | Rarely needed |
|
|
81
|
-
| `edit?` | `update?` | Rarely needed |
|
|
82
|
-
| `search?` | `index?` | Search-specific rules |
|
|
83
|
-
|
|
84
|
-
### Example with Ownership
|
|
85
|
-
|
|
86
|
-
```ruby
|
|
87
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
88
|
-
def create?
|
|
89
|
-
user.present?
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def read?
|
|
93
|
-
true
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def update?
|
|
97
|
-
owner? || admin?
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def destroy?
|
|
101
|
-
owner? || admin?
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
def owner?
|
|
107
|
-
record.user_id == user.id
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def admin?
|
|
111
|
-
user.admin?
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### Custom Action Permissions
|
|
117
|
-
|
|
118
|
-
For custom actions defined in definitions:
|
|
119
|
-
|
|
120
|
-
```ruby
|
|
121
|
-
def publish?
|
|
122
|
-
owner? && !record.published?
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def archive?
|
|
126
|
-
owner? || admin?
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def bulk_delete?
|
|
130
|
-
admin?
|
|
131
|
-
end
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Actions are secure by default - undefined methods return `false`.
|
|
135
|
-
|
|
136
|
-
## Attribute Permissions
|
|
137
|
-
|
|
138
|
-
### Core Methods (Must Override for Production)
|
|
139
|
-
|
|
140
|
-
```ruby
|
|
141
|
-
# What users can see (index, show)
|
|
142
|
-
def permitted_attributes_for_read
|
|
143
|
-
%i[title body author created_at]
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# What users can set (create, update)
|
|
147
|
-
def permitted_attributes_for_create
|
|
148
|
-
%i[title body category_id]
|
|
149
|
-
end
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### Derived Methods
|
|
153
|
-
|
|
154
|
-
| Method | Inherits From |
|
|
155
|
-
|--------|---------------|
|
|
156
|
-
| `permitted_attributes_for_update` | `permitted_attributes_for_create` |
|
|
157
|
-
| `permitted_attributes_for_index` | `permitted_attributes_for_read` |
|
|
158
|
-
| `permitted_attributes_for_show` | `permitted_attributes_for_read` |
|
|
159
|
-
| `permitted_attributes_for_new` | `permitted_attributes_for_create` |
|
|
160
|
-
| `permitted_attributes_for_edit` | `permitted_attributes_for_update` |
|
|
161
|
-
|
|
162
|
-
### Conditional Attribute Access
|
|
163
|
-
|
|
164
|
-
```ruby
|
|
165
|
-
def permitted_attributes_for_create
|
|
166
|
-
attrs = %i[title body]
|
|
167
|
-
attrs << :featured if user.admin?
|
|
168
|
-
attrs << :author_id if user.admin?
|
|
169
|
-
attrs
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def permitted_attributes_for_update
|
|
173
|
-
case record.status
|
|
174
|
-
when 'draft'
|
|
175
|
-
%i[title body category_id]
|
|
176
|
-
when 'published'
|
|
177
|
-
%i[body] # Can only edit body once published
|
|
178
|
-
else
|
|
179
|
-
[]
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Auto-Detection (Development Only)
|
|
185
|
-
|
|
186
|
-
In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly:
|
|
187
|
-
|
|
188
|
-
```
|
|
189
|
-
🚨 Resource field auto-detection: PostPolicy#permitted_attributes_for_create
|
|
190
|
-
Auto-detected resource fields result in security holes and will fail outside of development.
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
## Association Permissions
|
|
194
|
-
|
|
195
|
-
Control which associations appear in panels and forms:
|
|
196
|
-
|
|
197
|
-
```ruby
|
|
198
|
-
def permitted_associations
|
|
199
|
-
%i[comments tags author]
|
|
200
|
-
end
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Returns an empty array by default.
|
|
204
|
-
|
|
205
|
-
## Collection Scoping
|
|
206
|
-
|
|
207
|
-
### relation_scope
|
|
208
|
-
|
|
209
|
-
Filter which records users can see using ActionPolicy's `relation_scope`:
|
|
210
|
-
|
|
211
|
-
```ruby
|
|
212
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
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 Parent Scoping (Nested Resources)
|
|
226
|
-
|
|
227
|
-
Call `super` to apply automatic parent scoping for nested resources:
|
|
228
|
-
|
|
229
|
-
```ruby
|
|
230
|
-
relation_scope do |relation|
|
|
231
|
-
relation = super(relation) # Applies parent scoping automatically
|
|
232
|
-
|
|
233
|
-
if user.admin?
|
|
234
|
-
relation
|
|
235
|
-
else
|
|
236
|
-
relation.where(approved: true)
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
**Parent scoping takes precedence over entity scoping.** When a parent is present:
|
|
242
|
-
- For `has_many` associations: scopes via `parent.association_name`
|
|
243
|
-
- For `has_one` associations: scopes via `where(foreign_key: parent.id)`
|
|
244
|
-
|
|
245
|
-
### With Entity Scoping (Multi-tenancy)
|
|
246
|
-
|
|
247
|
-
When no parent is present, `super` applies entity scoping:
|
|
248
|
-
|
|
249
|
-
```ruby
|
|
250
|
-
relation_scope do |relation|
|
|
251
|
-
relation = super(relation) # Applies associated_with(entity_scope)
|
|
252
|
-
|
|
253
|
-
if user.admin?
|
|
254
|
-
relation
|
|
255
|
-
else
|
|
256
|
-
relation.where(published: true)
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
The default `relation_scope` automatically applies `relation.associated_with(entity_scope)` when an entity scope is present and no parent is set.
|
|
262
|
-
|
|
263
|
-
### default_relation_scope is Required
|
|
264
|
-
|
|
265
|
-
Plutonium verifies that `default_relation_scope` is called in every `relation_scope`. This prevents accidental multi-tenancy leaks when overriding scopes.
|
|
266
|
-
|
|
267
|
-
```ruby
|
|
268
|
-
# ❌ This will raise an error
|
|
269
|
-
relation_scope do |relation|
|
|
270
|
-
relation.where(published: true) # Missing default_relation_scope!
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# ✅ Correct - call default_relation_scope
|
|
274
|
-
relation_scope do |relation|
|
|
275
|
-
default_relation_scope(relation).where(published: true)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
# ✅ Also correct - super calls default_relation_scope
|
|
279
|
-
relation_scope do |relation|
|
|
280
|
-
super(relation).where(published: true)
|
|
281
|
-
end
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
When overriding an inherited scope:
|
|
285
|
-
|
|
286
|
-
```ruby
|
|
287
|
-
class AdminPostPolicy < PostPolicy
|
|
288
|
-
relation_scope do |relation|
|
|
289
|
-
# Replace inherited scope but keep Plutonium's parent/entity scoping
|
|
290
|
-
default_relation_scope(relation)
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
This method applies parent scoping (for nested resources) or entity scoping (for multi-tenancy) directly, bypassing any inherited scope customizations.
|
|
296
|
-
|
|
297
|
-
### Skipping Default Scoping
|
|
298
|
-
|
|
299
|
-
If you intentionally need to bypass scoping, call `skip_default_relation_scope!`:
|
|
300
|
-
|
|
301
|
-
```ruby
|
|
302
|
-
relation_scope do |relation|
|
|
303
|
-
skip_default_relation_scope!
|
|
304
|
-
relation # No parent/entity scoping applied
|
|
305
|
-
end
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
This should be rare - consider using a separate portal with different scoping rules instead.
|
|
309
|
-
|
|
310
|
-
## Portal-Specific Policies
|
|
311
|
-
|
|
312
|
-
Override policies for specific portals:
|
|
313
|
-
|
|
314
|
-
```ruby
|
|
315
|
-
# packages/admin_portal/app/policies/admin_portal/post_policy.rb
|
|
316
|
-
module AdminPortal
|
|
317
|
-
class PostPolicy < ::PostPolicy
|
|
318
|
-
def destroy?
|
|
319
|
-
true # Admins can delete any post
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def permitted_attributes_for_create
|
|
323
|
-
%i[title body featured internal_notes] # More fields
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
relation_scope do |relation|
|
|
327
|
-
relation # No restrictions for admins
|
|
328
|
-
end
|
|
329
|
-
end
|
|
330
|
-
end
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
## Custom Authorization Context
|
|
334
|
-
|
|
335
|
-
Add custom context using ActionPolicy's `authorize` directive:
|
|
336
|
-
|
|
337
|
-
```ruby
|
|
338
|
-
# In policy
|
|
339
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
340
|
-
authorize :department, allow_nil: true
|
|
341
|
-
|
|
342
|
-
def create?
|
|
343
|
-
department&.allows_posting?
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
# In controller
|
|
348
|
-
class PostsController < ResourceController
|
|
349
|
-
authorize :department, through: :current_department
|
|
350
|
-
|
|
351
|
-
private
|
|
352
|
-
|
|
353
|
-
def current_department
|
|
354
|
-
current_user.department
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
## Authorization Errors
|
|
360
|
-
|
|
361
|
-
When authorization fails:
|
|
362
|
-
|
|
363
|
-
```ruby
|
|
364
|
-
# Raises ActionPolicy::Unauthorized
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### Handling Errors
|
|
368
|
-
|
|
369
|
-
```ruby
|
|
370
|
-
# app/controllers/application_controller.rb
|
|
371
|
-
rescue_from ActionPolicy::Unauthorized do |exception|
|
|
372
|
-
redirect_to root_path, alert: "Not authorized"
|
|
373
|
-
end
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
## Common Patterns
|
|
377
|
-
|
|
378
|
-
### Role-Based
|
|
379
|
-
|
|
380
|
-
```ruby
|
|
381
|
-
def update?
|
|
382
|
-
case user.role
|
|
383
|
-
when 'admin' then true
|
|
384
|
-
when 'editor' then true
|
|
385
|
-
when 'author' then owner?
|
|
386
|
-
else false
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
### Status-Based
|
|
392
|
-
|
|
393
|
-
```ruby
|
|
394
|
-
def update?
|
|
395
|
-
return false if record.archived?
|
|
396
|
-
owner? || admin?
|
|
397
|
-
end
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
### Time-Based
|
|
401
|
-
|
|
402
|
-
```ruby
|
|
403
|
-
def update?
|
|
404
|
-
return false if record.created_at < 24.hours.ago
|
|
405
|
-
owner?
|
|
406
|
-
end
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### Hierarchical
|
|
410
|
-
|
|
411
|
-
```ruby
|
|
412
|
-
def read?
|
|
413
|
-
return true if admin?
|
|
414
|
-
return true if manager_of_department?
|
|
415
|
-
return true if owner?
|
|
416
|
-
record.public?
|
|
417
|
-
end
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
## Debugging
|
|
421
|
-
|
|
422
|
-
### Logging
|
|
423
|
-
|
|
424
|
-
```ruby
|
|
425
|
-
def update?
|
|
426
|
-
result = owner? || admin?
|
|
427
|
-
Rails.logger.debug { "PostPolicy#update? user=#{user.id} post=#{record.id}: #{result}" }
|
|
428
|
-
result
|
|
429
|
-
end
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
### Console Testing
|
|
433
|
-
|
|
434
|
-
```ruby
|
|
435
|
-
user = User.find(1)
|
|
436
|
-
post = Post.find(1)
|
|
437
|
-
|
|
438
|
-
# Use ActionPolicy's testing helpers
|
|
439
|
-
policy = PostPolicy.new(post, user: user)
|
|
440
|
-
policy.update?
|
|
441
|
-
policy.permitted_attributes_for_update
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
## Best Practices
|
|
445
|
-
|
|
446
|
-
1. **Always override `create?` and `read?`** - They default to `false`
|
|
447
|
-
2. **Define attributes explicitly** - Auto-detection only works in development
|
|
448
|
-
3. **Call `super` in `relation_scope`** - Preserves entity scoping
|
|
449
|
-
4. **Use derived methods** - Let `update?` inherit from `create?` when appropriate
|
|
450
|
-
5. **Keep policies focused** - Authorization logic only, no business logic
|
|
451
|
-
6. **Test edge cases** - Archived records, nil associations, role combinations
|
|
452
|
-
|
|
453
|
-
## Related
|
|
454
|
-
|
|
455
|
-
- [Multi-tenancy Guide](/guides/multi-tenancy)
|
|
456
|
-
- [ActionPolicy Documentation](https://actionpolicy.evilmartians.io/)
|