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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/# Plutonium: The pre-alpha demo.md +4 -2
  3. data/.claude/skills/assets/SKILL.md +416 -0
  4. data/.claude/skills/connect-resource/SKILL.md +112 -0
  5. data/.claude/skills/controller/SKILL.md +302 -0
  6. data/.claude/skills/create-resource/SKILL.md +240 -0
  7. data/.claude/skills/definition/SKILL.md +218 -0
  8. data/.claude/skills/definition-actions/SKILL.md +386 -0
  9. data/.claude/skills/definition-fields/SKILL.md +474 -0
  10. data/.claude/skills/definition-query/SKILL.md +334 -0
  11. data/.claude/skills/forms/SKILL.md +439 -0
  12. data/.claude/skills/installation/SKILL.md +300 -0
  13. data/.claude/skills/interaction/SKILL.md +382 -0
  14. data/.claude/skills/model/SKILL.md +267 -0
  15. data/.claude/skills/model-features/SKILL.md +286 -0
  16. data/.claude/skills/nested-resources/SKILL.md +274 -0
  17. data/.claude/skills/package/SKILL.md +191 -0
  18. data/.claude/skills/policy/SKILL.md +352 -0
  19. data/.claude/skills/portal/SKILL.md +400 -0
  20. data/.claude/skills/resource/SKILL.md +281 -0
  21. data/.claude/skills/rodauth/SKILL.md +452 -0
  22. data/.claude/skills/views/SKILL.md +563 -0
  23. data/Appraisals +46 -4
  24. data/CHANGELOG.md +32 -1
  25. data/app/assets/plutonium.css +2 -2
  26. data/config/brakeman.ignore +239 -0
  27. data/config/initializers/action_policy.rb +1 -1
  28. data/docs/.vitepress/config.ts +132 -47
  29. data/docs/concepts/architecture.md +226 -0
  30. data/docs/concepts/auto-detection.md +254 -0
  31. data/docs/concepts/index.md +61 -0
  32. data/docs/concepts/packages-portals.md +304 -0
  33. data/docs/concepts/resources.md +224 -0
  34. data/docs/cookbook/blog.md +412 -0
  35. data/docs/cookbook/index.md +289 -0
  36. data/docs/cookbook/saas.md +481 -0
  37. data/docs/getting-started/index.md +56 -0
  38. data/docs/getting-started/installation.md +146 -0
  39. data/docs/getting-started/tutorial/01-setup.md +118 -0
  40. data/docs/getting-started/tutorial/02-first-resource.md +180 -0
  41. data/docs/getting-started/tutorial/03-authentication.md +246 -0
  42. data/docs/getting-started/tutorial/04-authorization.md +170 -0
  43. data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
  44. data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
  45. data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
  46. data/docs/getting-started/tutorial/index.md +64 -0
  47. data/docs/guides/adding-resources.md +420 -0
  48. data/docs/guides/authentication.md +551 -0
  49. data/docs/guides/authorization.md +468 -0
  50. data/docs/guides/creating-packages.md +380 -0
  51. data/docs/guides/custom-actions.md +523 -0
  52. data/docs/guides/index.md +45 -0
  53. data/docs/guides/multi-tenancy.md +302 -0
  54. data/docs/guides/nested-resources.md +411 -0
  55. data/docs/guides/search-filtering.md +266 -0
  56. data/docs/guides/theming.md +321 -0
  57. data/docs/index.md +67 -26
  58. data/docs/public/CLAUDE.md +64 -21
  59. data/docs/reference/assets/index.md +496 -0
  60. data/docs/reference/controller/index.md +363 -0
  61. data/docs/reference/definition/actions.md +400 -0
  62. data/docs/reference/definition/fields.md +350 -0
  63. data/docs/reference/definition/index.md +252 -0
  64. data/docs/reference/definition/query.md +342 -0
  65. data/docs/reference/generators/index.md +469 -0
  66. data/docs/reference/index.md +49 -0
  67. data/docs/reference/interaction/index.md +445 -0
  68. data/docs/reference/model/features.md +248 -0
  69. data/docs/reference/model/index.md +219 -0
  70. data/docs/reference/policy/index.md +385 -0
  71. data/docs/reference/portal/index.md +382 -0
  72. data/docs/reference/views/forms.md +396 -0
  73. data/docs/reference/views/index.md +479 -0
  74. data/gemfiles/rails_7.gemfile +9 -2
  75. data/gemfiles/rails_7.gemfile.lock +146 -111
  76. data/gemfiles/rails_8.0.gemfile +20 -0
  77. data/gemfiles/rails_8.0.gemfile.lock +417 -0
  78. data/gemfiles/rails_8.1.gemfile +20 -0
  79. data/gemfiles/rails_8.1.gemfile.lock +419 -0
  80. data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
  81. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
  82. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
  83. data/lib/generators/pu/pkg/portal/USAGE +65 -0
  84. data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
  85. data/lib/generators/pu/res/conn/USAGE +71 -0
  86. data/lib/generators/pu/res/model/USAGE +106 -110
  87. data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
  88. data/lib/generators/pu/res/scaffold/USAGE +85 -0
  89. data/lib/generators/pu/rodauth/install_generator.rb +2 -6
  90. data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
  91. data/lib/generators/pu/skills/sync/USAGE +14 -0
  92. data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
  93. data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
  94. data/lib/plutonium/core/controller.rb +2 -2
  95. data/lib/plutonium/interaction/base.rb +1 -0
  96. data/lib/plutonium/package/engine.rb +2 -2
  97. data/lib/plutonium/query/adhoc_block.rb +6 -2
  98. data/lib/plutonium/query/model_scope.rb +1 -1
  99. data/lib/plutonium/railtie.rb +4 -0
  100. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  101. data/lib/plutonium/resource/query_object.rb +38 -8
  102. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  103. data/lib/plutonium/version.rb +1 -1
  104. data/lib/tasks/release.rake +19 -4
  105. data/package.json +1 -1
  106. metadata +76 -39
  107. data/brakeman.ignore +0 -28
  108. data/docs/api-examples.md +0 -49
  109. data/docs/guide/claude-code-guide.md +0 -74
  110. data/docs/guide/deep-dive/authorization.md +0 -189
  111. data/docs/guide/deep-dive/multitenancy.md +0 -256
  112. data/docs/guide/deep-dive/resources.md +0 -390
  113. data/docs/guide/getting-started/01-installation.md +0 -165
  114. data/docs/guide/index.md +0 -28
  115. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  116. data/docs/guide/introduction/02-core-concepts.md +0 -440
  117. data/docs/guide/tutorial/01-project-setup.md +0 -75
  118. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  119. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  120. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  121. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  122. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  123. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  124. data/docs/markdown-examples.md +0 -85
  125. data/docs/modules/action.md +0 -244
  126. data/docs/modules/authentication.md +0 -236
  127. data/docs/modules/configuration.md +0 -599
  128. data/docs/modules/controller.md +0 -443
  129. data/docs/modules/core.md +0 -316
  130. data/docs/modules/definition.md +0 -1308
  131. data/docs/modules/display.md +0 -759
  132. data/docs/modules/form.md +0 -495
  133. data/docs/modules/generator.md +0 -400
  134. data/docs/modules/index.md +0 -167
  135. data/docs/modules/interaction.md +0 -642
  136. data/docs/modules/package.md +0 -151
  137. data/docs/modules/policy.md +0 -176
  138. data/docs/modules/portal.md +0 -710
  139. data/docs/modules/query.md +0 -297
  140. data/docs/modules/resource_record.md +0 -618
  141. data/docs/modules/routing.md +0 -690
  142. data/docs/modules/table.md +0 -301
  143. 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)