StandardRB fixes
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
Dan Milne
2026-01-01 13:29:44 +11:00
parent 7d3af2bcec
commit 93a0edb0a2
79 changed files with 779 additions and 786 deletions

View File

@@ -24,16 +24,16 @@ module Clinch
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
# Configure SMTP settings using environment variables
# Configure SMTP settings using environment variables
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV.fetch('SMTP_ADDRESS', 'localhost'),
port: ENV.fetch('SMTP_PORT', 587),
domain: ENV.fetch('SMTP_DOMAIN', 'localhost'),
user_name: ENV.fetch('SMTP_USERNAME', nil),
password: ENV.fetch('SMTP_PASSWORD', nil),
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain').to_sym,
enable_starttls_auto: ENV.fetch('SMTP_STARTTLS_AUTO', 'true') == 'true',
address: ENV.fetch("SMTP_ADDRESS", "localhost"),
port: ENV.fetch("SMTP_PORT", 587),
domain: ENV.fetch("SMTP_DOMAIN", "localhost"),
user_name: ENV.fetch("SMTP_USERNAME", nil),
password: ENV.fetch("SMTP_PASSWORD", nil),
authentication: ENV.fetch("SMTP_AUTHENTICATION", "plain").to_sym,
enable_starttls_auto: ENV.fetch("SMTP_STARTTLS_AUTO", "true") == "true",
openssl_verify_mode: OpenSSL::SSL::VERIFY_PEER
}
end

View File

@@ -20,7 +20,7 @@ Rails.application.configure do
if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
config.public_file_server.headers = {"cache-control" => "public, max-age=#{2.days.to_i}"}
else
config.action_controller.perform_caching = false
end
@@ -39,10 +39,10 @@ Rails.application.configure do
config.action_mailer.perform_caching = false
# Set localhost to be used by links generated in mailer templates.
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
config.action_mailer.default_url_options = {host: "localhost", port: 3000}
# Log with request_id as a tag (same as production).
config.log_tags = [ :request_id ]
config.log_tags = [:request_id]
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
@@ -61,7 +61,6 @@ Rails.application.configure do
# Use async processor for background jobs in development
config.active_job.queue_adapter = :async
# Highlight code that triggered redirect in logs.
config.action_dispatch.verbose_redirect_logs = true

View File

