Authorization
Control what users can do once authenticated. Plutonium uses ActionPolicy with extensions for attribute permissions and tenant scoping.
Goal
For each resource, decide who can create / read / update / destroy / run custom actions, and which fields they can see and edit.
The three layers
Every policy controls three things:
- Action permissions —
create?,read?,update?,destroy?, plus your custom action methods. - Attribute permissions —
permitted_attributes_for_create,_for_read, etc. - Collection scope —
relation_scope(which records show up in lists).
🚨 Critical
create?andread?default tofalse. Always override them explicitly. Derived methods (update?,show?,index?) inherit automatically.permitted_attributes_for_*must be explicit in production. Dev auto-detects; production raises.relation_scopemust calldefault_relation_scope(relation)explicitly — neversuper. See Reference › Behavior › Policies.- Custom action ⇒ policy method.
action :publishneedsdef publish?on the policy. Undefined methods returnfalse→ action silently disappears.
Steps
1. Open the generated policy
After pu:res:scaffold + pu:res:conn, you have:
app/policies/post_policy.rb(base policy)packages/admin_portal/app/policies/admin_portal/post_policy.rb(per-portal override, seeded bypu:res:conn)
2. Override create? and read? explicitly
class PostPolicy < ResourcePolicy
def create? = user.present?
def read? = true
endThese default to false — without an explicit override, nobody can create or read records.
3. Override derived methods only when rules differ
update? inherits from create?. index?/show? inherit from read?. Only override when the rule is genuinely different:
def update?
user.admin? || record.author == user
end
def destroy?
user.admin?
end4. Declare attribute permissions
def permitted_attributes_for_create
%i[title content category]
end
def permitted_attributes_for_read
%i[title content category author published_at created_at]
endIndex has no record
permitted_attributes_for_index runs at collection level — record is nil. If you write a record-dependent _for_read, you MUST also declare an explicit _for_index. See Reference › Behavior › Policies › Index has no record.
5. Custom action methods
def publish?
update? && record.draft?
end
def archive?
user.admin?
endThe method name matches the action name plus ?. Undefined methods return false.
6. Optionally filter the collection — relation_scope
relation_scope do |relation|
default_relation_scope(relation).where(published: true)
end🚨 Always call default_relation_scope(relation) explicitly — not super. Bypassing it triggers verify_default_relation_scope_applied! at runtime.
Common patterns
Owner-based
def update? = record.author == user || user.admin?
def destroy? = update?Role-based
def create? = user.admin? || user.editor?
def update?
return true if user.admin?
user.editor? && record.author == user
endBlock archived records
def update? = !record.try(:archived?) && super
def destroy? = !record.try(:archived?) && superConditional attribute access
def permitted_attributes_for_create
attrs = %i[title content]
attrs += %i[featured author_id] if user.admin?
attrs
endTime-based
def update?
return false if record.created_at < 24.hours.ago
owner?
endBulk action authorization — per record
def bulk_archive?
create? && !record.locked? # checked PER record in the selection
end- Backend: if any selected record fails, the entire request is rejected.
- UI: only actions ALL selected records support are shown (intersection).
Records come from current_authorized_scope — users can only select records they can access.
Portal-specific policies
class PostPolicy < ResourcePolicy
def create? = user.present?
end
# Admin — more permissive
class AdminPortal::PostPolicy < ::PostPolicy
include AdminPortal::ResourcePolicy
def destroy? = true
def permitted_attributes_for_create = %i[title content featured internal_notes]
end
# Public — read-only
class PublicPortal::PostPolicy < ::PostPolicy
include PublicPortal::ResourcePolicy
def create? = false
endShow-page association tabs
def permitted_associations
%i[comments tags author]
endDrives the show-page tablist. Each named association must exist on the model AND be a registered Plutonium resource. See Reference › Behavior › Policies › Association permissions.
Not for nested forms
permitted_associations is for show-page navigation tabs, NOT nested forms. Nested forms come from nested_input :variants in the definition. See Reference › Resource › Definition › Nested inputs.
Multi-tenant scoping
When the portal sets scope_to_entity Organization, the inherited relation_scope automatically filters everything to the current org — no work in the policy. To add filters on top:
relation_scope do |relation|
default_relation_scope(relation).where(archived: false)
endSee Multi-tenancy and Reference › Tenancy › Entity scoping.
Anti-pattern: nested-attributes hashes in policies
# ❌ NEVER
def permitted_attributes_for_create
[:name, {variants_attributes: [:id, :name, :_destroy]}]
endNested params are extracted by the form definition, not the policy. The hash entry renders as a literal text input. Use just the association name:
# ✅ Policy permits just the association name
def permitted_attributes_for_create
[:name, :variants]
endnested_input :variants in the definition handles the rest. See Reference › Resource › Definition › Nested inputs.
Custom authorization context
# Policy
class PostPolicy < ResourcePolicy
authorize :department, allow_nil: true
def create? = department&.allows_posting?
end
# Controller
class PostsController < ResourceController
authorize :department, through: :current_department
private
def current_department = current_user.department
endCommon issues
- Undefined custom action policy method — the button silently disappears (undefined returns
false). Adddef my_action?to the policy. record.Xcrashes during index —recordisnilon index. Add an explicitpermitted_attributes_for_indexthat doesn't depend onrecord.verify_default_relation_scope_applied!raises — your customrelation_scopedoesn't calldefault_relation_scope(relation). Fix by composing:default_relation_scope(relation).where(...).superinrelation_scopedoesn't behave as expected — usedefault_relation_scope(relation)explicitly;super's semantics depend on how ActionPolicy registered the scope.
Related
- Reference › Behavior › Policies — full policy surface
- Reference › Tenancy › Entity scoping —
default_relation_scope, multi-tenant patterns - Authentication — who's the user in the first place
- Multi-tenancy — entity scoping setup
- Custom actions — defining the actions that need policy methods
