plutonium 0.33.0 → 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 (145) 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 +36 -0
  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/controllers/crud_actions.rb +21 -18
  102. data/lib/plutonium/resource/controllers/interactive_actions.rb +21 -25
  103. data/lib/plutonium/resource/query_object.rb +38 -8
  104. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  105. data/lib/plutonium/version.rb +1 -1
  106. data/lib/tasks/release.rake +19 -4
  107. data/package.json +1 -1
  108. metadata +76 -39
  109. data/brakeman.ignore +0 -28
  110. data/docs/api-examples.md +0 -49
  111. data/docs/guide/claude-code-guide.md +0 -74
  112. data/docs/guide/deep-dive/authorization.md +0 -189
  113. data/docs/guide/deep-dive/multitenancy.md +0 -256
  114. data/docs/guide/deep-dive/resources.md +0 -390
  115. data/docs/guide/getting-started/01-installation.md +0 -165
  116. data/docs/guide/index.md +0 -28
  117. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  118. data/docs/guide/introduction/02-core-concepts.md +0 -440
  119. data/docs/guide/tutorial/01-project-setup.md +0 -75
  120. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  121. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  122. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  123. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  124. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  125. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  126. data/docs/markdown-examples.md +0 -85
  127. data/docs/modules/action.md +0 -244
  128. data/docs/modules/authentication.md +0 -236
  129. data/docs/modules/configuration.md +0 -599
  130. data/docs/modules/controller.md +0 -443
  131. data/docs/modules/core.md +0 -316
  132. data/docs/modules/definition.md +0 -1308
  133. data/docs/modules/display.md +0 -759
  134. data/docs/modules/form.md +0 -495
  135. data/docs/modules/generator.md +0 -400
  136. data/docs/modules/index.md +0 -167
  137. data/docs/modules/interaction.md +0 -642
  138. data/docs/modules/package.md +0 -151
  139. data/docs/modules/policy.md +0 -176
  140. data/docs/modules/portal.md +0 -710
  141. data/docs/modules/query.md +0 -297
  142. data/docs/modules/resource_record.md +0 -618
  143. data/docs/modules/routing.md +0 -690
  144. data/docs/modules/table.md +0 -301
  145. data/docs/modules/ui.md +0 -631