@@ -16,7 +16,7 @@ Rails.application.configure do
config.action_controller.perform_caching = true
# Cache assets for far-future expiry since they are all digest stamped.
config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }
config.public_file_server.headers = {"cache-control" => "public, max-age=#{1.year.to_i}"}
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com"
@@ -34,16 +34,16 @@ Rails.application.configure do
# Note: Rails already sets X-Content-Type-Options: nosniff by default
# Note: Permissions-Policy is configured in config/initializers/permissions_policy.rb
config.action_dispatch.default_headers.merge!(
'X-Frame-Options' => 'DENY', # Override default SAMEORIGIN to prevent clickjacking
'Referrer-Policy' => 'strict-origin-when-cross-origin' # Control referrer information
"X-Frame-Options" => "DENY", # Override default SAMEORIGIN to prevent clickjacking
"Referrer-Policy" => "strict-origin-when-cross-origin" # Control referrer information
)
# Skip http-to-https redirect for the default health check endpoint.
# config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
# Log to STDOUT with the current request id as a default log tag.
config.log_tags = [ :request_id ]
config.logger = ActiveSupport::TaggedLogging.logger(STDOUT)
config.log_tags = [:request_id]
config.logger = ActiveSupport::TaggedLogging.logger($stdout)
# Change to "debug" to log everything (including potentially personally-identifiable information!).
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
@@ -66,7 +66,7 @@ Rails.application.configure do
# Set host to be used by links generated in mailer templates.
config.action_mailer.default_url_options = {
host: ENV.fetch('CLINCH_HOST', 'example.com')
host: ENV.fetch("CLINCH_HOST", "example.com")
}
# Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
@@ -86,13 +86,13 @@ Rails.application.configure do
config.active_record.dump_schema_after_migration = false
# Only use :id for inspections in production.
config.active_record.attributes_for_inspect = [ :id ]
config.active_record.attributes_for_inspect = [:id]
# Helper method to extract domain from CLINCH_HOST (removes protocol if present)
def self.extract_domain(host)
return host if host.blank?
# Remove protocol (http:// or https://) if present
host.gsub(/^https?:\/\//, '')
host.gsub(/^https?:\/\//, "")
end
# Helper method to ensure URL has https:// protocol
@@ -105,11 +105,11 @@ Rails.application.configure do
# Enable DNS rebinding protection and other `Host` header attacks.
# Configure allowed hosts based on deployment scenario
allowed_hosts = [
extract_domain(ENV.fetch('CLINCH_HOST', 'auth.example.com')), # External domain (auth service itself)
extract_domain(ENV.fetch("CLINCH_HOST", "auth.example.com")) # External domain (auth service itself)
]
# Use PublicSuffix to extract registrable domain and allow all subdomains
host_domain = extract_domain(ENV.fetch('CLINCH_HOST', 'auth.example.com'))
host_domain = extract_domain(ENV.fetch("CLINCH_HOST", "auth.example.com"))
if host_domain.present?
begin
# Use PublicSuffix to properly extract the domain
@@ -123,20 +123,20 @@ Rails.application.configure do
rescue PublicSuffix::DomainInvalid
# Fallback to simple domain extraction if PublicSuffix fails
Rails.logger.warn "Could not parse domain '#{host_domain}' with PublicSuffix, using fallback"
base_domain = host_domain.split('.').last(2).join('.')
base_domain = host_domain.split(".").last(2).join(".")
allowed_hosts << /.*#{Regexp.escape(base_domain)}/
end
end
# Allow Docker service names if running in same compose
if ENV['CLINCH_DOCKER_SERVICE_NAME']
allowed_hosts << ENV['CLINCH_DOCKER_SERVICE_NAME']
if ENV["CLINCH_DOCKER_SERVICE_NAME"]
allowed_hosts << ENV["CLINCH_DOCKER_SERVICE_NAME"]
end
# Allow internal IP access for cross-compose or host networking
if ENV['CLINCH_ALLOW_INTERNAL_IPS'] == 'true'
if ENV["CLINCH_ALLOW_INTERNAL_IPS"] == "true"
# Specific host IP
allowed_hosts << '192.168.2.246'
allowed_hosts << "192.168.2.246"
# Private IP ranges for internal network access
allowed_hosts += [
@@ -147,14 +147,14 @@ Rails.application.configure do
end
# Local development fallbacks
if ENV['CLINCH_ALLOW_LOCALHOST'] == 'true'
allowed_hosts += ['localhost', '127.0.0.1', '0.0.0.0']
if ENV["CLINCH_ALLOW_LOCALHOST"] == "true"
allowed_hosts += ["localhost", "127.0.0.1", "0.0.0.0"]
end
config.hosts = allowed_hosts
# Skip DNS rebinding protection for the default health check endpoint.
config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
config.host_authorization = {exclude: ->(request) { request.path == "/up" }}
# Sentry configuration for production
# Only enabled if SENTRY_DSN environment variable is set

View File

@@ -16,7 +16,7 @@ Rails.application.configure do
config.eager_load = ENV["CI"].present?
# Configure public file server for tests with cache-control for performance.
config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }
config.public_file_server.headers = {"cache-control" => "public, max-age=3600"}
# Show full error reports.
config.consider_all_requests_local = true
@@ -37,7 +37,7 @@ Rails.application.configure do
config.action_mailer.delivery_method = :test
# Set host to be used by links generated in mailer templates.
config.action_mailer.default_url_options = { host: "example.com" }
config.action_mailer.default_url_options = {host: "example.com"}
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr

View File

@@ -8,14 +8,14 @@
# - ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
# Use env vars if set, otherwise derive from SECRET_KEY_BASE (deterministic)
primary_key = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY') do
Rails.application.key_generator.generate_key('active_record_encryption_primary', 32)
primary_key = ENV.fetch("ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY") do
Rails.application.key_generator.generate_key("active_record_encryption_primary", 32)
end
deterministic_key = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY') do
Rails.application.key_generator.generate_key('active_record_encryption_deterministic', 32)
deterministic_key = ENV.fetch("ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY") do
Rails.application.key_generator.generate_key("active_record_encryption_deterministic", 32)
end
key_derivation_salt = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT') do
Rails.application.key_generator.generate_key('active_record_encryption_salt', 32)
key_derivation_salt = ENV.fetch("ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT") do
Rails.application.key_generator.generate_key("active_record_encryption_salt", 32)
end
# Configure Rails 7.1+ ActiveRecord encryption

View File

@@ -56,14 +56,13 @@ Rails.application.configure do
policy.require_trusted_types_for :none
# CSP reporting using report_uri (supported method)
policy.report_uri "/api/csp-violation-report"
policy.report_uri "/api/csp-violation-report"
end
# Start with CSP in report-only mode for testing
# Set to false after verifying everything works in production
config.content_security_policy_report_only = Rails.env.development?
# Report CSP violations (optional - uncomment to enable)
# config.content_security_policy_report_uri = "/csp-violations"
end
end

View File

@@ -8,7 +8,7 @@ Rails.application.config.after_initialize do
# Configure log rotation
csp_logger = Logger.new(
csp_log_path,
'daily', # Rotate daily
"daily", # Rotate daily
30 # Keep 30 old log files
)
@@ -16,7 +16,7 @@ Rails.application.config.after_initialize do
# Format: [TIMESTAMP] LEVEL MESSAGE
csp_logger.formatter = proc do |severity, datetime, progname, msg|
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} #{msg}\n"
"[#{datetime.strftime("%Y-%m-%d %H:%M:%S")}] #{severity} #{msg}\n"
end
module CspViolationLocalLogger
@@ -25,9 +25,9 @@ Rails.application.config.after_initialize do
# Skip logging if there's no meaningful violation data
return if csp_data.empty? ||
(csp_data[:violated_directive].nil? &&
csp_data[:blocked_uri].nil? &&
csp_data[:document_uri].nil?)
(csp_data[:violated_directive].nil? &&
csp_data[:blocked_uri].nil? &&
csp_data[:document_uri].nil?)
# Build a structured log message
violated_directive = csp_data[:violated_directive] || "unknown"
@@ -69,7 +69,6 @@ Rails.application.config.after_initialize do
# Also log to main Rails logger for visibility
Rails.logger.info "CSP violation logged to csp_violations.log: #{violated_directive} - #{blocked_uri}"
rescue => e
# Ensure logger errors don't break the CSP reporting flow
Rails.logger.error "Failed to log CSP violation to file: #{e.message}"
@@ -81,12 +80,12 @@ Rails.application.config.after_initialize do
csp_log_path = Rails.root.join("log", "csp_violations.log")
logger = Logger.new(
csp_log_path,
'daily', # Rotate daily
"daily", # Rotate daily
30 # Keep 30 old log files
)
logger.level = Logger::INFO
logger.formatter = proc do |severity, datetime, progname, msg|
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} #{msg}\n"
"[#{datetime.strftime("%Y-%m-%d %H:%M:%S")}] #{severity} #{msg}\n"
end
logger
end
@@ -120,9 +119,8 @@ Rails.application.config.after_initialize do
# Test write to ensure permissions are correct
csp_logger.info "CSP Logger initialized at #{Time.current}"
rescue => e
Rails.logger.error "Failed to initialize CSP local logger: #{e.message}"
Rails.logger.error "CSP violations will only be sent to Sentry (if configured)"
end
end
end

