Back to list
radioactive-labs

plutonium-policy

by radioactive-labs

Build production-ready Rails apps in minutes, not days. Convention-driven, fully customizable, AI-ready.

52🍴 7📅 Jan 23, 2026

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 from create?)

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)

MethodInherits FromOverride 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:

  1. Policy method (e.g., bulk_archive?) is checked per record in the selection
  2. Backend: If any selected record fails authorization, the entire request is rejected
  3. UI: Only actions that all selected records support are shown (intersection)
  4. 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)

MethodInherits From
permitted_attributes_for_updatepermitted_attributes_for_create
permitted_attributes_for_indexpermitted_attributes_for_read
permitted_attributes_for_showpermitted_attributes_for_read
permitted_attributes_for_newpermitted_attributes_for_create
permitted_attributes_for_editpermitted_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 via parent.association_name
  • For has_one: scopes via where(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_scope for 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

  1. Always override create? and read? - They default to false
  2. Define attributes explicitly - Auto-detection only works in development
  3. Call super in relation_scope - Preserves entity scoping
  4. Use derived methods - Let update? inherit from create? when appropriate
  5. Keep policies focused - Authorization logic only, no business logic
  6. Test edge cases - Archived records, nil associations, role combinations
  • plutonium-resource - How policies fit in the resource architecture
  • plutonium-definition-actions - Actions that need policy methods
  • plutonium-controller - How controllers use policies

Score

Total Score

75/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon