SimpleAuthorize
SimpleAuthorize is a lightweight, powerful authorization framework for Rails that provides policy-based access control without external dependencies. Inspired by Pundit, it offers a clean API for managing permissions in your Rails applications.
Features
- Policy-Based Authorization - Define authorization rules in dedicated policy classes
- Scope Filtering - Automatically filter collections based on user permissions
- Role-Based Access - Built-in support for role-based authorization
- Policy Composition - Mix and match reusable authorization modules
- Context-Aware Policies - Make authorization decisions based on request context (IP, time, location, etc.)
- Zero Dependencies - No external gems required (only Rails)
- Strong Parameters Integration - Automatically build permitted params from policies
- Test Friendly - Easy to test policies in isolation
- Rails Generators - Quickly scaffold policies for your models
Installation
Install the gem directly:
gem install simple_authorizeOr add this line to your application's Gemfile:
gem 'simple_authorize'Then execute:
bundle installAfter installation, run the generator to set up your application:
rails generate simple_authorize:installThis will create:
-
config/initializers/simple_authorize.rb- Configuration file -
app/policies/application_policy.rb- Base policy class
Quick Start
1. Include SimpleAuthorize in your ApplicationController
class ApplicationController < ActionController::Base
include SimpleAuthorize::Controller
rescue_from_authorization_errors
end2. Create a Policy
Generate a policy for your model using the generator:
rails generate simple_authorize:policy PostThis creates:
-
app/policies/post_policy.rb- Policy class with CRUD methods -
test/policies/post_policy_test.rb- Test file (or spec file with--spec)
Or create a policy class manually in app/policies/:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def index?
true
end
def show?
true
end
def create?
user.present?
end
def update?
user.present? && (record.user_id == user.id || user.admin?)
end
def destroy?
update?
end
class Scope < ApplicationPolicy::Scope
def resolve
if user&.admin?
scope.all
else
scope.where(published: true)
end
end
end
end3. Use Authorization in Your Controllers
class PostsController < ApplicationController
def index
@posts = policy_scope(Post)
end
def show
@post = Post.find(params[:id])
authorize @post
end
def create
@post = Post.new(post_params)
authorize @post
if @post.save
redirect_to @post
else
render :new
end
end
private
def post_params
params.require(:post).permit(:title, :body, :published)
end
end4. Use in Views
Check permissions in your views:
<% if policy(@post).update? %>
<%= link_to "Edit", edit_post_path(@post) %>
<% end %>
<% if policy(@post).destroy? %>
<%= link_to "Delete", post_path(@post), method: :delete %>
<% end %>Core Concepts
Policies
Policies are plain Ruby objects that encapsulate authorization logic. Each policy corresponds to a model and defines what actions users can perform.
class PostPolicy < ApplicationPolicy
def update?
# Only the owner or an admin can update
user.present? && (record.user_id == user.id || user.admin?)
end
endScopes
Scopes filter collections based on user permissions:
class PostPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
if user&.admin?
scope.all
else
scope.where(published: true)
end
end
end
endUse in controllers:
def index
@posts = policy_scope(Post)
endStrong Parameters
SimpleAuthorize can automatically build permitted parameters from policies:
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user&.admin?
[:title, :body, :published, :featured]
else
[:title, :body]
end
end
endUse in controllers:
def post_params
policy_params(Post, :post)
# Or manually:
# params.require(:post).permit(*permitted_attributes(Post.new))
endGenerators
SimpleAuthorize provides Rails generators to quickly scaffold policies:
Install Generator
rails generate simple_authorize:installCreates:
-
config/initializers/simple_authorize.rb- Configuration file -
app/policies/application_policy.rb- Base policy class
Policy Generator
rails generate simple_authorize:policy PostCreates:
-
app/policies/post_policy.rb- Policy with CRUD methods and scope -
test/policies/post_policy_test.rb- Minitest tests
Options:
-
--spec- Generate RSpec tests instead of Minitest -
--skip-test- Skip test file generation
Examples:
# Generate policy with RSpec tests
rails generate simple_authorize:policy Post --spec
# Generate policy without tests
rails generate simple_authorize:policy Post --skip-test
# Generate namespaced policy
rails generate simple_authorize:policy Admin::PostConfiguration
SimpleAuthorize can be configured in config/initializers/simple_authorize.rb:
Policy Caching
Enable policy caching to improve performance by caching policy instances per request:
SimpleAuthorize.configure do |config|
config.enable_policy_cache = true
endHow it works:
- Policy instances are cached for the duration of a single request
- Cache is automatically scoped by user, record, and policy class
- Each unique combination gets its own cached instance
- Cache is automatically cleared between requests
- Particularly useful in views where the same policy may be checked multiple times
Example performance impact:
<!-- Without caching: Creates 3 separate PostPolicy instances -->
<% if policy(@post).update? %>
<%= link_to "Edit", edit_post_path(@post) %>
<% end %>
<% if policy(@post).destroy? %>
<%= link_to "Delete", post_path(@post) %>
<% end %>
<% if policy(@post).publish? %>
<%= link_to "Publish", publish_post_path(@post) %>
<% end %>
<!-- With caching: Reuses the same PostPolicy instance -->Testing:
Use clear_policy_cache or reset_authorization to clear the cache in tests:
test "multiple checks use cached policy" do
SimpleAuthorize.configure { |config| config.enable_policy_cache = true }
policy1 = policy(@post)
policy2 = policy(@post)
assert_same policy1, policy2 # Same instance
clear_policy_cache
policy3 = policy(@post)
refute_same policy1, policy3 # New instance after clearing
endInstrumentation & Audit Logging
SimpleAuthorize emits ActiveSupport::Notifications events for all authorization checks, perfect for security auditing, debugging, and monitoring:
# Subscribe to authorization events
ActiveSupport::Notifications.subscribe("authorize.simple_authorize") do |name, start, finish, id, payload|
duration = finish - start
Rails.logger.info({
event: "authorization",
user_id: payload[:user_id],
action: payload[:query],
resource: "#{payload[:record_class]}##{payload[:record_id]}",
authorized: payload[:authorized],
duration_ms: (duration * 1000).round(2)
}.to_json)
end
# Subscribe to policy scope events
ActiveSupport::Notifications.subscribe("policy_scope.simple_authorize") do |name, start, finish, id, payload|
Rails.logger.info("Policy scope applied for #{payload[:scope]} by user #{payload[:user_id]}")
endEvent Payloads:
Authorization events (authorize.simple_authorize):
-
user: Current user object -
user_id: User ID -
record: The record being authorized -
record_id: Record ID -
record_class: Record class name -
query: Authorization method called (e.g., "update?") -
policy_class: Policy class used -
authorized: Boolean result -
error: Exception if authorization failed -
controller: Controller name (if available) -
action: Action name (if available)
Policy scope events (policy_scope.simple_authorize):
-
user: Current user object -
user_id: User ID -
scope: The scope being filtered -
policy_scope_class: Scope class used -
error: Exception if scope failed -
controller: Controller name (if available) -
action: Action name (if available)
Use Cases:
- Security auditing and compliance
- Debugging authorization issues
- Monitoring authorization performance
- Sending failed authorization attempts to security services
- Tracking which users access sensitive resources
Disable instrumentation (if needed for performance in specific scenarios):
SimpleAuthorize.configure do |config|
config.enable_instrumentation = false
endOther Configuration Options
SimpleAuthorize.configure do |config|
# Custom error message for unauthorized access
config.default_error_message = "Access denied!"
# Custom redirect path for unauthorized users
config.unauthorized_redirect_path = "/access-denied"
# Custom method to get current user (default: current_user)
config.current_user_method = :authenticated_user
endPolicy Composition
Policy Composition allows you to build complex authorization policies by combining reusable modules. This promotes DRY code and consistent authorization patterns across your application.
Using Built-in Policy Modules
SimpleAuthorize provides several ready-to-use policy modules:
class ArticlePolicy < ApplicationPolicy
include SimpleAuthorize::PolicyModules::Ownable
include SimpleAuthorize::PolicyModules::Publishable
def show?
published? || owner_or_admin?
end
def update?
owner_or_admin? && not_published?
end
endAvailable Policy Modules
Ownable
Provides ownership-based authorization:
-
owner?- Check if user owns the record -
owner_or_admin?- Check if user is owner or admin -
can_modify?- Common pattern for modification rights
Publishable
For content with draft/published states:
-
published?- Check if record is published -
can_publish?- Check if user can publish -
can_preview?- Check if user can preview drafts
Timestamped
Time-based authorization:
-
expired?- Check if record has expired -
within_time_window?- Check if record is in valid time range -
locked?- Check if record is time-locked
Approvable
For approval workflows:
-
approved?- Check if record is approved -
can_approve?- Check if user can approve (not their own content) -
can_submit_for_approval?- Check if user can submit for approval
SoftDeletable
For soft deletion support:
-
soft_deleted?- Check if record is soft deleted -
can_restore?- Check if user can restore -
can_permanently_destroy?- Check if user can hard delete
Creating Custom Policy Modules
module MyApp::PolicyModules::Subscribable
protected
def subscriber?
user&.subscriptions&.active&.any?
end
def premium_subscriber?
user&.subscription&.premium?
end
def can_access_premium_content?
premium_subscriber? || admin?
end
end
class PremiumContentPolicy < ApplicationPolicy
include MyApp::PolicyModules::Subscribable
def show?
can_access_premium_content?
end
endContext-Aware Policies
Context-Aware Policies allow you to make authorization decisions based on additional context beyond just the user and record. This is useful for IP-based restrictions, time-based access, rate limiting, and more.
Basic Usage
Override the authorization_context method in your controller:
class ApplicationController < ActionController::Base
include SimpleAuthorize::Controller
private
def authorization_context
{
ip_address: request.remote_ip,
user_agent: request.user_agent,
current_time: Time.current,
country: request.location&.country,
two_factor_verified: session[:two_factor_verified],
user_plan: current_user&.subscription&.plan
}
end
endUsing Context in Policies
Access context in your policies through the context method:
class SecureDocumentPolicy < ApplicationPolicy
def show?
# Require 2FA for sensitive documents
return false unless context[:two_factor_verified]
# Check IP restrictions
return false unless trusted_ip?
owner_or_admin?
end
private
def trusted_ip?
return true if context[:ip_address].nil?
trusted_ips = ["192.168.1.0/24", "10.0.0.0/8"]
trusted_ips.any? { |range| IPAddr.new(range).include?(context[:ip_address]) }
end
endCommon Context Patterns
Geographic Restrictions
class RegionalContentPolicy < ApplicationPolicy
def show?
allowed_countries = ["US", "CA", "UK"]
allowed_countries.include?(context[:country]) || admin?
end
endTime-Based Access
class BusinessHoursPolicy < ApplicationPolicy
def create?
return true if admin?
hour = context[:current_time].hour
hour >= 9 && hour < 17 # 9 AM to 5 PM only
end
endRate Limiting
class ApiPolicy < ApplicationPolicy
def create?
return true if admin?
request_count = context[:request_count] || 0
request_count < 100 # Limit to 100 requests
end
endPlan-Based Features
class ExportPolicy < ApplicationPolicy
def export?
case context[:user_plan]
when "enterprise"
true
when "pro"
owner_or_admin?
when "basic"
admin?
else
false
end
end
endContext with Policy Scopes
Context is also available in policy scopes:
class DocumentPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
if context[:department]
scope.where(department: context[:department])
elsif user.admin?
scope.all
else
scope.where(user: user)
end
end
end
endAdvanced Features
Headless Policies
For policies that don't correspond to a model:
class DashboardPolicy < ApplicationPolicy
def show?
user&.admin?
end
end
# In controller:
def show
authorize_headless(DashboardPolicy)
endCustom Query Methods
Define custom authorization queries:
class PostPolicy < ApplicationPolicy
def publish?
user&.admin? || (user&.contributor? && owner?)
end
end
# In controller:
authorize @post, :publish?Automatic Verification
Ensure every action is authorized:
class ApplicationController < ActionController::Base
include SimpleAuthorize::Controller
include SimpleAuthorize::Controller::AutoVerify # Enable auto-verification
rescue_from_authorization_errors
endThis will require authorize or policy_scope in all actions.
Skip verification when needed:
class PublicController < ApplicationController
skip_authorization_check :index, :show
endTesting
Test policies in isolation:
require 'test_helper'
class PostPolicyTest < ActiveSupport::TestCase
test "admin can update any post" do
admin = users(:admin)
post = posts(:one)
policy = PostPolicy.new(admin, post)
assert policy.update?
end
test "user can only update their own posts" do
user = users(:regular)
own_post = posts(:user_post)
other_post = posts(:other_post)
assert PostPolicy.new(user, own_post).update?
refute PostPolicy.new(user, other_post).update?
end
endDevelopment
After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install.
Comparison with Pundit
SimpleAuthorize is heavily inspired by Pundit and offers a similar API. Key differences:
| Feature | SimpleAuthorize | Pundit |
|---|---|---|
| Dependencies | None (Rails only) | Standalone gem |
| Base class | SimpleAuthorize::Policy |
ApplicationPolicy (user-defined) |
| Installation | Generator creates base policy | Manual setup required |
| Module name | SimpleAuthorize::Controller |
Pundit |
| Compatibility | Rails 6.0+ | Rails 4.0+ |
Migration from Pundit is straightforward - most code will work with minimal changes.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/scottlaplant/simple_authorize.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
License
The gem is available as open source under the terms of the MIT License.
Credits
SimpleAuthorize is heavily inspired by Pundit by Elabs. We're grateful to the Pundit team for pioneering this authorization pattern.