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.
- 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
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 fileapp/policies/application_policy.rb- Base policy class
class ApplicationController < ActionController::Base
include SimpleAuthorize::Controller
rescue_from_authorization_errors
endGenerate a policy for your model using the generator:
rails generate simple_authorize:policy PostThis creates:
app/policies/post_policy.rb- Policy class with CRUD methodstest/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
endclass 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
endCheck 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 %>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 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)
endSimpleAuthorize 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))
endSimpleAuthorize provides Rails generators to quickly scaffold policies:
rails generate simple_authorize:installCreates:
config/initializers/simple_authorize.rb- Configuration fileapp/policies/application_policy.rb- Base policy class
rails generate simple_authorize:policy PostCreates:
app/policies/post_policy.rb- Policy with CRUD methods and scopetest/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::PostSimpleAuthorize can be configured in config/initializers/simple_authorize.rb:
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
endSimpleAuthorize 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 objectuser_id: User IDrecord: The record being authorizedrecord_id: Record IDrecord_class: Record class namequery: Authorization method called (e.g., "update?")policy_class: Policy class usedauthorized: Boolean resulterror: Exception if authorization failedcontroller: Controller name (if available)action: Action name (if available)
Policy scope events (policy_scope.simple_authorize):
user: Current user objectuser_id: User IDscope: The scope being filteredpolicy_scope_class: Scope class usederror: Exception if scope failedcontroller: 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
endSimpleAuthorize.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 allows you to build complex authorization policies by combining reusable modules. This promotes DRY code and consistent authorization patterns across your application.
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
endProvides ownership-based authorization:
owner?- Check if user owns the recordowner_or_admin?- Check if user is owner or admincan_modify?- Common pattern for modification rights
For content with draft/published states:
published?- Check if record is publishedcan_publish?- Check if user can publishcan_preview?- Check if user can preview drafts
Time-based authorization:
expired?- Check if record has expiredwithin_time_window?- Check if record is in valid time rangelocked?- Check if record is time-locked
For approval workflows:
approved?- Check if record is approvedcan_approve?- Check if user can approve (not their own content)can_submit_for_approval?- Check if user can submit for approval
For soft deletion support:
soft_deleted?- Check if record is soft deletedcan_restore?- Check if user can restorecan_permanently_destroy?- Check if user can hard delete
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 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.
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
endAccess 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
endclass RegionalContentPolicy < ApplicationPolicy
def show?
allowed_countries = ["US", "CA", "UK"]
allowed_countries.include?(context[:country]) || admin?
end
endclass BusinessHoursPolicy < ApplicationPolicy
def create?
return true if admin?
hour = context[:current_time].hour
hour >= 9 && hour < 17 # 9 AM to 5 PM only
end
endclass ApiPolicy < ApplicationPolicy
def create?
return true if admin?
request_count = context[:request_count] || 0
request_count < 100 # Limit to 100 requests
end
endclass ExportPolicy < ApplicationPolicy
def export?
case context[:user_plan]
when "enterprise"
true
when "pro"
owner_or_admin?
when "basic"
admin?
else
false
end
end
endContext 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
endFor policies that don't correspond to a model:
class DashboardPolicy < ApplicationPolicy
def show?
user&.admin?
end
end
# In controller:
def show
authorize_headless(DashboardPolicy)
endDefine custom authorization queries:
class PostPolicy < ApplicationPolicy
def publish?
user&.admin? || (user&.contributor? && owner?)
end
end
# In controller:
authorize @post, :publish?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
endTest 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
endAfter 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.
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.
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
The gem is available as open source under the terms of the MIT License.
SimpleAuthorize is heavily inspired by Pundit by Elabs. We're grateful to the Pundit team for pioneering this authorization pattern.