Policy Reference
Complete reference for authorization policies. Built on ActionPolicy.
Overview
Policies control authorization at three levels:
- Action Permissions - Can user perform this action?
- Attribute Permissions - Which fields can user access?
- Scope Permissions - Which records can user see?
Base Class
class PostPolicy < Plutonium::Resource::Policy
# Policy code
endIn packages, inherit from the package's ResourcePolicy:
module AdminPortal
class PostPolicy < ::PostPolicy
# Portal-specific overrides
end
endAuthorization Context
Inside a policy, you have access to:
| Variable | Description |
|---|---|
user | Current authenticated user (required) |
record | 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) |
def update?
user # => Current user
record # => The Post instance
entity_scope # => Organization for multi-tenant portals
parent # => Parent record (for nested routes)
parent_association # => :comments (association name)
endAction Permissions
Core Actions (Must Override)
These default to false - you must override them:
class PostPolicy < Plutonium::Resource::Policy
def create?
user.present?
end
def read?
true
end
endDerived Actions
These inherit from core actions 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 |
Example with Ownership
class PostPolicy < Plutonium::Resource::Policy
def create?
user.present?
end
def read?
true
end
def update?
owner? || admin?
end
def destroy?
owner? || admin?
end
private
def owner?
record.user_id == user.id
end
def admin?
user.admin?
end
endCustom Action Permissions
For custom actions defined in definitions:
def publish?
owner? && !record.published?
end
def archive?
owner? || admin?
end
def bulk_delete?
admin?
endActions are secure by default - undefined methods return false.
Attribute Permissions
Core Methods (Must Override for Production)
# What users can see (index, show)
def permitted_attributes_for_read
%i[title body author created_at]
end
# What users can set (create, update)
def permitted_attributes_for_create
%i[title body category_id]
endDerived Methods
| 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 |
Conditional Attribute Access
def permitted_attributes_for_create
attrs = %i[title body]
attrs << :featured if user.admin?
attrs << :author_id if user.admin?
attrs
end
def permitted_attributes_for_update
case record.status
when 'draft'
%i[title body category_id]
when 'published'
%i[body] # Can only edit body once published
else
[]
end
endAuto-Detection (Development Only)
In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly:
🚨 Resource field auto-detection: PostPolicy#permitted_attributes_for_create
Auto-detected resource fields result in security holes and will fail outside of development.Association Permissions
Control which associations appear in panels and forms:
def permitted_associations
%i[comments tags author]
endReturns an empty array by default.
Collection Scoping
relation_scope
Filter which records users can see using ActionPolicy's relation_scope:
class PostPolicy < Plutonium::Resource::Policy
relation_scope do |relation|
if user.admin?
relation
else
relation.where(published: true).or(
relation.where(user_id: user.id)
)
end
end
endWith Parent Scoping (Nested Resources)
Call super to apply automatic parent scoping for nested resources:
relation_scope do |relation|
relation = super(relation) # Applies parent scoping automatically
if user.admin?
relation
else
relation.where(approved: true)
end
endParent scoping takes precedence over entity scoping. When a parent is present:
- For
has_manyassociations: scopes viaparent.association_name - For
has_oneassociations: 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) # Applies associated_with(entity_scope)
if user.admin?
relation
else
relation.where(published: true)
end
endThe default relation_scope automatically applies relation.associated_with(entity_scope) when an entity scope is present and no parent is set.
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)
endWhen 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
endThis method applies parent scoping (for nested resources) or entity scoping (for multi-tenancy) directly, bypassing any inherited scope customizations.
Skipping Default Scoping
If you intentionally need to bypass scoping, call skip_default_relation_scope!:
relation_scope do |relation|
skip_default_relation_scope!
relation # No parent/entity scoping applied
endThis should be rare - consider using a separate portal with different scoping rules instead.
Portal-Specific Policies
Override policies for specific portals:
# packages/admin_portal/app/policies/admin_portal/post_policy.rb
module AdminPortal
class PostPolicy < ::PostPolicy
def destroy?
true # Admins can delete any post
end
def permitted_attributes_for_create
%i[title body featured internal_notes] # More fields
end
relation_scope do |relation|
relation # No restrictions for admins
end
end
endCustom Authorization Context
Add custom context using ActionPolicy's authorize directive:
# In policy
class PostPolicy < Plutonium::Resource::Policy
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
endAuthorization Errors
When authorization fails:
# Raises ActionPolicy::UnauthorizedHandling Errors
# app/controllers/application_controller.rb
rescue_from ActionPolicy::Unauthorized do |exception|
redirect_to root_path, alert: "Not authorized"
endCommon Patterns
Role-Based
def update?
case user.role
when 'admin' then true
when 'editor' then true
when 'author' then owner?
else false
end
endStatus-Based
def update?
return false if record.archived?
owner? || admin?
endTime-Based
def update?
return false if record.created_at < 24.hours.ago
owner?
endHierarchical
def read?
return true if admin?
return true if manager_of_department?
return true if owner?
record.public?
endDebugging
Logging
def update?
result = owner? || admin?
Rails.logger.debug { "PostPolicy#update? user=#{user.id} post=#{record.id}: #{result}" }
result
endConsole Testing
user = User.find(1)
post = Post.find(1)
# Use ActionPolicy's testing helpers
policy = PostPolicy.new(post, user: user)
policy.update?
policy.permitted_attributes_for_updateBest 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