View File

@@ -3,12 +3,12 @@
Rails.application.config.permissions_policy do |f|
# Disable sensitive browser features for security
f.camera :none
f.gyroscope :none
f.microphone :none
f.payment :none
f.usb :none
f.magnetometer :none
f.camera :none
f.gyroscope :none
f.microphone :none
f.payment :none
f.usb :none
f.magnetometer :none
# You can enable specific features as needed:
# f.fullscreen :self

View File

@@ -74,7 +74,7 @@ Rails.application.configure do
app_environment: Rails.env,
# Add CSP policy status
csp_enabled: defined?(Rails.application.config.content_security_policy) &&
Rails.application.config.content_security_policy.present?
Rails.application.config.content_security_policy.present?
}
end
@@ -120,13 +120,13 @@ Rails.application.configure do
if breadcrumb[:data]
breadcrumb[:data].reject! { |key, value|
key.to_s.match?(/password|secret|token|key|authorization/i) ||
value.to_s.match?(/password|secret/i)
value.to_s.match?(/password|secret/i)
}
end
# Mark CSP-related events
if breadcrumb[:message]&.include?("CSP Violation") ||
breadcrumb[:category]&.include?("csp")
breadcrumb[:category]&.include?("csp")
breadcrumb[:data] ||= {}
breadcrumb[:data][:security_event] = true
breadcrumb[:data][:csp_violation] = true
@@ -137,4 +137,4 @@ Rails.application.configure do
# Only send errors in production unless explicitly enabled
config.sentry.enabled = Rails.env.production? || ENV["SENTRY_ENABLED_IN_DEVELOPMENT"] == "true"
end
end

