Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:07 +08:00
commit 80987b1934
15 changed files with 8486 additions and 0 deletions

View File

@@ -0,0 +1,841 @@
---
name: rack-middleware
description: Rack middleware development, configuration, and integration patterns. Use when working with middleware stacks or creating custom middleware.
---
# Rack Middleware Skill
## Tier 1: Quick Reference - Middleware Basics
### Middleware Structure
```ruby
class MyMiddleware
def initialize(app, options = {})
@app = app
@options = options
end
def call(env)
# Before request
# Modify env if needed
# Call next middleware
status, headers, body = @app.call(env)
# After request
# Modify response if needed
[status, headers, body]
end
end
# Usage
use MyMiddleware, option: 'value'
```
### Common Middleware
```ruby
# Session management
use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
# Security
use Rack::Protection
# Compression
use Rack::Deflater
# Logging
use Rack::CommonLogger
# Static files
use Rack::Static, urls: ['/css', '/js'], root: 'public'
```
### Middleware Ordering
```ruby
# config.ru - Correct order
use Rack::Deflater # 1. Compression
use Rack::Static # 2. Static files
use Rack::CommonLogger # 3. Logging
use Rack::Session::Cookie # 4. Sessions
use Rack::Protection # 5. Security
use CustomAuth # 6. Authentication
run Application # 7. Application
```
### Request/Response Access
```ruby
class SimpleMiddleware
def initialize(app)
@app = app
end
def call(env)
# Access request via env hash
method = env['REQUEST_METHOD']
path = env['PATH_INFO']
query = env['QUERY_STRING']
# Or use Rack::Request
request = Rack::Request.new(env)
params = request.params
# Process request
status, headers, body = @app.call(env)
# Modify response
headers['X-Custom-Header'] = 'value'
[status, headers, body]
end
end
```
---
## Tier 2: Detailed Instructions - Advanced Middleware
### Custom Middleware Development
**Request Logging Middleware:**
```ruby
require 'logger'
class RequestLogger
def initialize(app, options = {})
@app = app
@logger = options[:logger] || Logger.new(STDOUT)
@skip_paths = options[:skip_paths] || []
end
def call(env)
return @app.call(env) if skip_logging?(env)
start_time = Time.now
request = Rack::Request.new(env)
log_request_start(request)
status, headers, body = @app.call(env)
duration = Time.now - start_time
log_request_end(request, status, duration)
[status, headers, body]
rescue StandardError => e
log_error(request, e)
raise
end
private
def skip_logging?(env)
path = env['PATH_INFO']
@skip_paths.any? { |skip| path.start_with?(skip) }
end
def log_request_start(request)
@logger.info({
event: 'request.start',
method: request.request_method,
path: request.path,
ip: request.ip,
user_agent: request.user_agent
}.to_json)
end
def log_request_end(request, status, duration)
@logger.info({
event: 'request.end',
method: request.request_method,
path: request.path,
status: status,
duration: duration.round(3)
}.to_json)
end
def log_error(request, error)
@logger.error({
event: 'request.error',
method: request.request_method,
path: request.path,
error: error.class.name,
message: error.message,
backtrace: error.backtrace[0..5]
}.to_json)
end
end
# Usage
use RequestLogger, skip_paths: ['/health', '/metrics']
```
**Authentication Middleware:**
```ruby
class TokenAuthentication
def initialize(app, options = {})
@app = app
@token_header = options[:header] || 'HTTP_AUTHORIZATION'
@skip_paths = options[:skip_paths] || []
@realm = options[:realm] || 'Application'
end
def call(env)
return @app.call(env) if skip_authentication?(env)
token = extract_token(env)
if valid_token?(token)
user = find_user_by_token(token)
env['current_user'] = user
@app.call(env)
else
unauthorized_response
end
end
private
def skip_authentication?(env)
path = env['PATH_INFO']
method = env['REQUEST_METHOD']
# Skip for public paths
@skip_paths.any? { |skip| path.start_with?(skip) } ||
# Skip for OPTIONS (CORS preflight)
method == 'OPTIONS'
end
def extract_token(env)
auth_header = env[@token_header]
return nil unless auth_header
# Support "Bearer TOKEN" format
if auth_header.start_with?('Bearer ')
auth_header.split(' ', 2).last
else
auth_header
end
end
def valid_token?(token)
return false unless token
# Implement your token validation logic
# This is a placeholder
token.length >= 32
end
def find_user_by_token(token)
# Implement your user lookup logic
# This is a placeholder
{ id: 1, email: 'user@example.com' }
end
def unauthorized_response
[
401,
{
'Content-Type' => 'application/json',
'WWW-Authenticate' => "Bearer realm=\"#{@realm}\""
},
['{"error": "Unauthorized"}']
]
end
end
# Usage
use TokenAuthentication,
skip_paths: ['/login', '/register', '/public']
```
**Caching Middleware:**
```ruby
require 'digest/md5'
class SimpleCache
def initialize(app, options = {})
@app = app
@cache = {}
@ttl = options[:ttl] || 300 # 5 minutes
@cache_methods = options[:methods] || ['GET']
end
def call(env)
request = Rack::Request.new(env)
return @app.call(env) unless cacheable?(request)
cache_key = generate_cache_key(env)
if cached_response = get_from_cache(cache_key)
return cached_response
end
status, headers, body = @app.call(env)
if cacheable_response?(status)
cache_response(cache_key, [status, headers, body])
end
[status, headers, body]
end
private
def cacheable?(request)
@cache_methods.include?(request.request_method)
end
def cacheable_response?(status)
status == 200
end
def generate_cache_key(env)
# Include method, path, and query string
Digest::MD5.hexdigest([
env['REQUEST_METHOD'],
env['PATH_INFO'],
env['QUERY_STRING']
].join('|'))
end
def get_from_cache(key)
entry = @cache[key]
return nil unless entry
# Check if cache entry is still valid
if Time.now - entry[:cached_at] <= @ttl
entry[:response]
else
@cache.delete(key)
nil
end
end
def cache_response(key, response)
@cache[key] = {
response: response,
cached_at: Time.now
}
end
end
# Usage with Redis for distributed caching
class RedisCache
def initialize(app, options = {})
@app = app
@redis = Redis.new(url: options[:redis_url])
@ttl = options[:ttl] || 300
@namespace = options[:namespace] || 'cache'
end
def call(env)
request = Rack::Request.new(env)
return @app.call(env) unless request.get?
cache_key = generate_cache_key(env)
if cached = @redis.get(cache_key)
return Marshal.load(cached)
end
status, headers, body = @app.call(env)
if status == 200
@redis.setex(cache_key, @ttl, Marshal.dump([status, headers, body]))
end
[status, headers, body]
end
private
def generate_cache_key(env)
"#{@namespace}:#{Digest::MD5.hexdigest(env['PATH_INFO'] + env['QUERY_STRING'])}"
end
end
```
**Request Transformation Middleware:**
```ruby
class JSONBodyParser
def initialize(app)
@app = app
end
def call(env)
if json_request?(env)
body = env['rack.input'].read
env['rack.input'].rewind
begin
parsed = JSON.parse(body)
env['rack.request.form_hash'] = parsed
env['parsed_json'] = parsed
rescue JSON::ParserError => e
return error_response('Invalid JSON', 400)
end
end
@app.call(env)
end
private
def json_request?(env)
content_type = env['CONTENT_TYPE']
content_type && content_type.include?('application/json')
end
def error_response(message, status)
[
status,
{ 'Content-Type' => 'application/json' },
[{ error: message }.to_json]
]
end
end
# XML Parser
class XMLBodyParser
def initialize(app)
@app = app
end
def call(env)
if xml_request?(env)
body = env['rack.input'].read
env['rack.input'].rewind
begin
parsed = Hash.from_xml(body)
env['rack.request.form_hash'] = parsed
env['parsed_xml'] = parsed
rescue StandardError => e
return error_response('Invalid XML', 400)
end
end
@app.call(env)
end
private
def xml_request?(env)
content_type = env['CONTENT_TYPE']
content_type && (content_type.include?('application/xml') ||
content_type.include?('text/xml'))
end
def error_response(message, status)
[
status,
{ 'Content-Type' => 'application/json' },
[{ error: message }.to_json]
]
end
end
```
### Middleware Ordering Patterns
**Security-First Stack:**
```ruby
# config.ru
# 1. SSL redirect (production only)
use Rack::SSL if ENV['RACK_ENV'] == 'production'
# 2. Rate limiting (before everything else)
use Rack::Attack
# 3. Security headers
use SecurityHeaders
# 4. CORS (for API applications)
use Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :post, :put, :delete, :options]
end
end
# 5. Compression
use Rack::Deflater
# 6. Static files
use Rack::Static, urls: ['/public'], root: 'public'
# 7. Logging
use Rack::CommonLogger
# 8. Request parsing
use JSONBodyParser
# 9. Sessions
use Rack::Session::Cookie,
secret: ENV['SESSION_SECRET'],
same_site: :strict,
httponly: true,
secure: ENV['RACK_ENV'] == 'production'
# 10. Protection (CSRF, etc.)
use Rack::Protection
# 11. Authentication
use TokenAuthentication, skip_paths: ['/login', '/public']
# 12. Performance monitoring
use PerformanceMonitor
# 13. Application
run Application
```
**API-Focused Stack:**
```ruby
# config.ru for API
# 1. CORS first for preflight
use Rack::Cors do
allow do
origins ENV.fetch('ALLOWED_ORIGINS', '*').split(',')
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options],
credentials: true,
max_age: 86400
end
end
# 2. Rate limiting
use Rack::Attack
# 3. Compression
use Rack::Deflater
# 4. Logging (structured JSON logs)
use RequestLogger
# 5. Request parsing
use JSONBodyParser
# 6. Authentication
use TokenAuthentication, skip_paths: ['/auth']
# 7. Caching
use RedisCache, ttl: 300
# 8. Application
run API
```
### Conditional Middleware
**Environment-Based:**
```ruby
class ConditionalMiddleware
def initialize(app, condition, middleware, *args)
@app = if condition.call
middleware.new(app, *args)
else
app
end
end
def call(env)
@app.call(env)
end
end
# Usage
use ConditionalMiddleware,
-> { ENV['RACK_ENV'] == 'development' },
Rack::ShowExceptions
use ConditionalMiddleware,
-> { ENV['ENABLE_PROFILING'] == 'true' },
RackMiniProfiler
```
**Path-Based:**
```ruby
class PathBasedMiddleware
def initialize(app, pattern, middleware, *args)
@app = app
@pattern = pattern
@middleware = middleware.new(app, *args)
end
def call(env)
if env['PATH_INFO'].match?(@pattern)
@middleware.call(env)
else
@app.call(env)
end
end
end
# Usage
use PathBasedMiddleware, %r{^/api}, CacheMiddleware, ttl: 300
use PathBasedMiddleware, %r{^/admin}, AdminAuth
```
### Error Handling Middleware
```ruby
class ErrorHandler
def initialize(app, options = {})
@app = app
@logger = options[:logger] || Logger.new(STDOUT)
@error_handlers = options[:handlers] || {}
end
def call(env)
@app.call(env)
rescue StandardError => e
handle_error(env, e)
end
private
def handle_error(env, error)
request = Rack::Request.new(env)
# Log error
@logger.error({
error: error.class.name,
message: error.message,
path: request.path,
method: request.request_method,
backtrace: error.backtrace[0..10]
}.to_json)
# Custom handler for specific error types
if handler = @error_handlers[error.class]
return handler.call(error)
end
# Default error response
status = status_for_error(error)
[
status,
{ 'Content-Type' => 'application/json' },
[{ error: error.message, type: error.class.name }.to_json]
]
end
def status_for_error(error)
case error
when ArgumentError, ValidationError
400
when NotFoundError
404
when AuthorizationError
403
when AuthenticationError
401
else
500
end
end
end
# Usage
use ErrorHandler,
handlers: {
ValidationError => ->(e) {
[422, { 'Content-Type' => 'application/json' },
[{ error: e.message, details: e.details }.to_json]]
}
}
```
---
## Tier 3: Resources & Examples
### Complete Middleware Examples
**Performance Monitoring:**
```ruby
class PerformanceMonitor
def initialize(app, options = {})
@app = app
@threshold = options[:threshold] || 1.0 # 1 second
@logger = options[:logger] || Logger.new(STDOUT)
end
def call(env)
start_time = Time.now
memory_before = memory_usage
status, headers, body = @app.call(env)
duration = Time.now - start_time
memory_after = memory_usage
memory_delta = memory_after - memory_before
# Add performance headers
headers['X-Runtime'] = duration.to_s
headers['X-Memory-Delta'] = memory_delta.to_s
# Log slow requests
if duration > @threshold
log_slow_request(env, duration, memory_delta)
end
[status, headers, body]
end
private
def memory_usage
`ps -o rss= -p #{Process.pid}`.to_i / 1024.0 # MB
end
def log_slow_request(env, duration, memory)
@logger.warn({
event: 'slow_request',
method: env['REQUEST_METHOD'],
path: env['PATH_INFO'],
duration: duration.round(3),
memory_delta: memory.round(2)
}.to_json)
end
end
```
**Request ID Tracking:**
```ruby
class RequestID
def initialize(app, options = {})
@app = app
@header = options[:header] || 'X-Request-ID'
end
def call(env)
request_id = env["HTTP_#{@header.upcase.tr('-', '_')}"] || generate_id
env['request.id'] = request_id
status, headers, body = @app.call(env)
headers[@header] = request_id
[status, headers, body]
end
private
def generate_id
SecureRandom.uuid
end
end
```
**Response Modification:**
```ruby
class ResponseTransformer
def initialize(app, &block)
@app = app
@transformer = block
end
def call(env)
status, headers, body = @app.call(env)
if should_transform?(headers)
body = transform_body(body)
end
[status, headers, body]
end
private
def should_transform?(headers)
headers['Content-Type']&.include?('application/json')
end
def transform_body(body)
content = body.is_a?(Array) ? body.join : body.read
transformed = @transformer.call(content)
[transformed]
end
end
# Usage
use ResponseTransformer do |body|
data = JSON.parse(body)
data['timestamp'] = Time.now.to_i
data.to_json
end
```
### Testing Middleware
```ruby
RSpec.describe RequestLogger do
let(:app) { ->(env) { [200, {}, ['OK']] } }
let(:logger) { double('Logger', info: nil, error: nil) }
let(:middleware) { RequestLogger.new(app, logger: logger) }
let(:request) { Rack::MockRequest.new(middleware) }
describe 'request logging' do
it 'logs request start' do
expect(logger).to receive(:info).with(hash_including(event: 'request.start'))
request.get('/')
end
it 'logs request end with duration' do
expect(logger).to receive(:info).with(hash_including(
event: 'request.end',
duration: kind_of(Numeric)
))
request.get('/')
end
it 'includes request details' do
expect(logger).to receive(:info).with(hash_including(
method: 'GET',
path: '/test'
))
request.get('/test')
end
end
describe 'error logging' do
let(:app) { ->(env) { raise StandardError, 'Test error' } }
it 'logs errors' do
expect(logger).to receive(:error).with(hash_including(
event: 'request.error',
error: 'StandardError'
))
expect { request.get('/') }.to raise_error(StandardError)
end
end
describe 'skip paths' do
let(:middleware) { RequestLogger.new(app, logger: logger, skip_paths: ['/health']) }
it 'skips logging for configured paths' do
expect(logger).not_to receive(:info)
request.get('/health')
end
end
end
```
### Additional Resources
- **Middleware Template:** `assets/middleware-template.rb` - Boilerplate for new middleware
- **Middleware Examples:** `assets/middleware-examples/` - Collection of useful middleware
- **Configuration Guide:** `assets/configuration-guide.md` - Best practices for middleware configuration
- **Performance Guide:** `references/performance-optimization.md` - Optimizing middleware performance
- **Testing Guide:** `references/middleware-testing.md` - Comprehensive testing strategies

View File

@@ -0,0 +1,854 @@
---
name: ruby-patterns
description: Modern Ruby idioms, design patterns, metaprogramming techniques, and best practices. Use when writing Ruby code or refactoring for clarity.
---
# Ruby Patterns Skill
## Tier 1: Quick Reference - Common Idioms
### Conditional Assignment
```ruby
# Set if nil
value ||= default_value
# Set if falsy (nil or false)
value = value || default_value
# Safe navigation
user&.profile&.avatar&.url
```
### Array and Hash Shortcuts
```ruby
# Array creation
%w[apple banana orange] # ["apple", "banana", "orange"]
%i[name email age] # [:name, :email, :age]
# Hash creation
{ name: 'John', age: 30 } # Symbol keys
{ 'name' => 'John' } # String keys
# Hash access with default
hash.fetch(:key, default)
hash[:key] || default
```
### Enumerable Shortcuts
```ruby
# Transformation
array.map(&:upcase)
array.select(&:active?)
array.reject(&:empty?)
# Aggregation
array.sum
array.max
array.min
numbers.reduce(:+)
# Finding
array.find(&:valid?)
array.any?(&:present?)
array.all?(&:valid?)
```
### String Operations
```ruby
# Interpolation
"Hello #{name}!"
# Safe interpolation
"Result: %{value}" % { value: result }
# Multiline
<<~TEXT
Heredoc with indentation
removed automatically
TEXT
```
### Block Syntax
```ruby
# Single line - use braces
array.map { |x| x * 2 }
# Multi-line - use do/end
array.each do |item|
process(item)
log(item)
end
# Symbol to_proc
array.map(&:to_s)
array.select(&:even?)
```
### Guard Clauses
```ruby
def process(user)
return unless user
return unless user.active?
# Main logic here
end
```
### Case Statements
```ruby
# Traditional
case status
when 'active'
activate
when 'inactive'
deactivate
end
# With ranges
case age
when 0..17
'minor'
when 18..64
'adult'
else
'senior'
end
```
---
## Tier 2: Detailed Instructions - Design Patterns
### Creational Patterns
**Factory Pattern:**
```ruby
class UserFactory
def self.create(type, attributes)
case type
when :admin
AdminUser.new(attributes)
when :member
MemberUser.new(attributes)
when :guest
GuestUser.new(attributes)
else
raise ArgumentError, "Unknown user type: #{type}"
end
end
end
# Usage
user = UserFactory.create(:admin, name: 'John', email: 'john@example.com')
```
**Builder Pattern:**
```ruby
class QueryBuilder
def initialize
@conditions = []
@order = nil
@limit = nil
end
def where(condition)
@conditions << condition
self
end
def order(column)
@order = column
self
end
def limit(count)
@limit = count
self
end
def build
query = "SELECT * FROM users"
query += " WHERE #{@conditions.join(' AND ')}" if @conditions.any?
query += " ORDER BY #{@order}" if @order
query += " LIMIT #{@limit}" if @limit
query
end
end
# Usage
query = QueryBuilder.new
.where("active = true")
.where("age > 18")
.order("created_at DESC")
.limit(10)
.build
```
**Singleton Pattern:**
```ruby
require 'singleton'
class Configuration
include Singleton
attr_accessor :api_key, :timeout
def initialize
@api_key = ENV['API_KEY']
@timeout = 30
end
end
# Usage
config = Configuration.instance
config.api_key = 'new_key'
```
### Structural Patterns
**Decorator Pattern:**
```ruby
# Simple decorator
class User
attr_accessor :name, :email
def initialize(name, email)
@name = name
@email = email
end
end
class AdminUser < SimpleDelegator
def permissions
[:read, :write, :delete, :admin]
end
def admin?
true
end
end
# Usage
user = User.new('John', 'john@example.com')
admin = AdminUser.new(user)
admin.name # Delegates to user
admin.admin? # From decorator
# Using Ruby's Forwardable
require 'forwardable'
class UserDecorator
extend Forwardable
def_delegators :@user, :name, :email
def initialize(user)
@user = user
end
def display_name
"#{@user.name} (#{@user.email})"
end
end
```
**Adapter Pattern:**
```ruby
# Adapting third-party API
class LegacyPaymentGateway
def make_payment(amount, card)
# Legacy implementation
end
end
class PaymentAdapter
def initialize(gateway)
@gateway = gateway
end
def process(amount:, card_number:)
card = { number: card_number }
@gateway.make_payment(amount, card)
end
end
# Usage
legacy = LegacyPaymentGateway.new
adapter = PaymentAdapter.new(legacy)
adapter.process(amount: 100, card_number: '1234')
```
**Composite Pattern:**
```ruby
class File
attr_reader :name, :size
def initialize(name, size)
@name = name
@size = size
end
def total_size
size
end
end
class Directory
attr_reader :name
def initialize(name)
@name = name
@contents = []
end
def add(item)
@contents << item
end
def total_size
@contents.sum(&:total_size)
end
end
# Usage
root = Directory.new('root')
root.add(File.new('file1.txt', 100))
subdir = Directory.new('subdir')
subdir.add(File.new('file2.txt', 200))
root.add(subdir)
root.total_size # 300
```
### Behavioral Patterns
**Strategy Pattern:**
```ruby
class PaymentProcessor
def initialize(strategy)
@strategy = strategy
end
def process(amount)
@strategy.process(amount)
end
end
class CreditCardStrategy
def process(amount)
puts "Processing #{amount} via credit card"
end
end
class PayPalStrategy
def process(amount)
puts "Processing #{amount} via PayPal"
end
end
# Usage
processor = PaymentProcessor.new(CreditCardStrategy.new)
processor.process(100)
processor = PaymentProcessor.new(PayPalStrategy.new)
processor.process(100)
```
**Observer Pattern:**
```ruby
require 'observer'
class Order
include Observable
attr_reader :status
def initialize
@status = :pending
end
def complete!
@status = :completed
changed
notify_observers(self)
end
end
class EmailNotifier
def update(order)
puts "Sending email: Order #{order.object_id} is #{order.status}"
end
end
class SMSNotifier
def update(order)
puts "Sending SMS: Order #{order.object_id} is #{order.status}"
end
end
# Usage
order = Order.new
order.add_observer(EmailNotifier.new)
order.add_observer(SMSNotifier.new)
order.complete! # Both notifiers triggered
```
**Command Pattern:**
```ruby
class Command
def execute
raise NotImplementedError
end
def undo
raise NotImplementedError
end
end
class CreateUserCommand < Command
def initialize(user_service, params)
@user_service = user_service
@params = params
@user = nil
end
def execute
@user = @user_service.create(@params)
end
def undo
@user_service.delete(@user.id) if @user
end
end
class CommandInvoker
def initialize
@history = []
end
def execute(command)
command.execute
@history << command
end
def undo
command = @history.pop
command&.undo
end
end
# Usage
invoker = CommandInvoker.new
command = CreateUserCommand.new(user_service, { name: 'John' })
invoker.execute(command)
invoker.undo # Rolls back
```
### Metaprogramming Techniques
**Dynamic Method Definition:**
```ruby
class Model
ATTRIBUTES = [:name, :email, :age]
ATTRIBUTES.each do |attr|
define_method(attr) do
instance_variable_get("@#{attr}")
end
define_method("#{attr}=") do |value|
instance_variable_set("@#{attr}", value)
end
end
end
# Usage
model = Model.new
model.name = 'John'
model.name # 'John'
```
**Method Missing:**
```ruby
class DynamicFinder
def initialize(data)
@data = data
end
def method_missing(method_name, *args)
if method_name.to_s.start_with?('find_by_')
attribute = method_name.to_s.sub('find_by_', '')
@data.find { |item| item[attribute.to_sym] == args.first }
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?('find_by_') || super
end
end
# Usage
data = [
{ name: 'John', email: 'john@example.com' },
{ name: 'Jane', email: 'jane@example.com' }
]
finder = DynamicFinder.new(data)
finder.find_by_name('John') # { name: 'John', ... }
finder.find_by_email('jane@example.com') # { name: 'Jane', ... }
```
**Class Macros (DSL):**
```ruby
class Validator
def self.validates(attribute, rules)
@validations ||= []
@validations << [attribute, rules]
define_method(:valid?) do
self.class.instance_variable_get(:@validations).all? do |attr, rules|
value = send(attr)
validate_rules(value, rules)
end
end
end
def validate_rules(value, rules)
rules.all? do |rule, param|
case rule
when :presence
!value.nil? && !value.empty?
when :length
value.length <= param
when :format
value.match?(param)
else
true
end
end
end
end
class User < Validator
attr_accessor :name, :email
validates :name, presence: true, length: 50
validates :email, presence: true, format: /@/
def initialize(name, email)
@name = name
@email = email
end
end
# Usage
user = User.new('John', 'john@example.com')
user.valid? # true
```
**Module Inclusion Hooks:**
```ruby
module Timestampable
def self.included(base)
base.class_eval do
attr_accessor :created_at, :updated_at
define_method(:touch) do
self.updated_at = Time.now
end
end
end
end
# Using ActiveSupport::Concern for cleaner syntax
module Trackable
extend ActiveSupport::Concern
included do
attr_accessor :tracked_at
end
class_methods do
def tracking_enabled?
true
end
end
def track!
self.tracked_at = Time.now
end
end
class Model
include Timestampable
include Trackable
end
# Usage
model = Model.new
model.touch
model.track!
```
---
## Tier 3: Resources & Examples
### Performance Patterns
**Memoization:**
```ruby
# Basic memoization
def expensive_calculation
@expensive_calculation ||= begin
# Expensive operation
sleep 1
'result'
end
end
# Memoization with parameters
def user_posts(user_id)
@user_posts ||= {}
@user_posts[user_id] ||= Post.where(user_id: user_id).to_a
end
# Thread-safe memoization
require 'concurrent'
class Service
def initialize
@cache = Concurrent::Map.new
end
def get(key)
@cache.compute_if_absent(key) do
expensive_operation(key)
end
end
end
```
**Lazy Evaluation:**
```ruby
# Lazy enumeration for large datasets
(1..Float::INFINITY)
.lazy
.select { |n| n % 3 == 0 }
.first(10)
# Lazy file processing
File.foreach('large_file.txt').lazy
.select { |line| line.include?('ERROR') }
.map(&:strip)
.first(100)
# Custom lazy enumerator
def lazy_range(start, finish)
Enumerator.new do |yielder|
current = start
while current <= finish
yielder << current
current += 1
end
end.lazy
end
```
**Struct for Value Objects:**
```ruby
# Simple value object
User = Struct.new(:name, :email, :age) do
def adult?
age >= 18
end
def to_s
"#{name} <#{email}>"
end
end
# Keyword arguments (Ruby 2.5+)
User = Struct.new(:name, :email, :age, keyword_init: true)
user = User.new(name: 'John', email: 'john@example.com', age: 30)
# Data class (Ruby 3.2+)
User = Data.define(:name, :email, :age) do
def adult?
age >= 18
end
end
```
### Error Handling Patterns
**Custom Exceptions:**
```ruby
class ApplicationError < StandardError; end
class ValidationError < ApplicationError; end
class NotFoundError < ApplicationError; end
class AuthenticationError < ApplicationError; end
class UserService
def create(params)
raise ValidationError, 'Name is required' if params[:name].nil?
User.create(params)
rescue ActiveRecord::RecordNotFound => e
raise NotFoundError, e.message
end
end
# Usage with rescue
begin
user_service.create(params)
rescue ValidationError => e
render json: { error: e.message }, status: 422
rescue NotFoundError => e
render json: { error: e.message }, status: 404
rescue ApplicationError => e
render json: { error: e.message }, status: 500
end
```
**Result Object Pattern:**
```ruby
class Result
attr_reader :value, :error
def initialize(success, value, error = nil)
@success = success
@value = value
@error = error
end
def success?
@success
end
def failure?
!@success
end
def self.success(value)
new(true, value)
end
def self.failure(error)
new(false, nil, error)
end
def on_success(&block)
block.call(value) if success?
self
end
def on_failure(&block)
block.call(error) if failure?
self
end
end
# Usage
def create_user(params)
user = User.new(params)
if user.valid?
user.save
Result.success(user)
else
Result.failure(user.errors)
end
end
result = create_user(params)
result
.on_success { |user| send_welcome_email(user) }
.on_failure { |errors| log_errors(errors) }
```
### Testing Patterns
**Shared Examples:**
```ruby
RSpec.shared_examples 'a timestamped model' do
it 'has created_at' do
expect(subject).to respond_to(:created_at)
end
it 'has updated_at' do
expect(subject).to respond_to(:updated_at)
end
it 'sets timestamps on create' do
subject.save
expect(subject.created_at).to be_present
expect(subject.updated_at).to be_present
end
end
RSpec.describe User do
it_behaves_like 'a timestamped model'
end
```
### Functional Programming Patterns
**Composition:**
```ruby
# Function composition
add_one = ->(x) { x + 1 }
double = ->(x) { x * 2 }
square = ->(x) { x ** 2 }
# Manual composition
result = square.call(double.call(add_one.call(5))) # ((5+1)*2)^2 = 144
# Compose helper
def compose(*fns)
->(x) { fns.reverse.reduce(x) { |acc, fn| fn.call(acc) } }
end
composed = compose(square, double, add_one)
composed.call(5) # 144
```
**Immutability:**
```ruby
# Frozen objects
class ImmutablePoint
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
freeze
end
def move(dx, dy)
ImmutablePoint.new(@x + dx, @y + dy)
end
end
# Frozen literals (Ruby 3+)
# frozen_string_literal: true
NAME = 'John' # Frozen by default
```
### Additional Resources
See `assets/` directory for:
- `idioms-cheatsheet.md` - Quick reference for Ruby idioms
- `design-patterns.rb` - Complete implementations of all patterns
- `metaprogramming-examples.rb` - Advanced metaprogramming techniques
See `references/` directory for:
- Style guides and best practices
- Performance optimization examples
- Testing pattern library

View File

@@ -0,0 +1,656 @@
---
name: sinatra-patterns
description: Common Sinatra patterns, routing strategies, error handling, and application organization. Use when building Sinatra applications or designing routes.
---
# Sinatra Patterns Skill
## Tier 1: Quick Reference
### Common Routing Patterns
**Basic Routes:**
```ruby
get '/' do
'Hello World'
end
post '/users' do
# Create user
end
put '/users/:id' do
# Update user
end
delete '/users/:id' do
# Delete user
end
```
**Route Parameters:**
```ruby
# Named parameters
get '/users/:id' do
User.find(params[:id])
end
# Parameter constraints
get '/users/:id', :id => /\d+/ do
# Only matches numeric IDs
end
# Wildcard
get '/files/*.*' do
# params['splat'] contains matched segments
end
```
**Query Parameters:**
```ruby
get '/search' do
query = params[:q]
page = params[:page] || 1
results = search(query, page: page)
end
```
### Basic Middleware
```ruby
# Session middleware
use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
# Security middleware
use Rack::Protection
# Logging
use Rack::CommonLogger
# Compression
use Rack::Deflater
```
### Simple Error Handling
```ruby
not_found do
'Page not found'
end
error do
'Internal server error'
end
error 401 do
'Unauthorized'
end
```
### Helpers
```ruby
helpers do
def logged_in?
!session[:user_id].nil?
end
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
end
```
---
## Tier 2: Detailed Instructions
### Advanced Routing
**Modular Applications:**
```ruby
# app/controllers/base_controller.rb
class BaseController < Sinatra::Base
configure do
set :views, Proc.new { File.join(root, '../views') }
set :public_folder, Proc.new { File.join(root, '../public') }
end
helpers do
def json_response(data, status = 200)
content_type :json
halt status, data.to_json
end
end
end
# app/controllers/users_controller.rb
class UsersController < BaseController
get '/' do
users = User.all
json_response(users.map(&:to_hash))
end
get '/:id' do
user = User.find(params[:id]) || halt(404)
json_response(user.to_hash)
end
post '/' do
user = User.create(params[:user])
if user.persisted?
json_response(user.to_hash, 201)
else
json_response({ errors: user.errors }, 422)
end
end
end
# config.ru
map '/users' do
run UsersController
end
```
**Namespaces:**
```ruby
require 'sinatra/namespace'
class App < Sinatra::Base
register Sinatra::Namespace
namespace '/api' do
namespace '/v1' do
get '/users' do
# GET /api/v1/users
end
namespace '/admin' do
before do
authenticate_admin!
end
get '/stats' do
# GET /api/v1/admin/stats
end
end
end
end
end
```
**Route Conditions:**
```ruby
# User agent condition
get '/', :agent => /iPhone/ do
# Mobile version
end
# Custom conditions
set(:auth) do |role|
condition do
unless current_user && current_user.has_role?(role)
halt 403
end
end
end
get '/admin', :auth => :admin do
# Only accessible to admins
end
# Host-based routing
get '/', :host => 'admin.example.com' do
# Admin subdomain
end
```
**Content Negotiation:**
```ruby
get '/users/:id', :provides => [:json, :xml, :html] do
user = User.find(params[:id])
case request.accept.first.to_s
when 'application/json'
json user.to_json
when 'application/xml'
xml user.to_xml
else
erb :user, locals: { user: user }
end
end
# Or using provides helper
get '/users/:id' do
user = User.find(params[:id])
respond_to do |format|
format.json { json user.to_json }
format.xml { xml user.to_xml }
format.html { erb :user, locals: { user: user } }
end
end
```
### Middleware Composition
**Custom Middleware:**
```ruby
class RequestLogger
def initialize(app)
@app = app
end
def call(env)
start_time = Time.now
status, headers, body = @app.call(env)
duration = Time.now - start_time
puts "#{env['REQUEST_METHOD']} #{env['PATH_INFO']} - #{status} (#{duration}s)"
[status, headers, body]
end
end
use RequestLogger
```
**Middleware Ordering:**
```ruby
# config.ru
use Rack::Deflater # Compression first
use Rack::Static # Static files
use Rack::CommonLogger # Logging
use Rack::Session::Cookie # Sessions
use Rack::Protection # Security
use CustomAuthentication # Auth
run Application
```
### Template Integration
**ERB Templates:**
```ruby
# views/layout.erb
<!DOCTYPE html>
<html>
<head>
<title><%= @title || 'My App' %></title>
</head>
<body>
<%= yield %>
</body>
</html>
# views/users/index.erb
<h1>Users</h1>
<ul>
<% @users.each do |user| %>
<li><%= user.name %></li>
<% end %>
</ul>
# Controller
get '/users' do
@users = User.all
@title = 'User List'
erb :'users/index'
end
```
**Inline Templates:**
```ruby
get '/' do
erb :index
end
__END__
@@layout
<!DOCTYPE html>
<html>
<body><%= yield %></body>
</html>
@@index
<h1>Welcome</h1>
```
**Template Engines:**
```ruby
# Haml
get '/' do
haml :index
end
# Slim
get '/' do
slim :index
end
# Liquid (safe for user content)
get '/' do
liquid :index, locals: { user: current_user }
end
```
### Error Handling Patterns
**Comprehensive Error Handling:**
```ruby
class Application < Sinatra::Base
# Development configuration
configure :development do
set :show_exceptions, :after_handler
set :dump_errors, true
end
# Production configuration
configure :production do
set :show_exceptions, false
set :dump_errors, false
end
# Specific exception handlers
error ActiveRecord::RecordNotFound do
status 404
json({ error: 'Resource not found' })
end
error ActiveRecord::RecordInvalid do
status 422
json({ error: 'Validation failed', details: env['sinatra.error'].message })
end
error Sequel::NoMatchingRow do
status 404
json({ error: 'Not found' })
end
# HTTP status handlers
not_found do
json({ error: 'Endpoint not found' })
end
error 401 do
json({ error: 'Unauthorized' })
end
error 403 do
json({ error: 'Forbidden' })
end
error 422 do
json({ error: 'Unprocessable entity' })
end
# Catch-all error handler
error do
error = env['sinatra.error']
logger.error("Error: #{error.message}")
logger.error(error.backtrace.join("\n"))
status 500
json({ error: 'Internal server error' })
end
end
```
### Before/After Filters
**Request Filters:**
```ruby
# Global before filter
before do
content_type :json
end
# Path-specific filters
before '/admin/*' do
authenticate_admin!
end
# Conditional filters
before do
pass unless request.path.start_with?('/api')
authenticate_api_user!
end
# After filters
after do
# Add CORS headers
headers 'Access-Control-Allow-Origin' => '*'
end
# Modify response
after do
response.body = response.body.map(&:upcase) if params[:uppercase]
end
```
### Session Management
**Cookie Sessions:**
```ruby
use Rack::Session::Cookie,
key: 'app.session',
secret: ENV['SESSION_SECRET'],
expire_after: 86400, # 1 day
secure: production?,
httponly: true,
same_site: :strict
helpers do
def login(user)
session[:user_id] = user.id
session[:logged_in_at] = Time.now.to_i
end
def logout
session.clear
end
def current_user
return nil unless session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
end
```
---
## Tier 3: Resources & Examples
### Full Application Example
See `assets/modular-app-template/` for complete modular application structure.
### Performance Patterns
**Caching:**
```ruby
# HTTP caching
get '/public/data' do
cache_control :public, max_age: 3600
etag calculate_etag
last_modified last_update_time
json PublicData.all.map(&:to_hash)
end
# Fragment caching with Redis
require 'redis'
helpers do
def cache_fetch(key, expires_in: 300, &block)
cached = REDIS.get(key)
return JSON.parse(cached) if cached
data = block.call
REDIS.setex(key, expires_in, data.to_json)
data
end
end
get '/expensive-data' do
data = cache_fetch('expensive-data', expires_in: 600) do
perform_expensive_query
end
json data
end
```
**Streaming Responses:**
```ruby
# Stream large responses
get '/large-export' do
stream do |out|
User.find_each do |user|
out << user.to_csv_row
end
end
end
# Server-Sent Events
get '/events', provides: 'text/event-stream' do
stream :keep_open do |out|
EventSource.subscribe do |event|
out << "data: #{event.to_json}\n\n"
end
end
end
```
### Production Configuration
**Complete config.ru:**
```ruby
# config.ru
require_relative 'config/environment'
# Production middleware
if ENV['RACK_ENV'] == 'production'
use Rack::SSL
use Rack::Deflater
end
# Static files
use Rack::Static,
urls: ['/css', '/js', '/images'],
root: 'public',
header_rules: [
[:all, {'Cache-Control' => 'public, max-age=31536000'}]
]
# Logging
use Rack::CommonLogger
# Sessions
use Rack::Session::Cookie,
secret: ENV['SESSION_SECRET'],
same_site: :strict,
httponly: true,
secure: ENV['RACK_ENV'] == 'production'
# Security
use Rack::Protection,
except: [:session_hijacking],
use: :all
# Rate limiting (production)
if ENV['RACK_ENV'] == 'production'
require 'rack/attack'
use Rack::Attack
end
# Mount applications
map '/api/v1' do
run ApiV1::Application
end
map '/' do
run WebApplication
end
```
**Rack::Attack Configuration:**
```ruby
# config/rack_attack.rb
class Rack::Attack
# Throttle login attempts
throttle('login/ip', limit: 5, period: 60) do |req|
req.ip if req.path == '/login' && req.post?
end
# Throttle API requests
throttle('api/ip', limit: 100, period: 60) do |req|
req.ip if req.path.start_with?('/api')
end
# Block suspicious requests
blocklist('block bad user agents') do |req|
req.user_agent =~ /bad_bot/i
end
end
```
### Testing Patterns
See `references/testing-examples.rb` for comprehensive test patterns.
### Project Structure
**Recommended modular structure:**
```
app/
controllers/
base_controller.rb
api_controller.rb
users_controller.rb
posts_controller.rb
models/
user.rb
post.rb
services/
user_service.rb
authentication_service.rb
helpers/
application_helpers.rb
view_helpers.rb
config/
environment.rb
database.yml
puma.rb
db/
migrations/
lib/
middleware/
custom_auth.rb
tasks/
public/
css/
js/
images/
views/
layout.erb
users/
index.erb
show.erb
spec/
controllers/
models/
spec_helper.rb
config.ru
Gemfile
Rakefile
README.md
```
### Additional Resources
- **Routing Examples:** `assets/routing-examples.rb`
- **Middleware Patterns:** `assets/middleware-patterns.rb`
- **Modular App Template:** `assets/modular-app-template/`
- **Production Config:** `references/production-config.rb`
- **Testing Guide:** `references/testing-examples.rb`

View File

@@ -0,0 +1,771 @@
---
name: sinatra-security
description: Security best practices for Sinatra applications including input validation, CSRF protection, and authentication patterns. Use when hardening applications or conducting security reviews.
---
# Sinatra Security Skill
## Tier 1: Quick Reference - Essential Security
### CSRF Protection
```ruby
# Enable Rack::Protection
use Rack::Protection
# Or specifically CSRF
use Rack::Protection::AuthenticityToken
```
### XSS Prevention
```ruby
# In ERB templates - always escape by default
<%= user.bio %> # Escaped (safe)
<%== user.bio %> # Raw (dangerous!)
# In JSON responses - use proper JSON encoding
require 'json'
json({ name: user.name }.to_json)
```
### SQL Injection Prevention
```ruby
# BAD: String interpolation
DB["SELECT * FROM users WHERE email = '#{email}'"]
# GOOD: Parameterized queries
DB["SELECT * FROM users WHERE email = ?", email]
# GOOD: Hash conditions
User.where(email: email)
```
### Secure Sessions
```ruby
use Rack::Session::Cookie,
secret: ENV['SESSION_SECRET'], # Long random string
same_site: :strict,
httponly: true,
secure: production?
```
### Input Validation
```ruby
helpers do
def validate_email(email)
email.to_s.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
def validate_integer(value)
Integer(value)
rescue ArgumentError, TypeError
nil
end
end
post '/users' do
halt 400, 'Invalid email' unless validate_email(params[:email])
# Process...
end
```
### Authentication Check
```ruby
helpers do
def authenticate!
halt 401, json({ error: 'Unauthorized' }) unless current_user
end
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
end
before '/admin/*' do
authenticate!
end
```
---
## Tier 2: Detailed Instructions - Security Implementation
### Comprehensive CSRF Protection
**Configuration:**
```ruby
class Application < Sinatra::Base
# Enable CSRF protection
use Rack::Protection::AuthenticityToken,
except: [:json], # Skip for JSON APIs with token auth
allow_if: -> (env) {
# Skip for API endpoints with bearer token
env['HTTP_AUTHORIZATION']&.start_with?('Bearer ')
}
# Manual CSRF token generation
helpers do
def csrf_token
session[:csrf] ||= SecureRandom.hex(32)
end
def csrf_tag
"<input type='hidden' name='authenticity_token' value='#{csrf_token}'>"
end
def verify_csrf_token
token = params[:authenticity_token] || request.env['HTTP_X_CSRF_TOKEN']
halt 403, 'Invalid CSRF token' unless token == session[:csrf]
end
end
# Include in forms
post '/users' do
verify_csrf_token unless request.content_type == 'application/json'
# Process...
end
end
```
**In Views:**
```erb
<form method="POST" action="/users">
<%= csrf_tag %>
<!-- form fields -->
</form>
```
**For AJAX:**
```javascript
// Include CSRF token in AJAX requests
fetch('/users', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('[name=csrf_token]').value,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
```
### XSS Prevention Strategies
**Template Escaping:**
```ruby
# ERB - escape by default
<div><%= user_input %></div>
# Explicitly raw (only for trusted content)
<div><%== trusted_html %></div>
# Sanitize user HTML
require 'sanitize'
helpers do
def sanitize_html(html)
Sanitize.fragment(html, Sanitize::Config::RELAXED)
end
end
# In template
<div><%= sanitize_html(user_bio) %></div>
```
**JSON Responses:**
```ruby
# Always use proper JSON encoding
get '/api/users/:id' do
user = User.find(params[:id])
# BAD: Manual JSON construction
# "{ \"name\": \"#{user.name}\" }" # XSS if name contains quotes
# GOOD: Use JSON library
content_type :json
{ name: user.name, bio: user.bio }.to_json
end
```
**Content Security Policy:**
```ruby
class Application < Sinatra::Base
before do
headers 'Content-Security-Policy' => [
"default-src 'self'",
"script-src 'self' https://cdn.example.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'"
].join('; ')
end
end
```
### SQL Injection Prevention
**Parameterized Queries:**
```ruby
# Sequel
# BAD
DB["SELECT * FROM users WHERE name = '#{name}'"]
# GOOD
DB["SELECT * FROM users WHERE name = ?", name]
DB["SELECT * FROM users WHERE name = :name", name: name]
# ActiveRecord
# BAD
User.where("email = '#{email}'")
# GOOD
User.where(email: email)
User.where("email = ?", email)
User.where("email = :email", email: email)
```
**Input Validation:**
```ruby
helpers do
def validate_sql_param(param, type: :string)
case type
when :integer
Integer(param)
when :boolean
[true, 'true', '1', 1].include?(param)
when :string
param.to_s.gsub(/['";\\]/, '') # Remove dangerous chars
else
param
end
rescue ArgumentError
halt 400, 'Invalid parameter'
end
end
get '/users/:id' do
id = validate_sql_param(params[:id], type: :integer)
user = User.find(id)
json user.to_hash
end
```
### Authentication Patterns
**Password Authentication:**
```ruby
require 'bcrypt'
class User
include BCrypt
def password=(new_password)
@password_hash = Password.create(new_password)
end
def password_hash
@password_hash
end
def authenticate(password)
Password.new(password_hash) == password
end
end
# Registration
post '/register' do
user = User.new(
email: params[:email],
name: params[:name]
)
user.password = params[:password]
user.save
session[:user_id] = user.id
redirect '/dashboard'
end
# Login
post '/login' do
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
session[:logged_in_at] = Time.now.to_i
redirect '/dashboard'
else
halt 401, 'Invalid credentials'
end
end
```
**Token-Based Authentication:**
```ruby
require 'jwt'
class TokenAuth
SECRET = ENV['JWT_SECRET']
def self.encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, SECRET, 'HS256')
end
def self.decode(token)
body = JWT.decode(token, SECRET, true, algorithm: 'HS256')[0]
HashWithIndifferentAccess.new(body)
rescue JWT::DecodeError, JWT::ExpiredSignature
nil
end
end
# Middleware
class JWTAuth
def initialize(app)
@app = app
end
def call(env)
auth_header = env['HTTP_AUTHORIZATION']
token = auth_header&.split(' ')&.last
if payload = TokenAuth.decode(token)
env['current_user_id'] = payload[:user_id]
@app.call(env)
else
[401, { 'Content-Type' => 'application/json' },
['{"error": "Unauthorized"}']]
end
end
end
# Login endpoint
post '/api/login' do
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
token = TokenAuth.encode(user_id: user.id)
json({ token: token, user: user.to_hash })
else
halt 401, json({ error: 'Invalid credentials' })
end
end
# Protected routes
class API < Sinatra::Base
use JWTAuth
helpers do
def current_user
@current_user ||= User.find(request.env['current_user_id'])
end
end
get '/profile' do
json current_user.to_hash
end
end
```
**API Key Authentication:**
```ruby
class APIKeyAuth
def initialize(app)
@app = app
end
def call(env)
api_key = env['HTTP_X_API_KEY']
if valid_api_key?(api_key)
user = User.find_by(api_key: api_key)
env['current_user'] = user
@app.call(env)
else
[401, { 'Content-Type' => 'application/json' },
['{"error": "Invalid API key"}']]
end
end
private
def valid_api_key?(key)
key && User.exists?(api_key: key, active: true)
end
end
use APIKeyAuth
# Generate API keys
helpers do
def generate_api_key
SecureRandom.hex(32)
end
end
post '/api/keys' do
authenticate!
api_key = generate_api_key
current_user.update(api_key: api_key)
json({ api_key: api_key })
end
```
### Authorization Patterns
**Role-Based Access Control:**
```ruby
class User
ROLES = [:guest, :user, :admin, :superadmin]
def has_role?(role)
ROLES.index(self.role) >= ROLES.index(role)
end
def can?(action, resource)
case role
when :admin, :superadmin
true
when :user
action == :read || resource.user_id == id
else
action == :read
end
end
end
helpers do
def authorize!(action, resource)
unless current_user&.can?(action, resource)
halt 403, json({ error: 'Forbidden' })
end
end
end
# Usage
get '/posts/:id' do
post = Post.find(params[:id])
authorize!(:read, post)
json post.to_hash
end
delete '/posts/:id' do
post = Post.find(params[:id])
authorize!(:delete, post)
post.destroy
status 204
end
```
**Permission-Based Authorization:**
```ruby
class Permission
ACTIONS = {
posts: [:create, :read, :update, :delete],
users: [:read, :update, :delete],
comments: [:create, :read, :delete]
}
def self.check(user, action, resource_type)
return false unless user
permissions = user.permissions
permissions.include?("#{resource_type}:#{action}") ||
permissions.include?("#{resource_type}:*") ||
permissions.include?("*:*")
end
end
helpers do
def can?(action, resource_type)
Permission.check(current_user, action, resource_type)
end
def authorize!(action, resource_type)
unless can?(action, resource_type)
halt 403, json({ error: 'Forbidden' })
end
end
end
post '/posts' do
authorize!(:create, :posts)
# Create post
end
```
### Rate Limiting
**Using Rack::Attack:**
```ruby
require 'rack/attack'
class Rack::Attack
# Throttle login attempts
throttle('login/ip', limit: 5, period: 60) do |req|
req.ip if req.path == '/login' && req.post?
end
# Throttle API requests by API key
throttle('api/key', limit: 100, period: 60) do |req|
req.env['HTTP_X_API_KEY'] if req.path.start_with?('/api')
end
# Throttle by IP
throttle('req/ip', limit: 300, period: 60) do |req|
req.ip
end
# Block known bad actors
blocklist('block bad IPs') do |req|
BadIP.blocked?(req.ip)
end
# Custom response
self.throttled_responder = lambda do |env|
[
429,
{ 'Content-Type' => 'application/json' },
[{ error: 'Rate limit exceeded' }.to_json]
]
end
end
use Rack::Attack
```
### Secure File Uploads
```ruby
require 'securerandom'
class FileUploadHandler
ALLOWED_TYPES = {
'image/jpeg' => '.jpg',
'image/png' => '.png',
'image/gif' => '.gif',
'application/pdf' => '.pdf'
}
MAX_SIZE = 5 * 1024 * 1024 # 5MB
def self.process(file)
# Validate file presence
return { error: 'No file provided' } unless file
# Validate file size
if file[:tempfile].size > MAX_SIZE
return { error: 'File too large' }
end
# Validate content type
content_type = file[:type]
unless ALLOWED_TYPES.key?(content_type)
return { error: 'Invalid file type' }
end
# Sanitize filename
original_name = File.basename(file[:filename])
sanitized_name = original_name.gsub(/[^a-zA-Z0-9\._-]/, '')
# Generate unique filename
extension = ALLOWED_TYPES[content_type]
unique_name = "#{SecureRandom.hex(16)}#{extension}"
# Save file
upload_dir = 'uploads'
FileUtils.mkdir_p(upload_dir)
path = File.join(upload_dir, unique_name)
File.open(path, 'wb') do |f|
f.write(file[:tempfile].read)
end
{ success: true, path: path, filename: unique_name }
end
end
post '/upload' do
result = FileUploadHandler.process(params[:file])
if result[:error]
halt 400, json({ error: result[:error] })
else
json({ url: "/uploads/#{result[:filename]}" })
end
end
```
---
## Tier 3: Resources & Examples
### Security Headers
**Comprehensive Security Headers:**
```ruby
class SecurityHeaders
HEADERS = {
'X-Frame-Options' => 'DENY',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1; mode=block',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()',
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains'
}
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
headers.merge!(HEADERS)
[status, headers, body]
end
end
use SecurityHeaders
```
### OWASP Security Checklist
See `assets/owasp-checklist.md` for complete checklist covering:
1. **Injection Prevention**
- SQL Injection
- Command Injection
- LDAP Injection
- XML Injection
2. **Broken Authentication**
- Password policies
- Session management
- Multi-factor authentication
- Account lockout
3. **Sensitive Data Exposure**
- Encryption at rest
- Encryption in transit (HTTPS)
- Secure key storage
- Data minimization
4. **XML External Entities (XXE)**
- XML parser configuration
- Disable external entity processing
5. **Broken Access Control**
- Authentication on all protected routes
- Authorization checks
- IDOR prevention
- CORS configuration
6. **Security Misconfiguration**
- Remove default credentials
- Disable directory listing
- Error message handling
- Keep dependencies updated
7. **Cross-Site Scripting (XSS)**
- Output encoding
- Input validation
- Content Security Policy
- HTTPOnly cookies
8. **Insecure Deserialization**
- Validate serialized data
- Use safe serialization formats
- Sign serialized data
9. **Using Components with Known Vulnerabilities**
- Regular dependency updates
- Security audits (bundle audit)
- Monitor CVE databases
10. **Insufficient Logging & Monitoring**
- Log security events
- Monitor for attacks
- Alerting systems
- Log rotation and retention
### Security Testing Examples
**Testing Authentication:**
```ruby
RSpec.describe 'Authentication' do
describe 'POST /login' do
let(:user) { create(:user, email: 'test@example.com', password: 'password123') }
it 'succeeds with valid credentials' do
post '/login', { email: 'test@example.com', password: 'password123' }.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response).to be_ok
expect(json_response).to have_key('token')
end
it 'fails with invalid password' do
post '/login', { email: 'test@example.com', password: 'wrong' }.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(401)
end
it 'prevents brute force attacks' do
6.times do
post '/login', { email: 'test@example.com', password: 'wrong' }.to_json,
'CONTENT_TYPE' => 'application/json'
end
expect(last_response.status).to eq(429) # Rate limited
end
end
end
```
**Testing Authorization:**
```ruby
RSpec.describe 'Authorization' do
let(:user) { create(:user) }
let(:admin) { create(:user, role: :admin) }
let(:post) { create(:post, user: user) }
describe 'DELETE /posts/:id' do
it 'allows owner to delete' do
delete "/posts/#{post.id}", {}, auth_header(user.token)
expect(last_response.status).to eq(204)
end
it 'allows admin to delete' do
delete "/posts/#{post.id}", {}, auth_header(admin.token)
expect(last_response.status).to eq(204)
end
it 'denies other users' do
other_user = create(:user)
delete "/posts/#{post.id}", {}, auth_header(other_user.token)
expect(last_response.status).to eq(403)
end
it 'requires authentication' do
delete "/posts/#{post.id}"
expect(last_response.status).to eq(401)
end
end
end
```
### Additional Resources
- **Security Middleware:** `assets/security-middleware.rb`
- **Authentication Patterns:** `assets/auth-patterns.rb`
- **OWASP Checklist:** `assets/owasp-checklist.md`
- **Security Audit Template:** `references/security-audit-template.md`
- **Penetration Testing Guide:** `references/penetration-testing.md`