Authorization in Ruby on Rails extends beyond simple user roles and permissions. Complex applications require sophisticated access control mechanisms that adapt to various contexts while remaining maintainable and scalable.
Role hierarchies form the foundation of advanced authorization systems. A well-designed hierarchy allows permissions to flow naturally through organizational structures. Here’s how we implement a basic role hierarchy:
class Role < ApplicationRecord
has_and_belongs_to_many :parents, class_name: 'Role'
has_and_belongs_to_many :children, class_name: 'Role'
def inherits_from?(role)
parents.include?(role) || parents.any? { |p| p.inherits_from?(role) }
end
end
Context-based authorization considers environmental factors when making access decisions. Time, location, device type, and user history can influence permissions:
class ContextualPolicy
def initialize(user, resource, context = {})
@user = user
@resource = resource
@context = context
end
def allowed?
return false unless business_hours?
return false unless authorized_location?
return false if suspicious_activity?
true
end
private
def business_hours?
current_time = Time.current
current_time.on_weekday? && current_time.hour.between?(9, 17)
end
def authorized_location?
GeoService.authorized_location?(@context[:ip_address])
end
end
Resource ownership patterns determine how users relate to resources. We can implement flexible ownership models:
class Resource < ApplicationRecord
belongs_to :owner, class_name: 'User'
has_many :collaborators
has_many :shared_users, through: :collaborators, source: :user
def accessible_by?(user)
return true if owner == user
return true if shared_users.include?(user)
false
end
end
Dynamic permissions adjust based on runtime conditions. This approach offers flexibility for complex business rules:
class DynamicPermission
def initialize(rule_set)
@rule_set = rule_set
end
def evaluate(context)
@rule_set.rules.all? do |rule|
RuleEngine.evaluate(rule, context)
end
end
end
class RuleEngine
def self.evaluate(rule, context)
case rule.condition
when 'time_based'
evaluate_time_rule(rule, context)
when 'quota_based'
evaluate_quota_rule(rule, context)
when 'role_based'
evaluate_role_rule(rule, context)
end
end
end
Policy objects encapsulate authorization logic for specific resource types:
class DocumentPolicy
def initialize(user, document)
@user = user
@document = document
end
def can_edit?
return true if @user.admin?
return true if @document.owner == @user
return true if @user.department == @document.department
false
end
def can_delete?
return true if @user.admin?
return true if @document.owner == @user
false
end
end
Audit logging tracks authorization decisions for security and compliance:
class AuthorizationAudit
def self.log(user, resource, action, result)
AuditLog.create!(
user_id: user.id,
resource_type: resource.class.name,
resource_id: resource.id,
action: action,
result: result,
timestamp: Time.current,
metadata: {
user_roles: user.roles.pluck(:name),
ip_address: Current.ip_address,
user_agent: Current.user_agent
}
)
end
end
Cache strategies improve authorization performance:
class PermissionCache
def self.fetch(user, resource, action)
Rails.cache.fetch(cache_key(user, resource, action), expires_in: 5.minutes) do
calculate_permission(user, resource, action)
end
end
def self.cache_key(user, resource, action)
"permissions:#{user.id}:#{resource.class.name}:#{resource.id}:#{action}"
end
end
Integration with authentication systems strengthens security:
class ApplicationController < ActionController::Base
before_action :verify_authorization
private
def verify_authorization
return if skip_authorization?
authorization = AuthorizationManager.new(current_user, requested_resource)
unless authorization.can_access?
render json: { error: 'Unauthorized' }, status: :forbidden
end
end
end
Testing authorization logic requires comprehensive scenarios:
RSpec.describe DocumentPolicy do
let(:user) { create(:user) }
let(:document) { create(:document) }
describe '#can_edit?' do
context 'when user is admin' do
before { user.update(admin: true) }
it 'allows access' do
policy = DocumentPolicy.new(user, document)
expect(policy.can_edit?).to be true
end
end
context 'when user is document owner' do
let(:document) { create(:document, owner: user) }
it 'allows access' do
policy = DocumentPolicy.new(user, document)
expect(policy.can_edit?).to be true
end
end
end
end
Performance optimization for authorization checks:
class BatchAuthorization
def initialize(user, resources)
@user = user
@resources = resources
@permissions = {}
end
def authorize
preload_permissions
@resources.each_with_object({}) do |resource, results|
results[resource.id] = can_access?(resource)
end
end
private
def preload_permissions
@permissions = Permission.where(
user_id: @user.id,
resource_type: @resources.first.class.name,
resource_id: @resources.pluck(:id)
).index_by(&:resource_id)
end
end
This sophisticated authorization system provides flexibility, security, and maintainability. By combining these techniques, we create robust access control that meets complex business requirements while remaining performant and scalable.