@@ -1,74 +0,0 @@
1
- ---
2
- title: Claude Code Guide for Plutonium Development
3
- ---
4
-
5
- <script setup>
6
- import { withBase } from 'vitepress'
7
- </script>
8
-
9
- # Claude Code Guide for Plutonium Development
10
-
11
- This page provides comprehensive development guidance for building Plutonium applications effectively. This guide is designed to help AI assistants and developers understand the framework's patterns and best practices.
12
-
13
- ## Quick Start
14
-
15
- **Download the CLAUDE.md File**: <a :href="withBase('/CLAUDE.md')" target="_blank">📄 CLAUDE.md</a>
16
-
17
- **Or download directly from your terminal**:
18
-
19
- ::: code-group
20
-
21
- ```bash [Unix/Linux/macOS/WSL]
22
- curl -o CLAUDE.md https://radioactive-labs.github.io/plutonium-core/CLAUDE.md
23
- ```
24
-
25
- ```cmd [Windows]
26
- curl -o CLAUDE.md https://radioactive-labs.github.io/plutonium-core/CLAUDE.md
27
- ```
28
-
29
- :::
30
-
31
- ## Using This Guide
32
-
33
- Claude Code uses CLAUDE.md files for project-specific context:
34
-
35
- 1. **Download the guide**: Right-click the link above and "Save As" to download the `CLAUDE.md` file
36
- 2. **Place in your project root** as `CLAUDE.md`
37
- 3. **Claude Code automatically loads** this file for project context
38
- 4. **Enhance with your own instructions** by adding project-specific details
39
-
40
- The guide provides comprehensive patterns and examples for building Plutonium applications with AI assistance.
41
-
42
- ## What's Included
43
-
44
- The CLAUDE.md guide contains comprehensive guidelines for:
45
-
46
- ### 🏗️ **Framework Architecture**
47
- - Resource-oriented development patterns
48
- - Package architecture (Feature & Portal packages)
49
- - Component-based UI with Phlex
50
- - Business logic through Interactions
51
-
52
- ### 📝 **Resource Development**
53
- - **Auto-detection philosophy** - Field types are automatically detected from models
54
- - **Definition patterns** - Only override when needed
55
- - **Policy-based authorization** - Fine-grained permissions
56
- - **Interaction-driven business logic** - Encapsulated operations
57
-
58
- ### 🔧 **Development Patterns**
59
- - **Generator commands** for scaffolding
60
- - **Authentication setup** with Rodauth
61
- - **Multi-tenancy** with entity scoping
62
- - **Query objects** for filtering and search
63
-
64
- ### 🎨 **UI Customization**
65
- - **Component architecture** with Phlex
66
- - **Custom display blocks** with `phlexi_tag`
67
- - **Conditional rendering** with context awareness
68
- - **Layout customization** patterns
69
-
70
- ### ⚡ **Best Practices**
71
- - **Performance optimization** techniques
72
- - **Security guidelines** and defaults
73
- - **Code organization** principles
74
- - **Development workflow** recommendations
@@ -1,189 +0,0 @@
1
- # Deep Dive: Authorization
2
-
3
- Plutonium provides a robust authorization system built on top of [Action Policy](https://actionpolicy.evilmartians.io/). It's designed to be secure by default while offering fine-grained control over every aspect of your application's access control.
4
-
5
- ## Core Principles
6
-
7
- Authorization in Plutonium is handled by **Policy** classes. Every resource should have a corresponding policy that inherits from `Plutonium::Resource::Policy`.
8
-
9
- A policy's primary job is to answer the question: "Can the current `user` perform this `action` on this `record`?"
10
-
11
- ```ruby
12
- class PostPolicy < Plutonium::Resource::Policy
13
- # Can the user see a list of posts?
14
- def index?
15
- true # Everyone can see the list
16
- end
17
-
18
- # Can the user update this specific post?
19
- def update?
20
- # `record` is the post instance.
21
- # `user` is the current authenticated user.
22
- record.author == user || user.admin?
23
- end
24
-
25
- # ... other permissions
26
- end
27
- ```
28
-
29
- ## Secure by Default: The Permission Chain
30
-
31
- Plutonium policies are secure by default. If a permission is not explicitly granted, it's denied. This is achieved through a clear inheritance chain.
32
-
33
- ::: code-group
34
- ```ruby [Core Permissions]
35
- # These are the base permissions.
36
- # They both default to `false`. You MUST override them.
37
- def create?
38
- false
39
- end
40
-
41
- def read?
42
- false
43
- end
44
- ```
45
- ```ruby [Derived Permissions]
46
- # These permissions inherit from the core ones.
47
- # You can override them for more granular control.
48
- def update?
49
- create?
50
- end
51
-
52
- def destroy?
53
- create?
54
- end
55
-
56
- def index?
57
- read?
58
- end
59
-
60
- def show?
61
- read?
62
- end
63
- ```
64
- :::
65
-
66
- ::: danger Always Define Core Permissions
67
- Because `create?` and `read?` default to `false`, you must define them in your policy to grant any access. If `create?` is `false`, then `update?` and `destroy?` will also be `false` unless you explicitly override them.
68
- :::
69
-
70
- ## Attribute & Association Permissions
71
-
72
- Beyond actions, policies also control access to a resource's data at a granular level.
73
-
74
- ### Attribute Permissions
75
-
76
- Attribute permissions control which fields a user can see or submit in a form. They follow a similar inheritance chain.
77
-
78
- ::: code-group
79
- ```ruby [Read Attributes]
80
- # Controls which fields are returned for `index` and `show` actions.
81
- def permitted_attributes_for_read
82
- # By default, auto-detects all columns in development,
83
- # but MUST be overridden for production.
84
- end
85
- ```
86
- ```ruby [Create/Update Attributes]
87
- # Controls which fields are allowed in `create` and `update` actions.
88
- def permitted_attributes_for_create
89
- # By default, auto-detects columns (minus some system ones)
90
- # in development, but MUST be overridden for production.
91
- end
92
-
93
- def permitted_attributes_for_update
94
- # Inherits from `permitted_attributes_for_create` by default.
95
- permitted_attributes_for_create
96
- end
97
- ```
98
- :::
99
-
100
- ::: warning Override in Production
101
- The default auto-detection for attributes only works in development to speed up initial scaffolding. You **must** override `permitted_attributes_for_create` and `permitted_attributes_for_read` in your policies for them to work in production.
102
- :::
103
-
104
- ### Association Permissions
105
-
106
- By default, no associations are permitted. You must explicitly list which related resources can be included.
107
-
108
- ```ruby
109
- class PostPolicy < Plutonium::Resource::Policy
110
- def permitted_associations
111
- [:comments, :author]
112
- end
113
- end
114
- ```
115
-
116
- ## Scoping: Filtering Collections
117
-
118
- A policy's `relation_scope` is used to filter down a collection of records to only what the current user should see. This is applied automatically on `index` pages.
119
-
120
- ::: code-group
121
- ```ruby [Simple Scope]
122
- class PostPolicy < Plutonium::Resource::Policy
123
- relation_scope do |relation|
124
- if user.admin?
125
- relation # Admins see all posts
126
- else
127
- # Others only see their own posts or published posts
128
- relation.where(author: user).or(relation.where(published: true))
129
- end
130
- end
131
- end
132
- ```
133
- ```ruby [Multi-Tenant Scope]
134
- class PostPolicy < Plutonium::Resource::Policy
135
- relation_scope do |relation|
136
- # `super` applies the portal's entity scoping first
137
- # e.g., `relation.associated_with(current_organization)`
138
- relation = super(relation)
139
-
140
- # Then, apply additional logic
141
- if user.admin?
142
- relation
143
- else
144
- relation.where(published: true)
145
- end
146
- end
147
- end
148
- ```
149
- :::
150
-
151
- ## Authorization Context
152
-
153
- Policies have access to a `context` object. By default, Plutonium provides two:
154
-
155
- - **`user`**: The current authenticated user. This is **required**.
156
- - **`entity_scope`**: The current portal's multi-tenancy record (e.g., the current `Organization`). This is optional.
157
-
158
- You can add your own custom context objects for more complex scenarios.
159
-
160
- ::: details Adding Custom Context
161
- Imagine you have a separate `Ability` system that you also want to check.
162
-
163
- **1. Define the context in the Policy:**
164
- ```ruby
165
- class PostPolicy < ResourcePolicy
166
- authorize :ability, allow_nil: true
167
-
168
- def promote?
169
- # You can now use `ability` in your permission checks
170
- user.admin? && ability&.can?(:promote, record)
171
- end
172
- end
173
- ```
174
-
175
- **2. Provide the context from the Controller:**
176
- ```ruby
177
- class PostsController < ResourceController
178
- # This tells the policy how to find the `ability` object.
179
- authorize :ability, through: :current_ability
180
-
181
- private
182
-
183
- def current_ability
184
- # Your custom logic to find the ability object
185
- Ability.new(user)
186
- end
187
- end
188
- ```
189
- :::
@@ -1,256 +0,0 @@
1
- # Deep Dive: Multitenancy
2
-
3
- Plutonium is designed to make building sophisticated, multi-tenant applications straightforward. Multitenancy allows you to serve distinct groups of users (like different companies or teams) from a single application instance, ensuring each group's data is kept private and isolated.
4
-
5
- This is achieved through a powerful feature called **Entity Scoping**, which automates data isolation, URL generation, and authorization with minimal setup.
6
-
7
- ::: tip What You'll Learn
8
- - The core concept of Entity Scoping
9
- - A step-by-step guide to configuring multitenancy
10
- - The practical benefits of automatic data isolation and routing
11
- - Advanced patterns for complex multi-tenant scenarios
12
- - Best practices for security, testing, and performance
13
- :::
14
-
15
- ## How Entity Scoping Works
16
-
17
- Entity Scoping is the heart of Plutonium's multitenancy. Instead of manually filtering data in every controller and model, you declare a **Scoping Entity** for a user-facing **Portal**. This entity is typically a model like `Organization`, `Account`, or `Workspace`.
18
-
19
- Once configured, Plutonium handles the rest automatically.
20
-
21
- ::: code-group
22
- ```ruby [Without Plutonium Scoping]
23
- # Manual filtering is required everywhere
24
- class PostsController < ApplicationController
25
- def index
26
- # Manually find the organization and scope posts
27
- @organization = Organization.find(params[:organization_id])
28
- @posts = @organization.posts
29
- end
30
-
31
- def create
32
- # Manually associate new records with the organization
33
- @organization = Organization.find(params[:organization_id])
34
- @post = @organization.posts.build(post_params)
35
- # ...
36
- end
37
- end
38
- ```
39
-
40
- ```ruby [With Plutonium Scoping]
41
- # Scoping is automatic and declarative
42
- class PostsController < ResourceController
43
- def index
44
- # current_authorized_scope automatically returns posts scoped to the current organization
45
- # When using path strategy: URL -> /organizations/123/posts
46
- # When using subdomain strategy: URL -> /posts (on acme.yourapp.com)
47
- end
48
-
49
- def create
50
- # resource_params automatically includes the current organization association
51
- # Scoping works regardless of the strategy used
52
- end
53
- end
54
- ```
55
- :::
56
-
57
- ## Setting Up Your Multi-Tenant Portal
58
-
59
- Configuring multitenancy involves three main steps: defining your strategy, implementing it, and ensuring your models are correctly associated.
60
-
61
- ### 1. Configure Your Portal Engine
62
-
63
- First, you tell your portal which entity to scope to and which strategy to use. This is done in the portal's `engine.rb` file using `scope_to_entity`.
64
-
65
- A **Scoping Strategy** tells Plutonium how to identify the current tenant for each request.
66
-
67
- ::: code-group
68
- ```ruby [Path Strategy (Most Common)]
69
- # packages/admin_portal/lib/engine.rb
70
- # Tenant is identified by a URL parameter, e.g., /organizations/:organization_id
71
- scope_to_entity Organization, strategy: :path
72
- ```
73
-
74
- ```ruby [Custom Strategy (Subdomain)]
75
- # packages/customer_portal/lib/engine.rb
76
- # Tenant is identified by the a custom strategy that checks the subdomain, e.g., acme.yourapp.com
77
- scope_to_entity Organization, strategy: :current_organization
78
- ```
79
-
80
- ```ruby [Custom Parameter Key]
81
- # packages/client_portal/lib/engine.rb
82
- # Same as :path, but with a custom parameter name.
83
- scope_to_entity Client,
84
- strategy: :path,
85
- param_key: :client_slug # URL -> /clients/:client_slug
86
- ```
87
- :::
88
-
89
- You can also use any custom name for your strategy method (the method name becomes the strategy).
90
-
91
- ### 2. Implement the Strategy Method
92
-
93
- If you use any strategy other than `:path`, you must implement a controller method that returns the current tenant object. The method name must exactly match the strategy name.
94
-
95
- This logic typically lives in your portal's base controller concern.
96
-
97
- ::: code-group
98
- ```ruby [Subdomain Strategy]
99
- # packages/customer_portal/app/controllers/customer_portal/concerns/controller.rb
100
- private
101
-
102
- # Method name :current_organization matches the strategy
103
- def current_organization
104
- @current_organization ||= Organization.find_by!(subdomain: request.subdomain)
105
- rescue ActiveRecord::RecordNotFound
106
- redirect_to root_path, error: "Invalid organization subdomain"
107
- end
108
- ```
109
-
110
- ```ruby [Session-Based Strategy]
111
- # packages/internal_portal/app/controllers/internal_portal/concerns/controller.rb
112
- # In engine.rb: scope_to_entity Workspace, strategy: :current_workspace
113
- private
114
-
115
- def current_workspace
116
- return @current_workspace if defined?(@current_workspace)
117
-
118
- workspace_id = session[:workspace_id] || params[:workspace_id]
119
- @current_workspace = current_user.workspaces.find(workspace_id)
120
-
121
- session[:workspace_id] = @current_workspace.id # Remember for next request
122
- @current_workspace
123
- rescue ActiveRecord::RecordNotFound
124
- redirect_to workspace_selection_path, error: "Please select a workspace"
125
- end
126
- ```
127
- :::
128
-
129
- ### 3. Connect Your Models
130
-
131
- Plutonium needs to understand how your resources relate to the scoping entity. It automatically discovers these relationships in three ways:
132
-
133
- ::: code-group
134
- ```ruby [1. Direct Association (Preferred)]
135
- # The model belongs directly to the scoping entity.
136
- class Post < ApplicationRecord
137
- belongs_to :organization # Direct link
138
- end
139
- ```
140
-
141
- ```ruby [2. Indirect Association]
142
- # The model belongs to another model that has the direct link.
143
- # Plutonium automatically follows the chain: Comment -> Post -> Organization
144
- class Comment < ApplicationRecord
145
- belongs_to :post
146
- has_one :organization, through: :post # Indirect link
147
- end
148
- ```
149
-
150
- ```ruby [3. Custom Scope (For Complex Cases)]
151
- # For complex relationships, you can define an explicit scope.
152
- # The scope name must be `associated_with_#{scoping_entity_name}`.
153
- class Invoice < ApplicationRecord
154
- belongs_to :customer
155
-
156
- scope :associated_with_organization, ->(organization) do
157
- joins(customer: :organization_memberships)
158
- .where(organization_memberships: { organization_id: organization.id })
159
- end
160
- end
161
- ```
162
- :::
163
-
164
- ## The Benefits in Practice
165
-
166
- With this setup complete, you gain several powerful features across your portal.
167
-
168
- ### Tenant-Aware Routing
169
-
170
- Your application's URLs are automatically transformed to include the tenant context, and Plutonium's URL helpers adapt accordingly.
171
-
172
- - **URL Transformation:** Routes like `/posts` and `/posts/123` become `/:organization_id/posts` and `/:organization_id/posts/123`.
173
- - **Automatic URL Generation:** The `resource_url_for` helper automatically includes the current tenant in all generated URLs, so links and forms work without any changes.
174
-
175
- ```ruby
176
- # Both of these helpers are automatically aware of the current tenant.
177
- resource_url_for(Post) # => "/organizations/456/posts"
178
- form_with model: @post # action -> "/organizations/456/posts/123"
179
- ```
180
-
181
- ### Secure Data Scoping
182
-
183
- All data access is automatically and securely filtered to the current tenant.
184
-
185
- - **Query Scoping:** A query like `Post.all` is automatically converted to `current_scoped_entity.posts`. This prevents accidental data leaks.
186
- - **Record Scoping:** When fetching a single record (e.g., for `show` or `edit`), Plutonium ensures it belongs to the current tenant. If not, it raises an `ActiveRecord::RecordNotFound` error, just as if the record didn't exist.
187
-
188
- ### Integrated Authorization
189
-
190
- The current `entity_scope` is seamlessly passed to your authorization policies, allowing for fine-grained, tenant-aware rules.
191
-
192
- ```ruby
193
- class PostPolicy < Plutonium::Resource::Policy
194
- # `entity_scope` is automatically available in all policy methods.
195
- authorize :entity_scope, allow_nil: true
196
-
197
- def update?
198
- # A user can only update a post if it belongs to their current tenant
199
- # AND they are the author of the post.
200
- record.organization == entity_scope && record.author == user
201
- end
202
-
203
- relation_scope do |relation|
204
- # `super` automatically applies the base entity scoping.
205
- relation = super(relation)
206
-
207
- # Add more logic: Admins can see all posts within their organization,
208
- # but others can only see published posts.
209
- user.admin? ? relation : relation.where(published: true)
210
- end
211
- end
212
- ```
213
-
214
- ## Security Best Practices
215
-
216
- Securing a multi-tenant application is critical. While Plutonium provides strong defaults, you must ensure your implementation is secure.
217
-
218
- ::: warning Always Validate Tenant Access
219
- A user might belong to multiple tenants. It's crucial to verify that the logged-in user has permission to access the tenant specified in the URL. Failure to do so could allow a user to see data from another organization they don't belong to.
220
- :::
221
-
222
- ```ruby
223
- # ✅ Good: Proper tenant validation
224
- # In your custom strategy method or a before_action:
225
- private
226
-
227
- def current_organization
228
- @current_organization ||= begin
229
- # Find the organization from the URL
230
- organization = Organization.find(params[:organization_id])
231
-
232
- # CRITICAL: Verify the current user is a member of that organization
233
- unless current_user.organizations.include?(organization)
234
- raise ActionPolicy::Unauthorized, "Access denied to organization"
235
- end
236
-
237
- organization
238
- end
239
- end
240
-
241
- # ❌ Dangerous: No access validation
242
- def current_organization
243
- # This allows ANY authenticated user to access ANY organization's data
244
- # simply by changing the ID in the URL.
245
- Organization.find(params[:organization_id])
246
- end
247
- ```
248
-
249
- ## Advanced Patterns
250
-
251
- Plutonium's scoping is flexible enough to handle more complex scenarios.
252
-
253
- - **Multi-Level Tenancy:** For hierarchical tenancy (e.g., Company -> Department), you can apply the primary scope at the engine level and add secondary scoping logic inside your policies' `relation_scope`.
254
- - **Cross-Tenant Data Access:** For resources that can be shared, define a custom `associated_with_...` scope that includes both shared records and records belonging to the current tenant.
255
- - **Tenant Switching:** Build a controller that allows users to change their active tenant by updating a `session` key, then use a session-based scoping strategy to read it.
256
- - **API Multitenancy:** Create a custom scoping strategy (e.g., `:api_tenant`) that authenticates and identifies the tenant based on an API key or JWT from the request headers.