View File

@@ -47,7 +47,7 @@ Rails.application.config.after_initialize do
timestamp: csp_data[:timestamp]
}
},
user: csp_data[:current_user_id] ? { id: csp_data[:current_user_id] } : nil
user: csp_data[:current_user_id] ? {id: csp_data[:current_user_id]} : nil
)
# Log to Rails logger for redundancy
@@ -69,10 +69,10 @@ Rails.application.config.after_initialize do
parsed.host
rescue URI::InvalidURIError
# Handle cases where URI might be malformed or just a path
if uri.start_with?('/')
if uri.start_with?("/")
nil # It's a relative path, no domain
else
uri.split('/').first # Best effort extraction
uri.split("/").first # Best effort extraction
end
end
end
@@ -117,4 +117,4 @@ Rails.application.config.after_initialize do
else
Rails.logger.info "Sentry not initialized - CSP violations will only be logged locally"
end
end
end

View File

@@ -3,5 +3,5 @@
# Derived from SECRET_KEY_BASE - no storage needed, deterministic output
# Optional: Set OIDC_TOKEN_PREFIX_HMAC env var to override with explicit key
module TokenHmac
KEY = ENV['OIDC_TOKEN_PREFIX_HMAC'] || Rails.application.key_generator.generate_key('oidc_token_prefix', 32)
KEY = ENV["OIDC_TOKEN_PREFIX_HMAC"] || Rails.application.key_generator.generate_key("oidc_token_prefix", 32)
end

View File

@@ -68,4 +68,4 @@ end
# CLINCH_RP_NAME="Example Company Identity Provider"
# CLINCH_WEBAUTHN_ATTESTATION=none
# CLINCH_WEBAUTHN_USER_VERIFICATION=preferred
# CLINCH_WEBAUTHN_RESIDENT_KEY=preferred
# CLINCH_WEBAUTHN_RESIDENT_KEY=preferred

View File

@@ -31,7 +31,6 @@ threads threads_count, threads_count
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
port ENV.fetch("PORT", 3000)
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart

View File

@@ -8,7 +8,7 @@ Rails.application.routes.draw do
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
get "up" => "rails/health#show", :as => :rails_health_check
# Authentication routes
get "/signup", to: "users#new", as: :signup
@@ -61,21 +61,21 @@ Rails.application.routes.draw do
end
# TOTP (2FA) routes
get '/totp/new', to: 'totp#new', as: :new_totp
post '/totp', to: 'totp#create', as: :totp
delete '/totp', to: 'totp#destroy'
get '/totp/backup_codes', to: 'totp#backup_codes', as: :backup_codes_totp
post '/totp/verify_password', to: 'totp#verify_password', as: :verify_password_totp
get '/totp/regenerate_backup_codes', to: 'totp#regenerate_backup_codes', as: :regenerate_backup_codes_totp
post '/totp/regenerate_backup_codes', to: 'totp#create_new_backup_codes', as: :create_new_backup_codes_totp
post '/totp/complete_setup', to: 'totp#complete_setup', as: :complete_totp_setup
get "/totp/new", to: "totp#new", as: :new_totp
post "/totp", to: "totp#create", as: :totp
delete "/totp", to: "totp#destroy"
get "/totp/backup_codes", to: "totp#backup_codes", as: :backup_codes_totp
post "/totp/verify_password", to: "totp#verify_password", as: :verify_password_totp
get "/totp/regenerate_backup_codes", to: "totp#regenerate_backup_codes", as: :regenerate_backup_codes_totp
post "/totp/regenerate_backup_codes", to: "totp#create_new_backup_codes", as: :create_new_backup_codes_totp
post "/totp/complete_setup", to: "totp#complete_setup", as: :complete_totp_setup
# WebAuthn (Passkeys) routes
get '/webauthn/new', to: 'webauthn#new', as: :new_webauthn
post '/webauthn/challenge', to: 'webauthn#challenge'
post '/webauthn/create', to: 'webauthn#create'
delete '/webauthn/:id', to: 'webauthn#destroy', as: :webauthn_credential
get '/webauthn/check', to: 'webauthn#check'
get "/webauthn/new", to: "webauthn#new", as: :new_webauthn
post "/webauthn/challenge", to: "webauthn#challenge"
post "/webauthn/create", to: "webauthn#create"
delete "/webauthn/:id", to: "webauthn#destroy", as: :webauthn_credential
get "/webauthn/check", to: "webauthn#check"
# Admin routes
namespace :admin do