Refactor email delivery and background jobs system
- Switch from SolidQueue to async job processor for simpler background job handling - Remove SolidQueue gem and related configuration files - Add letter_opener gem for development email preview - Fix invitation email template issues (invitation_login_token method and route helper) - Configure SMTP settings via environment variables in application.rb - Add email delivery configuration banner on admin users page - Improve admin users page with inline action buttons and SMTP configuration warnings - Update development and production environments to use async processor - Add helper methods to detect SMTP configuration and filter out localhost settings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
275
test/controllers/api/forward_auth_controller_test.rb
Normal file
275
test/controllers/api/forward_auth_controller_test.rb
Normal file
@@ -0,0 +1,275 @@
|
||||
require "test_helper"
|
||||
|
||||
module Api
|
||||
class ForwardAuthControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@user = users(:one)
|
||||
@admin_user = users(:two)
|
||||
@inactive_user = users(:three)
|
||||
@group = groups(:one)
|
||||
@rule = ForwardAuthRule.create!(domain_pattern: "test.example.com", active: true)
|
||||
@inactive_rule = ForwardAuthRule.create!(domain_pattern: "inactive.example.com", active: false)
|
||||
end
|
||||
|
||||
# Authentication Tests
|
||||
test "should redirect to login when no session cookie" do
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 302
|
||||
assert_match %r{/signin}, response.location
|
||||
assert_equal "No session cookie", response.headers["X-Auth-Reason"]
|
||||
end
|
||||
|
||||
test "should redirect when session cookie is invalid" do
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "test.example.com",
|
||||
"Cookie" => "_clinch_session_id=invalid_session_id"
|
||||
}
|
||||
|
||||
assert_response 302
|
||||
assert_match %r{/signin}, response.location
|
||||
assert_equal "Invalid session", response.headers["X-Auth-Reason"]
|
||||
end
|
||||
|
||||
test "should redirect when session is expired" do
|
||||
expired_session = @user.sessions.create!(created_at: 1.year.ago)
|
||||
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "test.example.com",
|
||||
"Cookie" => "_clinch_session_id=#{expired_session.id}"
|
||||
}
|
||||
|
||||
assert_response 302
|
||||
assert_match %r{/signin}, response.location
|
||||
assert_equal "Session expired", response.headers["X-Auth-Reason"]
|
||||
end
|
||||
|
||||
test "should redirect when user is inactive" do
|
||||
sign_in_as(@inactive_user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 302
|
||||
assert_equal "User account is not active", response.headers["X-Auth-Reason"]
|
||||
end
|
||||
|
||||
test "should return 200 when user is authenticated" do
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 200
|
||||
end
|
||||
|
||||
# Rule Matching Tests
|
||||
test "should return 200 when matching rule exists" do
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 200
|
||||
end
|
||||
|
||||
test "should return 200 with default headers when no rule matches" do
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "unknown.example.com" }
|
||||
|
||||
assert_response 200
|
||||
assert_equal "X-Remote-User", response.headers["X-Remote-User"]
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
end
|
||||
|
||||
test "should return 403 when rule exists but is inactive" do
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "inactive.example.com" }
|
||||
|
||||
assert_response 403
|
||||
assert_equal "No authentication rule configured for this domain", response.headers["X-Auth-Reason"]
|
||||
end
|
||||
|
||||
test "should return 403 when rule exists but user not in allowed groups" do
|
||||
@rule.allowed_groups << @group
|
||||
sign_in_as(@user) # User not in group
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 403
|
||||
assert_match %r{permission to access this domain}, response.headers["X-Auth-Reason"]
|
||||
end
|
||||
|
||||
test "should return 200 when user is in allowed groups" do
|
||||
@rule.allowed_groups << @group
|
||||
@user.groups << @group
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 200
|
||||
end
|
||||
|
||||
# Domain Pattern Tests
|
||||
test "should match wildcard domains correctly" do
|
||||
wildcard_rule = ForwardAuthRule.create!(domain_pattern: "*.example.com", active: true)
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "app.example.com" }
|
||||
assert_response 200
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "api.example.com" }
|
||||
assert_response 200
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "other.com" }
|
||||
assert_response 200 # Falls back to default behavior
|
||||
end
|
||||
|
||||
test "should match exact domains correctly" do
|
||||
exact_rule = ForwardAuthRule.create!(domain_pattern: "api.example.com", active: true)
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "api.example.com" }
|
||||
assert_response 200
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "app.api.example.com" }
|
||||
assert_response 200 # Falls back to default behavior
|
||||
end
|
||||
|
||||
# Header Configuration Tests
|
||||
test "should return default headers when rule has no custom config" do
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 200
|
||||
assert_equal "X-Remote-User", response.headers.keys.find { |k| k.include?("User") }
|
||||
assert_equal "X-Remote-Email", response.headers.keys.find { |k| k.include?("Email") }
|
||||
assert_equal "X-Remote-Name", response.headers.keys.find { |k| k.include?("Name") }
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
end
|
||||
|
||||
test "should return custom headers when configured" do
|
||||
custom_rule = ForwardAuthRule.create!(
|
||||
domain_pattern: "custom.example.com",
|
||||
active: true,
|
||||
headers_config: {
|
||||
user: "X-WEBAUTH-USER",
|
||||
email: "X-WEBAUTH-EMAIL",
|
||||
groups: "X-WEBAUTH-ROLES"
|
||||
}
|
||||
)
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "custom.example.com" }
|
||||
|
||||
assert_response 200
|
||||
assert_equal "X-WEBAUTH-USER", response.headers.keys.find { |k| k.include?("USER") }
|
||||
assert_equal "X-WEBAUTH-EMAIL", response.headers.keys.find { |k| k.include?("EMAIL") }
|
||||
assert_equal @user.email_address, response.headers["X-WEBAUTH-USER"]
|
||||
end
|
||||
|
||||
test "should return no headers when all headers disabled" do
|
||||
no_headers_rule = ForwardAuthRule.create!(
|
||||
domain_pattern: "noheaders.example.com",
|
||||
active: true,
|
||||
headers_config: { user: "", email: "", name: "", groups: "", admin: "" }
|
||||
)
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "noheaders.example.com" }
|
||||
|
||||
assert_response 200
|
||||
auth_headers = response.headers.select { |k, v| k.match?(/^(X-|Remote-)/i) }
|
||||
assert_empty auth_headers
|
||||
end
|
||||
|
||||
test "should include groups header when user has groups" do
|
||||
@user.groups << @group
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 200
|
||||
assert_equal @group.name, response.headers["X-Remote-Groups"]
|
||||
end
|
||||
|
||||
test "should not include groups header when user has no groups" do
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 200
|
||||
assert_nil response.headers["X-Remote-Groups"]
|
||||
end
|
||||
|
||||
test "should include admin header correctly" do
|
||||
sign_in_as(@admin_user) # Assuming users(:two) is admin
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 200
|
||||
assert_equal "true", response.headers["X-Remote-Admin"]
|
||||
end
|
||||
|
||||
test "should include multiple groups when user has multiple groups" do
|
||||
group2 = groups(:two)
|
||||
@user.groups << @group
|
||||
@user.groups << group2
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
assert_response 200
|
||||
groups_header = response.headers["X-Remote-Groups"]
|
||||
assert_includes groups_header, @group.name
|
||||
assert_includes groups_header, group2.name
|
||||
end
|
||||
|
||||
# Header Fallback Tests
|
||||
test "should fall back to Host header when X-Forwarded-Host is missing" do
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "Host" => "test.example.com" }
|
||||
|
||||
assert_response 200
|
||||
end
|
||||
|
||||
test "should handle requests without any host headers" do
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify"
|
||||
|
||||
assert_response 200
|
||||
assert_equal "User #{@user.email_address} authenticated (no domain specified)",
|
||||
request.env["action_dispatch.instance"].instance_variable_get(:@logged_messages)&.last
|
||||
end
|
||||
|
||||
# Security Tests
|
||||
test "should handle malformed session IDs gracefully" do
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "test.example.com",
|
||||
"Cookie" => "_clinch_session_id=malformed_session_id_with_special_chars!@#$%"
|
||||
}
|
||||
|
||||
assert_response 302
|
||||
assert_equal "Invalid session", response.headers["X-Auth-Reason"]
|
||||
end
|
||||
|
||||
test "should handle very long domain names" do
|
||||
long_domain = "a" * 250 + ".example.com"
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => long_domain }
|
||||
|
||||
assert_response 200 # Should fall back to default behavior
|
||||
end
|
||||
|
||||
test "should handle case insensitive domain matching" do
|
||||
sign_in_as(@user)
|
||||
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "TEST.Example.COM" }
|
||||
|
||||
assert_response 200
|
||||
end
|
||||
end
|
||||
end
|
||||
322
test/integration/forward_auth_integration_test.rb
Normal file
322
test/integration/forward_auth_integration_test.rb
Normal file
@@ -0,0 +1,322 @@
|
||||
require "test_helper"
|
||||
|
||||
class ForwardAuthIntegrationTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@user = users(:one)
|
||||
@admin_user = users(:two)
|
||||
@group = groups(:one)
|
||||
@group2 = groups(:two)
|
||||
end
|
||||
|
||||
# Basic Authentication Flow Tests
|
||||
test "complete authentication flow: unauthenticated to authenticated" do
|
||||
# Step 1: Unauthenticated request should redirect
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
assert_response 302
|
||||
assert_match %r{/signin}, response.location
|
||||
assert_equal "No session cookie", response.headers["X-Auth-Reason"]
|
||||
|
||||
# Step 2: Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
assert_redirected_to "/"
|
||||
assert cookies[:session_id]
|
||||
|
||||
# Step 3: Authenticated request should succeed
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
assert_response 200
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
end
|
||||
|
||||
test "session persistence across multiple requests" do
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
session_cookie = cookies[:session_id]
|
||||
assert session_cookie
|
||||
|
||||
# Multiple requests should work with same session
|
||||
3.times do |i|
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "app#{i}.example.com" }
|
||||
assert_response 200
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
end
|
||||
end
|
||||
|
||||
test "session expiration handling" do
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
|
||||
# Manually expire the session
|
||||
session = Session.find_by(id: cookies.signed[:session_id])
|
||||
session.update!(created_at: 1.year.ago)
|
||||
|
||||
# Request should fail and redirect to login
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
assert_response 302
|
||||
assert_equal "Session expired", response.headers["X-Auth-Reason"]
|
||||
end
|
||||
|
||||
# Domain and Rule Integration Tests
|
||||
test "different domain patterns with same session" do
|
||||
# Create test rules
|
||||
wildcard_rule = ForwardAuthRule.create!(domain_pattern: "*.example.com", active: true)
|
||||
exact_rule = ForwardAuthRule.create!(domain_pattern: "api.example.com", active: true)
|
||||
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
|
||||
# Test wildcard domain
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "app.example.com" }
|
||||
assert_response 200
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
|
||||
# Test exact domain
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "api.example.com" }
|
||||
assert_response 200
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
|
||||
# Test non-matching domain (should use defaults)
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "other.example.com" }
|
||||
assert_response 200
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
end
|
||||
|
||||
test "group-based access control integration" do
|
||||
# Create restricted rule
|
||||
restricted_rule = ForwardAuthRule.create!(domain_pattern: "restricted.example.com", active: true)
|
||||
restricted_rule.allowed_groups << @group
|
||||
|
||||
# Sign in user without group
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
|
||||
# Should be denied access
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "restricted.example.com" }
|
||||
assert_response 403
|
||||
assert_match %r{permission to access this domain}, response.headers["X-Auth-Reason"]
|
||||
|
||||
# Add user to group
|
||||
@user.groups << @group
|
||||
|
||||
# Should now be allowed
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "restricted.example.com" }
|
||||
assert_response 200
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
end
|
||||
|
||||
# Header Configuration Integration Tests
|
||||
test "different header configurations with same user" do
|
||||
# Create rules with different header configs
|
||||
default_rule = ForwardAuthRule.create!(domain_pattern: "default.example.com", active: true)
|
||||
custom_rule = ForwardAuthRule.create!(
|
||||
domain_pattern: "custom.example.com",
|
||||
active: true,
|
||||
headers_config: { user: "X-WEBAUTH-USER", groups: "X-WEBAUTH-ROLES" }
|
||||
)
|
||||
no_headers_rule = ForwardAuthRule.create!(
|
||||
domain_pattern: "noheaders.example.com",
|
||||
active: true,
|
||||
headers_config: { user: "", email: "", name: "", groups: "", admin: "" }
|
||||
)
|
||||
|
||||
# Add user to groups
|
||||
@user.groups << @group
|
||||
@user.groups << @group2
|
||||
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
|
||||
# Test default headers
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "default.example.com" }
|
||||
assert_response 200
|
||||
assert_equal "X-Remote-User", response.headers.keys.find { |k| k.include?("User") }
|
||||
assert_equal "X-Remote-Groups", response.headers.keys.find { |k| k.include?("Groups") }
|
||||
|
||||
# Test custom headers
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "custom.example.com" }
|
||||
assert_response 200
|
||||
assert_equal "X-WEBAUTH-USER", response.headers.keys.find { |k| k.include?("USER") }
|
||||
assert_equal "X-WEBAUTH-ROLES", response.headers.keys.find { |k| k.include?("ROLES") }
|
||||
|
||||
# Test no headers
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "noheaders.example.com" }
|
||||
assert_response 200
|
||||
auth_headers = response.headers.select { |k, v| k.match?(/^(X-|Remote-)/i) }
|
||||
assert_empty auth_headers
|
||||
end
|
||||
|
||||
# Redirect URL Integration Tests
|
||||
test "redirect URL preserves original request information" do
|
||||
# Test with various redirect parameters
|
||||
test_cases = [
|
||||
{ rd: "https://app.example.com/", rm: "GET" },
|
||||
{ rd: "https://grafana.example.com/dashboard", rm: "POST" },
|
||||
{ rd: "https://metube.example.com/videos", rm: "PUT" }
|
||||
]
|
||||
|
||||
test_cases.each do |params|
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }, params: params
|
||||
|
||||
assert_response 302
|
||||
location = response.location
|
||||
|
||||
# Should contain the original redirect URL
|
||||
assert_includes location, params[:rd]
|
||||
assert_includes location, params[:rm]
|
||||
assert_includes location, "/signin"
|
||||
end
|
||||
end
|
||||
|
||||
test "return URL functionality after authentication" do
|
||||
# Initial request should set return URL
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "test.example.com",
|
||||
"X-Forwarded-Uri" => "/admin"
|
||||
}, params: { rd: "https://app.example.com/admin" }
|
||||
|
||||
assert_response 302
|
||||
location = response.location
|
||||
|
||||
# Extract return URL from location
|
||||
assert_match /rd=([^&]+)/, location
|
||||
return_url = CGI.unescape($1)
|
||||
assert_equal "https://app.example.com/admin", return_url
|
||||
|
||||
# Store session return URL
|
||||
return_to_after_authenticating = session[:return_to_after_authenticating]
|
||||
assert_equal "https://app.example.com/admin", return_to_after_authenticating
|
||||
end
|
||||
|
||||
# Multiple User Scenarios Integration Tests
|
||||
test "multiple users with different access levels" do
|
||||
regular_user = users(:one)
|
||||
admin_user = users(:two)
|
||||
|
||||
# Create restricted rule
|
||||
admin_rule = ForwardAuthRule.create!(
|
||||
domain_pattern: "admin.example.com",
|
||||
active: true,
|
||||
headers_config: { user: "X-Admin-User", admin: "X-Admin-Flag" }
|
||||
)
|
||||
|
||||
# Test regular user
|
||||
post "/signin", params: { email_address: regular_user.email_address, password: "password" }
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "admin.example.com" }
|
||||
assert_response 200
|
||||
assert_equal regular_user.email_address, response.headers["X-Admin-User"]
|
||||
|
||||
# Sign out
|
||||
delete "/session"
|
||||
|
||||
# Test admin user
|
||||
post "/signin", params: { email_address: admin_user.email_address, password: "password" }
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "admin.example.com" }
|
||||
assert_response 200
|
||||
assert_equal admin_user.email_address, response.headers["X-Admin-User"]
|
||||
assert_equal "true", response.headers["X-Admin-Flag"]
|
||||
end
|
||||
|
||||
# Security Integration Tests
|
||||
test "session hijacking prevention" do
|
||||
# User A signs in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
user_a_session = cookies[:session_id]
|
||||
|
||||
# User B signs in
|
||||
delete "/session"
|
||||
post "/signin", params: { email_address: @admin_user.email_address, password: "password" }
|
||||
user_b_session = cookies[:session_id]
|
||||
|
||||
# User A's session should still work
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "test.example.com",
|
||||
"Cookie" => "_clinch_session_id=#{user_a_session}"
|
||||
}
|
||||
assert_response 200
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
|
||||
# User B's session should work
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "test.example.com",
|
||||
"Cookie" => "_clinch_session_id=#{user_b_session}"
|
||||
}
|
||||
assert_response 200
|
||||
assert_equal @admin_user.email_address, response.headers["X-Remote-User"]
|
||||
end
|
||||
|
||||
test "concurrent requests with same session" do
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
session_cookie = cookies[:session_id]
|
||||
|
||||
# Simulate concurrent requests
|
||||
threads = []
|
||||
results = []
|
||||
|
||||
5.times do |i|
|
||||
threads << Thread.new do
|
||||
# Create a new integration test instance for this thread
|
||||
test_instance = self.class.new
|
||||
test_instance.setup_controller_request_and_response
|
||||
|
||||
test_instance.get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "app#{i}.example.com",
|
||||
"Cookie" => "_clinch_session_id=#{session_cookie}"
|
||||
}
|
||||
|
||||
results << {
|
||||
thread_id: i,
|
||||
status: test_instance.response.status,
|
||||
user: test_instance.response.headers["X-Remote-User"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
threads.each(&:join)
|
||||
|
||||
# All requests should succeed
|
||||
results.each do |result|
|
||||
assert_equal 200, result[:status], "Thread #{result[:thread_id]} failed"
|
||||
assert_equal @user.email_address, result[:user], "Thread #{result[:thread_id]} has wrong user"
|
||||
end
|
||||
end
|
||||
|
||||
# Performance Integration Tests
|
||||
test "response times are reasonable" do
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
|
||||
# Test multiple requests
|
||||
start_time = Time.current
|
||||
|
||||
10.times do |i|
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "app#{i}.example.com" }
|
||||
assert_response 200
|
||||
end
|
||||
|
||||
end_time = Time.current
|
||||
total_time = end_time - start_time
|
||||
average_time = total_time / 10
|
||||
|
||||
# Each request should take less than 100ms on average
|
||||
assert average_time < 0.1, "Average response time #{average_time}s is too slow"
|
||||
end
|
||||
|
||||
# Error Handling Integration Tests
|
||||
test "graceful handling of malformed headers" do
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
|
||||
# Test various malformed header combinations
|
||||
test_cases = [
|
||||
{ "X-Forwarded-Host" => nil },
|
||||
{ "X-Forwarded-Host" => "" },
|
||||
{ "X-Forwarded-Host" => " " },
|
||||
{ "Host" => nil },
|
||||
{ "Host" => "" }
|
||||
]
|
||||
|
||||
test_cases.each_with_index do |headers, i|
|
||||
get "/api/verify", headers: headers
|
||||
assert_response 200, "Failed on test case #{i}: #{headers.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -124,4 +124,272 @@ class ForwardAuthRuleTest < ActiveSupport::TestCase
|
||||
|
||||
assert_not @rule.user_allowed?(user)
|
||||
end
|
||||
|
||||
# Header Configuration Tests
|
||||
test "effective_headers should return default headers when no custom config" do
|
||||
@rule.save!
|
||||
|
||||
expected = ForwardAuthRule::DEFAULT_HEADERS
|
||||
assert_equal expected, @rule.effective_headers
|
||||
end
|
||||
|
||||
test "effective_headers should merge custom headers with defaults" do
|
||||
@rule.save!
|
||||
@rule.update!(headers_config: { user: "X-Forwarded-User", email: "X-Forwarded-Email" })
|
||||
|
||||
expected = ForwardAuthRule::DEFAULT_HEADERS.merge(
|
||||
user: "X-Forwarded-User",
|
||||
email: "X-Forwarded-Email"
|
||||
)
|
||||
assert_equal expected, @rule.effective_headers
|
||||
end
|
||||
|
||||
test "headers_for_user should generate correct headers for user with groups" do
|
||||
group = groups(:one)
|
||||
user = users(:one)
|
||||
user.groups << group
|
||||
@rule.save!
|
||||
|
||||
headers = @rule.headers_for_user(user)
|
||||
|
||||
assert_equal user.email_address, headers["X-Remote-User"]
|
||||
assert_equal user.email_address, headers["X-Remote-Email"]
|
||||
assert_equal user.email_address, headers["X-Remote-Name"]
|
||||
assert_equal group.name, headers["X-Remote-Groups"]
|
||||
assert_equal "true", headers["X-Remote-Admin"]
|
||||
end
|
||||
|
||||
test "headers_for_user should generate correct headers for user without groups" do
|
||||
user = users(:one)
|
||||
@rule.save!
|
||||
|
||||
headers = @rule.headers_for_user(user)
|
||||
|
||||
assert_equal user.email_address, headers["X-Remote-User"]
|
||||
assert_equal user.email_address, headers["X-Remote-Email"]
|
||||
assert_equal user.email_address, headers["X-Remote-Name"]
|
||||
assert_nil headers["X-Remote-Groups"] # No groups, no header
|
||||
assert_equal "true", headers["X-Remote-Admin"]
|
||||
end
|
||||
|
||||
test "headers_for_user should work with custom headers" do
|
||||
user = users(:one)
|
||||
@rule.update!(headers_config: {
|
||||
user: "X-Forwarded-User",
|
||||
groups: "X-Custom-Groups"
|
||||
})
|
||||
|
||||
headers = @rule.headers_for_user(user)
|
||||
|
||||
assert_equal user.email_address, headers["X-Forwarded-User"]
|
||||
assert_nil headers["X-Remote-User"] # Should be overridden
|
||||
assert_equal user.email_address, headers["X-Remote-Email"] # Default preserved
|
||||
assert_nil headers["X-Custom-Groups"] # User has no groups
|
||||
end
|
||||
|
||||
test "headers_for_user should return empty hash when all headers disabled" do
|
||||
user = users(:one)
|
||||
@rule.update!(headers_config: {
|
||||
user: "",
|
||||
email: "",
|
||||
name: "",
|
||||
groups: "",
|
||||
admin: ""
|
||||
})
|
||||
|
||||
headers = @rule.headers_for_user(user)
|
||||
assert_empty headers
|
||||
end
|
||||
|
||||
test "headers_disabled? should correctly identify disabled headers" do
|
||||
@rule.save!
|
||||
assert_not @rule.headers_disabled?
|
||||
|
||||
@rule.update!(headers_config: { user: "X-Custom-User" })
|
||||
assert_not @rule.headers_disabled?
|
||||
|
||||
@rule.update!(headers_config: { user: "", email: "", name: "", groups: "", admin: "" })
|
||||
assert @rule.headers_disabled?
|
||||
end
|
||||
|
||||
# Additional Domain Pattern Tests
|
||||
test "matches_domain? should handle complex patterns" do
|
||||
@rule.save!
|
||||
|
||||
# Test multiple wildcards
|
||||
@rule.update!(domain_pattern: "*.*.example.com")
|
||||
assert @rule.matches_domain?("app.dev.example.com")
|
||||
assert @rule.matches_domain?("api.staging.example.com")
|
||||
assert_not @rule.matches_domain?("example.com")
|
||||
assert_not @rule.matches_domain?("app.example.org")
|
||||
|
||||
# Test exact domain with dots
|
||||
@rule.update!(domain_pattern: "api.v2.example.com")
|
||||
assert @rule.matches_domain?("api.v2.example.com")
|
||||
assert_not @rule.matches_domain?("api.v3.example.com")
|
||||
assert_not @rule.matches_domain?("v2.api.example.com")
|
||||
end
|
||||
|
||||
test "matches_domain? should handle case insensitivity" do
|
||||
@rule.update!(domain_pattern: "*.EXAMPLE.COM")
|
||||
@rule.save!
|
||||
|
||||
assert @rule.matches_domain?("app.example.com")
|
||||
assert @rule.matches_domain?("APP.EXAMPLE.COM")
|
||||
assert @rule.matches_domain?("App.Example.Com")
|
||||
end
|
||||
|
||||
test "matches_domain? should handle empty and nil domains" do
|
||||
@rule.save!
|
||||
|
||||
assert_not @rule.matches_domain?("")
|
||||
assert_not @rule.matches_domain?(nil)
|
||||
end
|
||||
|
||||
# Advanced Header Configuration Tests
|
||||
test "headers_for_user should handle partial header configuration" do
|
||||
user = users(:one)
|
||||
user.groups << groups(:one)
|
||||
@rule.update!(headers_config: {
|
||||
user: "X-Custom-User",
|
||||
email: "", # Disabled
|
||||
groups: "X-Custom-Groups"
|
||||
})
|
||||
@rule.save!
|
||||
|
||||
headers = @rule.headers_for_user(user)
|
||||
|
||||
# Should include custom user header
|
||||
assert_equal "X-Custom-User", headers.keys.find { |k| k.include?("User") }
|
||||
assert_equal user.email_address, headers["X-Custom-User"]
|
||||
|
||||
# Should include default email header (not overridden)
|
||||
assert_equal "X-Remote-Email", headers.keys.find { |k| k.include?("Email") }
|
||||
assert_equal user.email_address, headers["X-Remote-Email"]
|
||||
|
||||
# Should include custom groups header
|
||||
assert_equal "X-Custom-Groups", headers.keys.find { |k| k.include?("Groups") }
|
||||
assert_equal groups(:one).name, headers["X-Custom-Groups"]
|
||||
|
||||
# Should include default name header (not overridden)
|
||||
assert_equal "X-Remote-Name", headers.keys.find { |k| k.include?("Name") }
|
||||
end
|
||||
|
||||
test "headers_for_user should handle user without groups when groups header configured" do
|
||||
user = users(:one)
|
||||
user.groups.clear # No groups
|
||||
@rule.update!(headers_config: { groups: "X-Custom-Groups" })
|
||||
@rule.save!
|
||||
|
||||
headers = @rule.headers_for_user(user)
|
||||
|
||||
# Should not include groups header for user with no groups
|
||||
assert_nil headers["X-Custom-Groups"]
|
||||
assert_nil headers["X-Remote-Groups"]
|
||||
end
|
||||
|
||||
test "headers_for_user should handle non-admin user correctly" do
|
||||
user = users(:one)
|
||||
# Ensure user is not admin
|
||||
user.update!(admin: false)
|
||||
@rule.save!
|
||||
|
||||
headers = @rule.headers_for_user(user)
|
||||
|
||||
assert_equal "false", headers["X-Remote-Admin"]
|
||||
end
|
||||
|
||||
test "headers_for_user should work with nil headers_config" do
|
||||
user = users(:one)
|
||||
@rule.update!(headers_config: nil)
|
||||
@rule.save!
|
||||
|
||||
headers = @rule.headers_for_user(user)
|
||||
|
||||
# Should use default headers
|
||||
assert_equal "X-Remote-User", headers.keys.find { |k| k.include?("User") }
|
||||
assert_equal user.email_address, headers["X-Remote-User"]
|
||||
end
|
||||
|
||||
test "effective_headers should handle symbol keys in headers_config" do
|
||||
@rule.update!(headers_config: { user: "X-Symbol-User", email: "X-Symbol-Email" })
|
||||
@rule.save!
|
||||
|
||||
effective = @rule.effective_headers
|
||||
|
||||
assert_equal "X-Symbol-User", effective[:user]
|
||||
assert_equal "X-Symbol-Email", effective[:email]
|
||||
assert_equal "X-Remote-Name", effective[:name] # Default
|
||||
end
|
||||
|
||||
test "effective_headers should handle string keys in headers_config" do
|
||||
@rule.update!(headers_config: { "user" => "X-String-User", "email" => "X-String-Email" })
|
||||
@rule.save!
|
||||
|
||||
effective = @rule.effective_headers
|
||||
|
||||
assert_equal "X-String-User", effective[:user]
|
||||
assert_equal "X-String-Email", effective[:email]
|
||||
assert_equal "X-Remote-Name", effective[:name] # Default
|
||||
end
|
||||
|
||||
# Policy and Access Control Tests
|
||||
test "policy_for_user should handle user with TOTP enabled" do
|
||||
user = users(:one)
|
||||
user.update!(totp_secret: "test_secret")
|
||||
@rule.allowed_groups << groups(:one)
|
||||
user.groups << groups(:one)
|
||||
@rule.save!
|
||||
|
||||
policy = @rule.policy_for_user(user)
|
||||
assert_equal "two_factor", policy
|
||||
end
|
||||
|
||||
test "policy_for_user should handle user without TOTP" do
|
||||
user = users(:one)
|
||||
user.update!(totp_secret: nil)
|
||||
@rule.allowed_groups << groups(:one)
|
||||
user.groups << groups(:one)
|
||||
@rule.save!
|
||||
|
||||
policy = @rule.policy_for_user(user)
|
||||
assert_equal "one_factor", policy
|
||||
end
|
||||
|
||||
test "policy_for_user should handle user with multiple groups" do
|
||||
user = users(:one)
|
||||
group1 = groups(:one)
|
||||
group2 = groups(:two)
|
||||
@rule.allowed_groups << group1
|
||||
@rule.allowed_groups << group2
|
||||
user.groups << group1
|
||||
@rule.save!
|
||||
|
||||
policy = @rule.policy_for_user(user)
|
||||
assert_equal "one_factor", policy
|
||||
end
|
||||
|
||||
test "user_allowed? should handle user with multiple groups, one allowed" do
|
||||
user = users(:one)
|
||||
allowed_group = groups(:one)
|
||||
other_group = groups(:two)
|
||||
@rule.allowed_groups << allowed_group
|
||||
user.groups << allowed_group
|
||||
user.groups << other_group
|
||||
@rule.save!
|
||||
|
||||
assert @rule.user_allowed?(user)
|
||||
end
|
||||
|
||||
test "user_allowed? should handle user with multiple groups, none allowed" do
|
||||
user = users(:one)
|
||||
group1 = groups(:one)
|
||||
group2 = groups(:two)
|
||||
# Don't add any groups to allowed_groups
|
||||
user.groups << group1
|
||||
user.groups << group2
|
||||
@rule.save!
|
||||
|
||||
assert_not @rule.user_allowed?(user)
|
||||
end
|
||||
end
|
||||
|
||||
96
test/simple_role_test.rb
Normal file
96
test/simple_role_test.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# Simple test script to verify role mapping functionality
|
||||
# Run with: ruby test/simple_role_test.rb
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
puts "🧪 Testing OIDC Role Mapping functionality..."
|
||||
|
||||
begin
|
||||
# Create test user
|
||||
user = User.create!(
|
||||
email_address: "test#{Time.current.to_i}@example.com",
|
||||
password: "password123",
|
||||
admin: false,
|
||||
status: :active
|
||||
)
|
||||
puts "✅ Created test user: #{user.email_address}"
|
||||
|
||||
# Create test application
|
||||
application = Application.create!(
|
||||
name: "Test Role App",
|
||||
slug: "test-role-app-#{Time.current.to_i}",
|
||||
app_type: "oidc",
|
||||
role_mapping_mode: "oidc_managed"
|
||||
)
|
||||
puts "✅ Created test application: #{application.name}"
|
||||
|
||||
# Create role
|
||||
role = application.application_roles.create!(
|
||||
name: "admin",
|
||||
display_name: "Administrator",
|
||||
description: "Full access role"
|
||||
)
|
||||
puts "✅ Created role: #{role.name}"
|
||||
|
||||
# Test role assignment
|
||||
application.assign_role_to_user!(user, "admin", source: 'manual')
|
||||
puts "✅ Assigned role to user"
|
||||
|
||||
# Verify role assignment
|
||||
unless application.user_has_role?(user, "admin")
|
||||
raise "Role should be assigned to user"
|
||||
end
|
||||
puts "✅ Verified role assignment"
|
||||
|
||||
# Test role mapping engine
|
||||
claims = { "roles" => ["admin", "editor"] }
|
||||
RoleMappingEngine.sync_user_roles!(user, application, claims)
|
||||
puts "✅ Synced roles from OIDC claims"
|
||||
|
||||
# Test JWT generation with roles
|
||||
token = OidcJwtService.generate_id_token(user, application)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
unless decoded["roles"]&.include?("admin")
|
||||
raise "JWT should contain roles"
|
||||
end
|
||||
puts "✅ JWT includes roles claim"
|
||||
|
||||
# Test custom claim name
|
||||
application.update!(role_claim_name: "user_roles")
|
||||
token = OidcJwtService.generate_id_token(user, application)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
unless decoded["user_roles"]&.include?("admin")
|
||||
raise "JWT should use custom claim name"
|
||||
end
|
||||
puts "✅ Custom claim name works"
|
||||
|
||||
# Test role prefix filtering
|
||||
application.update!(role_prefix: "app-")
|
||||
role.update!(name: "app-admin")
|
||||
application.assign_role_to_user!(user, "app-admin", source: 'manual')
|
||||
|
||||
claims = { "roles" => ["app-admin", "external-role"] }
|
||||
RoleMappingEngine.sync_user_roles!(user, application, claims)
|
||||
unless application.user_has_role?(user, "app-admin")
|
||||
raise "Prefixed role should be assigned"
|
||||
end
|
||||
if application.user_has_role?(user, "external-role")
|
||||
raise "Non-prefixed role should be filtered"
|
||||
end
|
||||
puts "✅ Role prefix filtering works"
|
||||
|
||||
# Cleanup
|
||||
user.destroy
|
||||
application.destroy
|
||||
puts "🧹 Cleaned up test data"
|
||||
|
||||
puts "\n🎉 All tests passed! OIDC Role Mapping is working correctly."
|
||||
|
||||
rescue => e
|
||||
puts "❌ Test failed: #{e.message}"
|
||||
puts e.backtrace.first(5)
|
||||
exit 1
|
||||
end
|
||||
|
||||
398
test/system/forward_auth_system_test.rb
Normal file
398
test/system/forward_auth_system_test.rb
Normal file
@@ -0,0 +1,398 @@
|
||||
require "test_helper"
|
||||
|
||||
class ForwardAuthSystemTest < ActionDispatch::SystemTestCase
|
||||
driven_by :rack_test
|
||||
|
||||
setup do
|
||||
@user = users(:one)
|
||||
@admin_user = users(:two)
|
||||
@group = groups(:one)
|
||||
@group2 = groups(:two)
|
||||
end
|
||||
|
||||
# End-to-End Authentication Flow Tests
|
||||
test "complete forward auth flow with default headers" do
|
||||
# Create a rule with default headers
|
||||
rule = ForwardAuthRule.create!(domain_pattern: "app.example.com", active: true)
|
||||
|
||||
# Step 1: Unauthenticated request to protected resource
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "app.example.com",
|
||||
"X-Forwarded-Uri" => "/dashboard"
|
||||
}, params: { rd: "https://app.example.com/dashboard" }
|
||||
|
||||
assert_response 302
|
||||
location = response.location
|
||||
assert_match %r{/signin}, location
|
||||
assert_match %r{rd=https://app.example.com/dashboard}, location
|
||||
|
||||
# Step 2: Extract return URL from session
|
||||
assert_equal "https://app.example.com/dashboard", session[:return_to_after_authenticating]
|
||||
|
||||
# Step 3: Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
|
||||
assert_response 302
|
||||
assert_redirected_to "https://app.example.com/dashboard"
|
||||
|
||||
# Step 4: Authenticated request to protected resource
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "app.example.com" }
|
||||
|
||||
assert_response 200
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
assert_equal @user.email_address, response.headers["X-Remote-Email"]
|
||||
assert_equal "false", response.headers["X-Remote-Admin"] unless @user.admin?
|
||||
end
|
||||
|
||||
test "multiple domain access with single session" do
|
||||
# Create rules for different applications
|
||||
app_rule = ForwardAuthRule.create!(domain_pattern: "app.example.com", active: true)
|
||||
grafana_rule = ForwardAuthRule.create!(
|
||||
domain_pattern: "grafana.example.com",
|
||||
active: true,
|
||||
headers_config: { user: "X-WEBAUTH-USER", email: "X-WEBAUTH-EMAIL" }
|
||||
)
|
||||
metube_rule = ForwardAuthRule.create!(
|
||||
domain_pattern: "metube.example.com",
|
||||
active: true,
|
||||
headers_config: { user: "", email: "", name: "", groups: "", admin: "" }
|
||||
)
|
||||
|
||||
# Sign in once
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
assert_response 302
|
||||
assert_redirected_to "/"
|
||||
|
||||
# Test access to different applications
|
||||
# App with default headers
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "app.example.com" }
|
||||
assert_response 200
|
||||
assert_equal "X-Remote-User", response.headers.keys.find { |k| k.include?("User") }
|
||||
|
||||
# Grafana with custom headers
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "grafana.example.com" }
|
||||
assert_response 200
|
||||
assert_equal "X-WEBAUTH-USER", response.headers.keys.find { |k| k.include?("USER") }
|
||||
|
||||
# Metube with no headers
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "metube.example.com" }
|
||||
assert_response 200
|
||||
auth_headers = response.headers.select { |k, v| k.match?(/^(X-|Remote-)/i) }
|
||||
assert_empty auth_headers
|
||||
end
|
||||
|
||||
# Group-Based Access Control System Tests
|
||||
test "group-based access control with multiple groups" do
|
||||
# Create restricted rule
|
||||
restricted_rule = ForwardAuthRule.create!(
|
||||
domain_pattern: "admin.example.com",
|
||||
active: true
|
||||
)
|
||||
restricted_rule.allowed_groups << @group
|
||||
restricted_rule.allowed_groups << @group2
|
||||
|
||||
# Add user to first group only
|
||||
@user.groups << @group
|
||||
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
assert_response 302
|
||||
|
||||
# Should have access (in allowed group)
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "admin.example.com" }
|
||||
assert_response 200
|
||||
assert_equal @group.name, response.headers["X-Remote-Groups"]
|
||||
|
||||
# Add user to second group
|
||||
@user.groups << @group2
|
||||
|
||||
# Should show multiple groups
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "admin.example.com" }
|
||||
assert_response 200
|
||||
groups_header = response.headers["X-Remote-Groups"]
|
||||
assert_includes groups_header, @group.name
|
||||
assert_includes groups_header, @group2.name
|
||||
|
||||
# Remove user from all groups
|
||||
@user.groups.clear
|
||||
|
||||
# Should be denied
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "admin.example.com" }
|
||||
assert_response 403
|
||||
end
|
||||
|
||||
test "bypass mode when no groups assigned to rule" do
|
||||
# Create bypass rule (no groups)
|
||||
bypass_rule = ForwardAuthRule.create!(
|
||||
domain_pattern: "public.example.com",
|
||||
active: true
|
||||
)
|
||||
|
||||
# Create user with no groups
|
||||
@user.groups.clear
|
||||
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
assert_response 302
|
||||
|
||||
# Should have access (bypass mode)
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "public.example.com" }
|
||||
assert_response 200
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
end
|
||||
|
||||
# Security System Tests
|
||||
test "session security and isolation" do
|
||||
# User A signs in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
user_a_session = cookies[:session_id]
|
||||
|
||||
# User B signs in
|
||||
delete "/session"
|
||||
post "/signin", params: { email_address: @admin_user.email_address, password: "password" }
|
||||
user_b_session = cookies[:session_id]
|
||||
|
||||
# User A should still be able to access resources
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "test.example.com",
|
||||
"Cookie" => "_clinch_session_id=#{user_a_session}"
|
||||
}
|
||||
assert_response 200
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
|
||||
# User B should be able to access resources
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "test.example.com",
|
||||
"Cookie" => "_clinch_session_id=#{user_b_session}"
|
||||
}
|
||||
assert_response 200
|
||||
assert_equal @admin_user.email_address, response.headers["X-Remote-User"]
|
||||
|
||||
# Sessions should be independent
|
||||
assert_not_equal user_a_session, user_b_session
|
||||
end
|
||||
|
||||
test "session expiration and cleanup" do
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
session_id = cookies[:session_id]
|
||||
|
||||
# Should work initially
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
assert_response 200
|
||||
|
||||
# Manually expire session
|
||||
session = Session.find(session_id)
|
||||
session.update!(created_at: 1.year.ago)
|
||||
|
||||
# Should redirect to login
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
assert_response 302
|
||||
assert_equal "Session expired", response.headers["X-Auth-Reason"]
|
||||
|
||||
# Session should be cleaned up
|
||||
assert_nil Session.find_by(id: session_id)
|
||||
end
|
||||
|
||||
test "concurrent access with rate limiting considerations" do
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
session_cookie = cookies[:session_id]
|
||||
|
||||
# Simulate multiple concurrent requests from different IPs
|
||||
threads = []
|
||||
results = []
|
||||
|
||||
10.times do |i|
|
||||
threads << Thread.new do
|
||||
start_time = Time.current
|
||||
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "app#{i}.example.com",
|
||||
"X-Forwarded-For" => "192.168.1.#{100 + i}",
|
||||
"Cookie" => "_clinch_session_id=#{session_cookie}"
|
||||
}
|
||||
|
||||
end_time = Time.current
|
||||
|
||||
results << {
|
||||
thread_id: i,
|
||||
status: response.status,
|
||||
user: response.headers["X-Remote-User"],
|
||||
duration: end_time - start_time
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
threads.each(&:join)
|
||||
|
||||
# All requests should succeed
|
||||
results.each do |result|
|
||||
assert_equal 200, result[:status], "Thread #{result[:thread_id]} failed"
|
||||
assert_equal @user.email_address, result[:user], "Thread #{result[:thread_id]} has wrong user"
|
||||
assert result[:duration] < 1.0, "Thread #{result[:thread_id]} was too slow"
|
||||
end
|
||||
end
|
||||
|
||||
# Complex Scenario System Tests
|
||||
test "complex multi-application scenario" do
|
||||
# Setup multiple applications with different requirements
|
||||
apps = [
|
||||
{
|
||||
domain: "dashboard.example.com",
|
||||
headers_config: { user: "X-DASHBOARD-USER", groups: "X-DASHBOARD-GROUPS" },
|
||||
groups: [@group]
|
||||
},
|
||||
{
|
||||
domain: "api.example.com",
|
||||
headers_config: { user: "X-API-USER", email: "X-API-EMAIL" },
|
||||
groups: []
|
||||
},
|
||||
{
|
||||
domain: "logs.example.com",
|
||||
headers_config: { user: "", email: "", name: "", groups: "", admin: "" },
|
||||
groups: []
|
||||
}
|
||||
]
|
||||
|
||||
# Create rules for each app
|
||||
rules = apps.map do |app|
|
||||
rule = ForwardAuthRule.create!(
|
||||
domain_pattern: app[:domain],
|
||||
active: true,
|
||||
headers_config: app[:headers_config]
|
||||
)
|
||||
app[:groups].each { |group| rule.allowed_groups << group }
|
||||
rule
|
||||
end
|
||||
|
||||
# Add user to required groups
|
||||
@user.groups << @group
|
||||
|
||||
# Sign in once
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
assert_response 302
|
||||
|
||||
# Test access to each application
|
||||
apps.each do |app|
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => app[:domain] }
|
||||
assert_response 200, "Failed for #{app[:domain]}"
|
||||
|
||||
# Verify headers are correct
|
||||
if app[:headers_config][:user].present?
|
||||
assert_equal app[:headers_config][:user],
|
||||
response.headers.keys.find { |k| k.include?("USER") },
|
||||
"Wrong user header for #{app[:domain]}"
|
||||
assert_equal @user.email_address, response.headers[app[:headers_config][:user]]
|
||||
else
|
||||
# Should have no auth headers
|
||||
auth_headers = response.headers.select { |k, v| k.match?(/^(X-|Remote-)/i) }
|
||||
assert_empty auth_headers, "Should have no headers for #{app[:domain]}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "domain pattern edge cases" do
|
||||
# Test various domain patterns
|
||||
patterns = [
|
||||
{ pattern: "*.example.com", domains: ["app.example.com", "api.example.com", "sub.app.example.com"] },
|
||||
{ pattern: "api.*.com", domains: ["api.example.com", "api.test.com"] },
|
||||
{ pattern: "*.*.example.com", domains: ["app.dev.example.com", "api.staging.example.com"] }
|
||||
]
|
||||
|
||||
patterns.each do |pattern_config|
|
||||
rule = ForwardAuthRule.create!(
|
||||
domain_pattern: pattern_config[:pattern],
|
||||
active: true
|
||||
)
|
||||
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
|
||||
# Test each domain
|
||||
pattern_config[:domains].each do |domain|
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => domain }
|
||||
assert_response 200, "Failed for pattern #{pattern_config[:pattern]} with domain #{domain}"
|
||||
assert_equal @user.email_address, response.headers["X-Remote-User"]
|
||||
end
|
||||
|
||||
# Clean up for next test
|
||||
delete "/session"
|
||||
end
|
||||
end
|
||||
|
||||
# Performance System Tests
|
||||
test "system performance under load" do
|
||||
# Create test rule
|
||||
rule = ForwardAuthRule.create!(domain_pattern: "loadtest.example.com", active: true)
|
||||
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
session_cookie = cookies[:session_id]
|
||||
|
||||
# Performance test
|
||||
start_time = Time.current
|
||||
request_count = 50
|
||||
results = []
|
||||
|
||||
request_count.times do |i|
|
||||
request_start = Time.current
|
||||
|
||||
get "/api/verify", headers: {
|
||||
"X-Forwarded-Host" => "app#{i}.loadtest.example.com",
|
||||
"Cookie" => "_clinch_session_id=#{session_cookie}"
|
||||
}
|
||||
|
||||
request_end = Time.current
|
||||
|
||||
results << {
|
||||
request_id: i,
|
||||
status: response.status,
|
||||
duration: request_end - request_start
|
||||
}
|
||||
end
|
||||
|
||||
total_time = Time.current - start_time
|
||||
average_duration = results.map { |r| r[:duration] }.sum / request_count
|
||||
|
||||
# Performance assertions
|
||||
assert total_time < 5.0, "Total time #{total_time}s is too slow"
|
||||
assert average_duration < 0.1, "Average request time #{average_duration}s is too slow"
|
||||
assert results.all? { |r| r[:status] == 200 }, "Some requests failed"
|
||||
|
||||
# Calculate requests per second
|
||||
rps = request_count / total_time
|
||||
assert rps > 10, "Requests per second #{rps} is too low"
|
||||
end
|
||||
|
||||
# Error Recovery System Tests
|
||||
test "graceful degradation with database issues" do
|
||||
# Sign in first
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
assert_response 302
|
||||
|
||||
# Simulate database connection issue by mocking
|
||||
original_method = Session.method(:find_by)
|
||||
|
||||
# Mock database failure
|
||||
Session.define_singleton_method(:find_by) do |id|
|
||||
raise ActiveRecord::ConnectionNotEstablished, "Database connection lost"
|
||||
end
|
||||
|
||||
begin
|
||||
# Request should handle the error gracefully
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
|
||||
# Should return 302 (redirect to login) rather than 500 error
|
||||
assert_response 302, "Should gracefully handle database issues"
|
||||
assert_equal "Invalid session", response.headers["X-Auth-Reason"]
|
||||
ensure
|
||||
# Restore original method
|
||||
Session.define_singleton_method(:find_by, original_method)
|
||||
end
|
||||
|
||||
# Normal operation should still work
|
||||
get "/api/verify", headers: { "X-Forwarded-Host" => "test.example.com" }
|
||||
assert_response 200
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user