
plutonium-policy
by radioactive-labs
Build production-ready Rails apps in minutes, not days. Convention-driven, fully customizable, AI-ready.
SKILL.md
name: plutonium-policy description: Plutonium resource policies - authorization, attribute permissions, and scoping
Plutonium Policies
Policies control WHO can do WHAT with resources. Built on ActionPolicy.
Plutonium extends ActionPolicy with:
- Attribute permissions (
permitted_attributes_for_*) - Association permissions (
permitted_associations) - Automatic entity scoping for multi-tenancy
- Derived action methods (e.g.,
update?inherits fromcreate?)
Base Class
# app/policies/resource_policy.rb (generated during install)
class ResourcePolicy < Plutonium::Resource::Policy
# App-wide authorization defaults
end
# app/policies/post_policy.rb (per resource)
class PostPolicy < ResourcePolicy
def create?
user.present?
end
def read?
true
end
def permitted_attributes_for_create
%i[title content]
end
def permitted_attributes_for_read
%i[title content author created_at]
end
end
Action Permissions
Core Actions (Must Override)
def create? # Default: false - MUST override
user.present?
end
def read? # Default: false - MUST override
true
end
Derived Actions (Inherit by Default)
| Method | Inherits From | Override When |
|---|---|---|
update? | create? | Different update rules |
destroy? | create? | Different delete rules |
index? | read? | Custom listing rules |
show? | read? | Record-specific read rules |
new? | create? | Rarely needed |
edit? | update? | Rarely needed |
search? | index? | Search-specific rules |
Custom Actions
Define methods matching your action names:
def publish?
update? && record.draft?
end
def archive?
create? && !record.archived?
end
def invite_user?
user.admin?
end
Actions are secure by default - undefined methods return false.
Bulk Action Authorization
Bulk actions (operating on multiple selected records) support per-record authorization:
def bulk_archive?
create? && !record.locked? # Per-record check
end
def bulk_publish?
user.admin? || record.author == user
end
How bulk authorization works:
- Policy method (e.g.,
bulk_archive?) is checked per record in the selection - Backend: If any selected record fails authorization, the entire request is rejected
- UI: Only actions that all selected records support are shown (intersection)
- Records are fetched via
current_authorized_scope- only accessible records can be selected
This provides full per-record authorization while keeping the UI clean - users only see actions they can actually perform on their entire selection.
Attribute Permissions
Core Methods (Must Override for Production)
# What users can see (index, show)
def permitted_attributes_for_read
%i[title content author published_at created_at]
end
# What users can set (create, update)
def permitted_attributes_for_create
%i[title content]
end
Derived Methods (Inherit by Default)
| Method | Inherits From |
|---|---|
permitted_attributes_for_update | permitted_attributes_for_create |
permitted_attributes_for_index | permitted_attributes_for_read |
permitted_attributes_for_show | permitted_attributes_for_read |
permitted_attributes_for_new | permitted_attributes_for_create |
permitted_attributes_for_edit | permitted_attributes_for_update |
Per-Action Attributes
Show different fields for different views:
def permitted_attributes_for_index
%i[title author created_at] # Minimal for list
end
def permitted_attributes_for_read
%i[title content author tags created_at updated_at] # Full for detail
end
Auto-Detection (Development Only)
In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly.
Association Permissions
Control which associations can be rendered:
def permitted_associations
%i[comments tags author]
end
Used for:
- Nested forms
- Related data displays
- Association fields in tables
Collection Scoping
Filter which records users can see:
relation_scope do |relation|
if user.admin?
relation
else
relation.where(author: user)
end
end
With Parent Scoping (Nested Resources)
For nested resources, call super to apply automatic parent scoping:
relation_scope do |relation|
relation = super(relation) # Applies parent scoping automatically
if user.admin?
relation
else
relation.where(published: true)
end
end
Parent scoping takes precedence over entity scoping. When a parent is present:
- For
has_many: scopes viaparent.association_name - For
has_one: scopes viawhere(foreign_key: parent.id)
With Entity Scoping (Multi-tenancy)
When no parent is present, super applies entity scoping:
relation_scope do |relation|
relation = super(relation) # Apply entity scope if no parent
if user.admin?
relation
else
relation.where(published: true)
end
end
default_relation_scope is Required
Plutonium verifies that default_relation_scope is called in every relation_scope. This prevents accidental multi-tenancy leaks when overriding scopes.
# ❌ This will raise an error
relation_scope do |relation|
relation.where(published: true) # Missing default_relation_scope!
end
# ✅ Correct - call default_relation_scope
relation_scope do |relation|
default_relation_scope(relation).where(published: true)
end
# ✅ Also correct - super calls default_relation_scope
relation_scope do |relation|
super(relation).where(published: true)
end
When overriding an inherited scope:
class AdminPostPolicy < PostPolicy
relation_scope do |relation|
# Replace inherited scope but keep Plutonium's parent/entity scoping
default_relation_scope(relation)
end
end
Skipping Default Scoping (Rare)
If you intentionally need to skip scoping, call skip_default_relation_scope!:
relation_scope do |relation|
skip_default_relation_scope!
relation # No parent/entity scoping applied
end
Consider using a separate portal instead of skipping scoping.
Portal-Specific Policies
Override policies per portal:
# Base policy
class PostPolicy < ResourcePolicy
def create?
user.present?
end
end
# Admin portal - more permissive
class AdminPortal::PostPolicy < ::PostPolicy
include AdminPortal::ResourcePolicy
def destroy?
true # Admins can always delete
end
def permitted_attributes_for_create
%i[title content featured internal_notes] # More fields
end
end
# Public portal - restricted
class PublicPortal::PostPolicy < ::PostPolicy
include PublicPortal::ResourcePolicy
def create?
false # No public creation
end
end
Common Patterns
Check Model Capabilities
def archive?
return false unless record.respond_to?(:archived!)
return false if record.archived?
user.admin?
end
Prevent Actions on Archived Records
def update?
return false if record.try(:archived?)
super
end
def destroy?
return false if record.try(:archived?)
super
end
Owner-Based Permissions
def update?
record.author == user || user.admin?
end
def destroy?
update? # Same rules as update
end
Role-Based Permissions
def create?
user.admin? || user.editor?
end
def read?
true # Everyone can read
end
def update?
return true if user.admin?
return true if user.editor? && record.author == user
false
end
Conditional Attribute Access
def permitted_attributes_for_create
attrs = %i[title content]
attrs << :featured if user.admin?
attrs << :author_id if user.admin? # Only admins can set author
attrs
end
Authorization Context
Policies have access to:
user # Current user (required)
record # The resource being authorized
entity_scope # Current scoped entity (for multi-tenancy)
parent # Parent record for nested resources (nil if not nested)
parent_association # Association name on parent (e.g., :comments)
Nested Resource Context
For nested resources (e.g., /posts/123/nested_comments), the policy receives:
class CommentPolicy < ResourcePolicy
def create?
# parent is the Post instance
# parent_association is :comments
parent.present? && user.can_comment_on?(parent)
end
relation_scope do |relation|
# super() uses parent and parent_association for scoping
relation = super(relation)
relation
end
end
Custom Context
Add custom context in controllers:
# In policy
class PostPolicy < ResourcePolicy
authorize :department, allow_nil: true
def create?
department&.allows_posting?
end
end
# In controller
class PostsController < ResourceController
authorize :department, through: :current_department
private
def current_department
current_user.department
end
end
Controller Integration
Built-in CRUD actions automatically:
- Call
authorize_current!at the start of each action - Apply
relation_scopefor index/listings - Filter params through
permitted_attributes
After-action callbacks verify authorization was performed - if you add custom actions, you must call authorize_current! yourself or skip verification.
Skip Verification (When Needed)
class PostsController < ResourceController
skip_verify_authorize_current only: [:custom_action]
def custom_action
# Handle authorization manually
end
end
Best Practices
- Always override
create?andread?- They default tofalse - Define attributes explicitly - Auto-detection only works in development
- Call
superinrelation_scope- Preserves entity scoping - Use derived methods - Let
update?inherit fromcreate?when appropriate - Keep policies focused - Authorization logic only, no business logic
- Test edge cases - Archived records, nil associations, role combinations
Related Skills
plutonium-resource- How policies fit in the resource architectureplutonium-definition-actions- Actions that need policy methodsplutonium-controller- How controllers use policies
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon
