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 Entity Scoping
Call super to preserve automatic entity scoping:
relation_scope do |relation|
relation = super(relation) # Apply entity scope first
if user.admin?
relation
else
relation.where(published: true)
end
end
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)
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