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,219 @@
|
|
|
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/comments`). The `associated_with` scope is automatically available for querying.
|
|
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
|
+
- [Resources Concept](/concepts/resources)
|
|
219
|
+
- [Definition Reference](/reference/definition/)
|
|
@@ -0,0 +1,385 @@
|
|
|
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
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
def update?
|
|
42
|
+
user # => Current user
|
|
43
|
+
record # => The Post instance
|
|
44
|
+
entity_scope # => Organization for multi-tenant portals
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Action Permissions
|
|
49
|
+
|
|
50
|
+
### Core Actions (Must Override)
|
|
51
|
+
|
|
52
|
+
These default to `false` - you must override them:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class PostPolicy < Plutonium::Resource::Policy
|
|
56
|
+
def create?
|
|
57
|
+
user.present?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def read?
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Derived Actions
|
|
67
|
+
|
|
68
|
+
These inherit from core actions by default:
|
|
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
|
+
|
|
80
|
+
### Example with Ownership
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
class PostPolicy < Plutonium::Resource::Policy
|
|
84
|
+
def create?
|
|
85
|
+
user.present?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def read?
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def update?
|
|
93
|
+
owner? || admin?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def destroy?
|
|
97
|
+
owner? || admin?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def owner?
|
|
103
|
+
record.user_id == user.id
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def admin?
|
|
107
|
+
user.admin?
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Custom Action Permissions
|
|
113
|
+
|
|
114
|
+
For custom actions defined in definitions:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
def publish?
|
|
118
|
+
owner? && !record.published?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def archive?
|
|
122
|
+
owner? || admin?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def bulk_delete?
|
|
126
|
+
admin?
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Actions are secure by default - undefined methods return `false`.
|
|
131
|
+
|
|
132
|
+
## Attribute Permissions
|
|
133
|
+
|
|
134
|
+
### Core Methods (Must Override for Production)
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# What users can see (index, show)
|
|
138
|
+
def permitted_attributes_for_read
|
|
139
|
+
%i[title body author created_at]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# What users can set (create, update)
|
|
143
|
+
def permitted_attributes_for_create
|
|
144
|
+
%i[title body category_id]
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Derived Methods
|
|
149
|
+
|
|
150
|
+
| Method | Inherits From |
|
|
151
|
+
|--------|---------------|
|
|
152
|
+
| `permitted_attributes_for_update` | `permitted_attributes_for_create` |
|
|
153
|
+
| `permitted_attributes_for_index` | `permitted_attributes_for_read` |
|
|
154
|
+
| `permitted_attributes_for_show` | `permitted_attributes_for_read` |
|
|
155
|
+
| `permitted_attributes_for_new` | `permitted_attributes_for_create` |
|
|
156
|
+
| `permitted_attributes_for_edit` | `permitted_attributes_for_update` |
|
|
157
|
+
|
|
158
|
+
### Conditional Attribute Access
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
def permitted_attributes_for_create
|
|
162
|
+
attrs = %i[title body]
|
|
163
|
+
attrs << :featured if user.admin?
|
|
164
|
+
attrs << :author_id if user.admin?
|
|
165
|
+
attrs
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def permitted_attributes_for_update
|
|
169
|
+
case record.status
|
|
170
|
+
when 'draft'
|
|
171
|
+
%i[title body category_id]
|
|
172
|
+
when 'published'
|
|
173
|
+
%i[body] # Can only edit body once published
|
|
174
|
+
else
|
|
175
|
+
[]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Auto-Detection (Development Only)
|
|
181
|
+
|
|
182
|
+
In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
🚨 Resource field auto-detection: PostPolicy#permitted_attributes_for_create
|
|
186
|
+
Auto-detected resource fields result in security holes and will fail outside of development.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Association Permissions
|
|
190
|
+
|
|
191
|
+
Control which associations appear in panels and forms:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
def permitted_associations
|
|
195
|
+
%i[comments tags author]
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Returns an empty array by default.
|
|
200
|
+
|
|
201
|
+
## Collection Scoping
|
|
202
|
+
|
|
203
|
+
### relation_scope
|
|
204
|
+
|
|
205
|
+
Filter which records users can see using ActionPolicy's `relation_scope`:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
class PostPolicy < Plutonium::Resource::Policy
|
|
209
|
+
relation_scope do |relation|
|
|
210
|
+
if user.admin?
|
|
211
|
+
relation
|
|
212
|
+
else
|
|
213
|
+
relation.where(published: true).or(
|
|
214
|
+
relation.where(user_id: user.id)
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### With Entity Scoping
|
|
222
|
+
|
|
223
|
+
Call `super` to preserve automatic entity scoping for multi-tenancy:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
relation_scope do |relation|
|
|
227
|
+
relation = super(relation) # Applies associated_with(entity_scope)
|
|
228
|
+
|
|
229
|
+
if user.admin?
|
|
230
|
+
relation
|
|
231
|
+
else
|
|
232
|
+
relation.where(published: true)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The default `relation_scope` automatically applies `relation.associated_with(entity_scope)` when an entity scope is present.
|
|
238
|
+
|
|
239
|
+
## Portal-Specific Policies
|
|
240
|
+
|
|
241
|
+
Override policies for specific portals:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
# packages/admin_portal/app/policies/admin_portal/post_policy.rb
|
|
245
|
+
module AdminPortal
|
|
246
|
+
class PostPolicy < ::PostPolicy
|
|
247
|
+
def destroy?
|
|
248
|
+
true # Admins can delete any post
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def permitted_attributes_for_create
|
|
252
|
+
%i[title body featured internal_notes] # More fields
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
relation_scope do |relation|
|
|
256
|
+
relation # No restrictions for admins
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Custom Authorization Context
|
|
263
|
+
|
|
264
|
+
Add custom context using ActionPolicy's `authorize` directive:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
# In policy
|
|
268
|
+
class PostPolicy < Plutonium::Resource::Policy
|
|
269
|
+
authorize :department, allow_nil: true
|
|
270
|
+
|
|
271
|
+
def create?
|
|
272
|
+
department&.allows_posting?
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# In controller
|
|
277
|
+
class PostsController < ResourceController
|
|
278
|
+
authorize :department, through: :current_department
|
|
279
|
+
|
|
280
|
+
private
|
|
281
|
+
|
|
282
|
+
def current_department
|
|
283
|
+
current_user.department
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Authorization Errors
|
|
289
|
+
|
|
290
|
+
When authorization fails:
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# Raises ActionPolicy::Unauthorized
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Handling Errors
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
# app/controllers/application_controller.rb
|
|
300
|
+
rescue_from ActionPolicy::Unauthorized do |exception|
|
|
301
|
+
redirect_to root_path, alert: "Not authorized"
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Common Patterns
|
|
306
|
+
|
|
307
|
+
### Role-Based
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
def update?
|
|
311
|
+
case user.role
|
|
312
|
+
when 'admin' then true
|
|
313
|
+
when 'editor' then true
|
|
314
|
+
when 'author' then owner?
|
|
315
|
+
else false
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Status-Based
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
def update?
|
|
324
|
+
return false if record.archived?
|
|
325
|
+
owner? || admin?
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Time-Based
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
def update?
|
|
333
|
+
return false if record.created_at < 24.hours.ago
|
|
334
|
+
owner?
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Hierarchical
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
def read?
|
|
342
|
+
return true if admin?
|
|
343
|
+
return true if manager_of_department?
|
|
344
|
+
return true if owner?
|
|
345
|
+
record.public?
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Debugging
|
|
350
|
+
|
|
351
|
+
### Logging
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
def update?
|
|
355
|
+
result = owner? || admin?
|
|
356
|
+
Rails.logger.debug { "PostPolicy#update? user=#{user.id} post=#{record.id}: #{result}" }
|
|
357
|
+
result
|
|
358
|
+
end
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Console Testing
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
user = User.find(1)
|
|
365
|
+
post = Post.find(1)
|
|
366
|
+
|
|
367
|
+
# Use ActionPolicy's testing helpers
|
|
368
|
+
policy = PostPolicy.new(post, user: user)
|
|
369
|
+
policy.update?
|
|
370
|
+
policy.permitted_attributes_for_update
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Best Practices
|
|
374
|
+
|
|
375
|
+
1. **Always override `create?` and `read?`** - They default to `false`
|
|
376
|
+
2. **Define attributes explicitly** - Auto-detection only works in development
|
|
377
|
+
3. **Call `super` in `relation_scope`** - Preserves entity scoping
|
|
378
|
+
4. **Use derived methods** - Let `update?` inherit from `create?` when appropriate
|
|
379
|
+
5. **Keep policies focused** - Authorization logic only, no business logic
|
|
380
|
+
6. **Test edge cases** - Archived records, nil associations, role combinations
|
|
381
|
+
|
|
382
|
+
## Related
|
|
383
|
+
|
|
384
|
+
- [Multi-tenancy Guide](/guides/multi-tenancy)
|
|
385
|
+
- [ActionPolicy Documentation](https://actionpolicy.evilmartians.io/)
|