chore: vendor sglang v0.5.10 snapshot

This commit is contained in:
2026-04-24 12:29:36 +00:00
parent 78f0d15221
commit bded08301f
4308 changed files with 1200894 additions and 2 deletions

View File

@@ -0,0 +1,937 @@
//! Integration tests for control plane authentication.
//!
//! Tests the full authentication flow including:
//! - JWT token validation with mock JWKS server
//! - API key authentication
//! - Role-based access control
//! - Token expiration and replay protection
use std::{
net::SocketAddr,
sync::LazyLock,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use axum::{routing::get, Json, Router};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use jsonwebtoken::{encode, EncodingKey, Header};
use rsa::{traits::PublicKeyParts, RsaPrivateKey};
use serde::{Deserialize, Serialize};
use serde_json::json;
use smg::auth::{ApiKeyEntry, ControlPlaneAuthConfig, ControlPlaneAuthState, JwtConfig, Role};
use tokio::net::TcpListener;
const TEST_KEY_ID: &str = "test-key-1";
/// Test RSA key pair - generates matching public/private key components
struct TestKeyPair {
private_key_pem: String,
n_base64url: String,
e_base64url: String,
}
impl TestKeyPair {
fn generate() -> Self {
// Generate a new 2048-bit RSA key pair for testing
use rsa::{pkcs8::EncodePrivateKey, rand_core::OsRng};
let mut rng = OsRng;
let private_key =
RsaPrivateKey::new(&mut rng, 2048).expect("Failed to generate RSA key pair");
// Get the public key components
let n_bytes = private_key.n().to_bytes_be();
let e_bytes = private_key.e().to_bytes_be();
// Encode to base64url (no padding) for JWKS
let n_base64url = URL_SAFE_NO_PAD.encode(&n_bytes);
let e_base64url = URL_SAFE_NO_PAD.encode(&e_bytes);
// Export private key as PEM
let private_key_pem = private_key
.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
.expect("Failed to export private key")
.to_string();
Self {
private_key_pem,
n_base64url,
e_base64url,
}
}
}
/// Lazily initialized test key pair (shared across all tests)
static TEST_KEYS: LazyLock<TestKeyPair> = LazyLock::new(TestKeyPair::generate);
/// Claims structure for test tokens
#[derive(Debug, Serialize, Deserialize)]
struct TestClaims {
sub: String,
iss: String,
aud: String,
exp: u64,
iat: u64,
#[serde(skip_serializing_if = "Option::is_none")]
jti: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
roles: Option<Vec<String>>,
}
/// Create a test JWT token using the generated test key pair
fn create_test_token(claims: &TestClaims) -> String {
let mut header = Header::new(jsonwebtoken::Algorithm::RS256);
header.kid = Some(TEST_KEY_ID.to_string());
let key = EncodingKey::from_rsa_pem(TEST_KEYS.private_key_pem.as_bytes())
.expect("Failed to create encoding key");
encode(&header, claims, &key).expect("Failed to encode token")
}
/// Create claims with default values
fn create_claims(sub: &str, roles: Vec<&str>) -> TestClaims {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
TestClaims {
sub: sub.to_string(),
iss: "https://test-issuer.example.com".to_string(),
aud: "test-gateway".to_string(),
exp: now + 3600, // 1 hour from now
iat: now,
jti: Some(uuid::Uuid::new_v4().to_string()),
name: Some("Test User".to_string()),
roles: Some(roles.into_iter().map(String::from).collect()),
}
}
/// Start a mock JWKS server using the generated test key pair
async fn start_mock_jwks_server() -> (SocketAddr, tokio::task::JoinHandle<()>) {
// Use the generated key pair components
let jwks_response = json!({
"keys": [{
"kty": "RSA",
"use": "sig",
"kid": TEST_KEY_ID,
"alg": "RS256",
"n": TEST_KEYS.n_base64url,
"e": TEST_KEYS.e_base64url
}]
});
let app = Router::new().route(
"/.well-known/jwks.json",
get(move || {
let jwks = jwks_response.clone();
async move { Json(jwks) }
}),
);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let handle = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
// Give server time to start
tokio::time::sleep(Duration::from_millis(50)).await;
(addr, handle)
}
// ============================================================================
// JWT Authentication Tests
// ============================================================================
#[tokio::test]
async fn test_jwt_valid_token_with_admin_role() {
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config = JwtConfig::new("https://test-issuer.example.com", "test-gateway")
.with_jwks_uri(format!(
"http://127.0.0.1:{}/.well-known/jwks.json",
addr.port()
))
.with_role_mapping("admin", Role::Admin);
let config = ControlPlaneAuthConfig {
jwt: Some(jwt_config),
api_keys: vec![],
audit_enabled: false,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
// Create a valid token with admin role
let claims = create_claims("admin@example.com", vec!["admin"]);
let token = create_test_token(&claims);
// Validate the token
let jwt_validator = state.jwt_validator.as_ref().expect("JWT validator not set");
let result = jwt_validator.validate(&token).await;
assert!(
result.is_ok(),
"Token validation failed: {:?}",
result.err()
);
let validated = result.unwrap();
assert_eq!(validated.subject, "admin@example.com");
assert_eq!(validated.role, Role::Admin);
}
#[tokio::test]
async fn test_jwt_valid_token_with_user_role() {
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config = JwtConfig::new("https://test-issuer.example.com", "test-gateway")
.with_jwks_uri(format!(
"http://127.0.0.1:{}/.well-known/jwks.json",
addr.port()
))
.with_role_mapping("admin", Role::Admin);
let config = ControlPlaneAuthConfig {
jwt: Some(jwt_config),
api_keys: vec![],
audit_enabled: false,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
// Create a valid token with user role (not admin)
let claims = create_claims("user@example.com", vec!["user", "viewer"]);
let token = create_test_token(&claims);
let jwt_validator = state.jwt_validator.as_ref().expect("JWT validator not set");
let result = jwt_validator.validate(&token).await;
assert!(result.is_ok());
let validated = result.unwrap();
assert_eq!(validated.subject, "user@example.com");
assert_eq!(validated.role, Role::User); // Not admin
}
#[tokio::test]
async fn test_jwt_expired_token() {
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config = JwtConfig::new("https://test-issuer.example.com", "test-gateway")
.with_jwks_uri(format!(
"http://127.0.0.1:{}/.well-known/jwks.json",
addr.port()
));
let config = ControlPlaneAuthConfig {
jwt: Some(jwt_config),
api_keys: vec![],
audit_enabled: false,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
// Create an expired token
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let claims = TestClaims {
sub: "user@example.com".to_string(),
iss: "https://test-issuer.example.com".to_string(),
aud: "test-gateway".to_string(),
exp: now - 3600, // Expired 1 hour ago
iat: now - 7200,
jti: None,
name: None,
roles: Some(vec!["admin".to_string()]),
};
let token = create_test_token(&claims);
let jwt_validator = state.jwt_validator.as_ref().expect("JWT validator not set");
let result = jwt_validator.validate(&token).await;
assert!(result.is_err(), "Expired token should fail validation");
let err = result.unwrap_err();
assert!(
err.to_string().contains("exp") || err.to_string().contains("ExpiredSignature"),
"Error should mention expiration: {}",
err
);
}
#[tokio::test]
async fn test_jwt_wrong_audience() {
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config =
JwtConfig::new("https://test-issuer.example.com", "correct-audience").with_jwks_uri(
format!("http://127.0.0.1:{}/.well-known/jwks.json", addr.port()),
);
let config = ControlPlaneAuthConfig {
jwt: Some(jwt_config),
api_keys: vec![],
audit_enabled: false,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
// Create a token with wrong audience
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let claims = TestClaims {
sub: "user@example.com".to_string(),
iss: "https://test-issuer.example.com".to_string(),
aud: "wrong-audience".to_string(), // Wrong audience
exp: now + 3600,
iat: now,
jti: None,
name: None,
roles: Some(vec!["admin".to_string()]),
};
let token = create_test_token(&claims);
let jwt_validator = state.jwt_validator.as_ref().expect("JWT validator not set");
let result = jwt_validator.validate(&token).await;
assert!(result.is_err(), "Wrong audience should fail validation");
}
#[tokio::test]
async fn test_jwt_wrong_issuer() {
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config =
JwtConfig::new("https://correct-issuer.example.com", "test-gateway").with_jwks_uri(
format!("http://127.0.0.1:{}/.well-known/jwks.json", addr.port()),
);
let config = ControlPlaneAuthConfig {
jwt: Some(jwt_config),
api_keys: vec![],
audit_enabled: false,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
// Create a token with wrong issuer
let claims = create_claims("user@example.com", vec!["admin"]);
// claims has issuer "https://test-issuer.example.com" which doesn't match
let token = create_test_token(&claims);
let jwt_validator = state.jwt_validator.as_ref().expect("JWT validator not set");
let result = jwt_validator.validate(&token).await;
assert!(result.is_err(), "Wrong issuer should fail validation");
}
// ============================================================================
// API Key Authentication Tests
// ============================================================================
#[tokio::test]
async fn test_api_key_valid_admin() {
let config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![ApiKeyEntry::new(
"admin-key-1",
"Admin Service Account",
"sk-test-admin-key-12345",
Role::Admin,
)],
audit_enabled: false,
};
// Verify we can find the API key
let found = config.find_api_key("sk-test-admin-key-12345");
assert!(found.is_some(), "API key should be found");
let entry = found.unwrap();
assert_eq!(entry.id, "admin-key-1");
assert_eq!(entry.role, Role::Admin);
}
#[tokio::test]
async fn test_api_key_valid_user() {
let config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![ApiKeyEntry::new(
"user-key-1",
"User Service Account",
"sk-test-user-key-12345",
Role::User,
)],
audit_enabled: false,
};
let found = config.find_api_key("sk-test-user-key-12345");
assert!(found.is_some());
let entry = found.unwrap();
assert_eq!(entry.role, Role::User);
}
#[tokio::test]
async fn test_api_key_invalid() {
let config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![ApiKeyEntry::new(
"admin-key-1",
"Admin Service Account",
"sk-correct-key",
Role::Admin,
)],
audit_enabled: false,
};
// Try with wrong key
let found = config.find_api_key("sk-wrong-key");
assert!(found.is_none(), "Wrong API key should not be found");
}
#[tokio::test]
async fn test_api_key_timing_attack_resistance() {
// Verify that key comparison uses constant-time comparison
let config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![ApiKeyEntry::new(
"key-1",
"Test Key",
"sk-abcdefghijklmnop",
Role::Admin,
)],
audit_enabled: false,
};
// These should all take roughly the same time due to constant-time comparison
// (We can't easily test timing in unit tests, but we verify the function works)
assert!(config.find_api_key("sk-abcdefghijklmnop").is_some());
assert!(config.find_api_key("sk-abcdefghijklmnox").is_none()); // One char different
assert!(config.find_api_key("sk-xxxxxxxxxxxxxxxx").is_none()); // All different
assert!(config.find_api_key("sk-a").is_none()); // Much shorter
}
// ============================================================================
// Combined Auth Tests (JWT + API Key fallback)
// ============================================================================
#[tokio::test]
async fn test_combined_auth_jwt_and_api_keys() {
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config = JwtConfig::new("https://test-issuer.example.com", "test-gateway")
.with_jwks_uri(format!(
"http://127.0.0.1:{}/.well-known/jwks.json",
addr.port()
))
.with_role_mapping("admin", Role::Admin);
let config = ControlPlaneAuthConfig {
jwt: Some(jwt_config),
api_keys: vec![ApiKeyEntry::new(
"backup-key",
"Backup Admin Key",
"sk-backup-admin-key",
Role::Admin,
)],
audit_enabled: true,
};
let state = ControlPlaneAuthState::from_config(config.clone())
.await
.expect("Failed to create auth state");
// Both JWT and API key should work
assert!(state.jwt_validator.is_some());
assert!(config.find_api_key("sk-backup-admin-key").is_some());
// Verify JWT works
let claims = create_claims("jwt-user@example.com", vec!["admin"]);
let token = create_test_token(&claims);
let result = state.jwt_validator.as_ref().unwrap().validate(&token).await;
assert!(result.is_ok());
}
// ============================================================================
// Role-Based Access Control Tests
// ============================================================================
#[tokio::test]
async fn test_role_admin_has_access() {
assert!(Role::Admin.is_admin());
}
#[tokio::test]
async fn test_role_user_no_admin_access() {
assert!(!Role::User.is_admin());
}
#[tokio::test]
async fn test_role_parsing() {
assert_eq!("admin".parse::<Role>().unwrap(), Role::Admin);
assert_eq!("Admin".parse::<Role>().unwrap(), Role::Admin);
assert_eq!("ADMIN".parse::<Role>().unwrap(), Role::Admin);
assert_eq!("user".parse::<Role>().unwrap(), Role::User);
assert_eq!("User".parse::<Role>().unwrap(), Role::User);
assert_eq!("USER".parse::<Role>().unwrap(), Role::User);
}
// ============================================================================
// try_init Helper Tests
// ============================================================================
#[tokio::test]
async fn test_try_init_with_no_config() {
let result = ControlPlaneAuthState::try_init(None).await;
assert!(result.is_none());
}
#[tokio::test]
async fn test_try_init_with_disabled_config() {
let config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![], // No auth configured = disabled
audit_enabled: false,
};
let result = ControlPlaneAuthState::try_init(Some(&config)).await;
assert!(result.is_none());
}
#[tokio::test]
async fn test_try_init_with_api_keys_only() {
let config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![ApiKeyEntry::new("key-1", "Test", "sk-test", Role::Admin)],
audit_enabled: false,
};
let result = ControlPlaneAuthState::try_init(Some(&config)).await;
assert!(result.is_some());
}
// ============================================================================
// Audit Logging Tests
// ============================================================================
#[tokio::test]
async fn test_audit_logging_enabled() {
let config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![ApiKeyEntry::new("key-1", "Test", "sk-test", Role::Admin)],
audit_enabled: true,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
assert!(state.audit_logger.is_enabled());
}
#[tokio::test]
async fn test_audit_logging_disabled() {
let config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![ApiKeyEntry::new("key-1", "Test", "sk-test", Role::Admin)],
audit_enabled: false,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
assert!(!state.audit_logger.is_enabled());
}
// ============================================================================
// JTI Replay Protection Tests
// ============================================================================
#[tokio::test]
async fn test_jwt_jti_replay_protection() {
use smg::auth::JwtValidator;
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config = JwtConfig::new("https://test-issuer.example.com", "test-gateway")
.with_jwks_uri(format!(
"http://127.0.0.1:{}/.well-known/jwks.json",
addr.port()
))
.with_role_mapping("admin", Role::Admin);
// Create validator with JTI replay protection enabled
let validator = JwtValidator::from_config_with_options(jwt_config, true)
.await
.expect("Failed to create JWT validator");
// Create a token with a specific JTI
let claims = create_claims("user@example.com", vec!["admin"]);
let token = create_test_token(&claims);
// First validation should succeed
let result1 = validator.validate(&token).await;
assert!(result1.is_ok(), "First validation should succeed");
// Second validation with same token should fail (replay)
let result2 = validator.validate(&token).await;
assert!(
result2.is_err(),
"Second validation should fail (replay detected)"
);
let err = result2.unwrap_err();
assert!(
err.to_string().contains("replay") || err.to_string().contains("already been used"),
"Error should mention replay: {}",
err
);
}
#[tokio::test]
async fn test_jwt_different_tokens_no_replay() {
use smg::auth::JwtValidator;
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config = JwtConfig::new("https://test-issuer.example.com", "test-gateway")
.with_jwks_uri(format!(
"http://127.0.0.1:{}/.well-known/jwks.json",
addr.port()
))
.with_role_mapping("admin", Role::Admin);
// Create validator with JTI replay protection enabled
let validator = JwtValidator::from_config_with_options(jwt_config, true)
.await
.expect("Failed to create JWT validator");
// Create two different tokens (different JTIs)
let claims1 = create_claims("user1@example.com", vec!["admin"]);
let claims2 = create_claims("user2@example.com", vec!["admin"]);
let token1 = create_test_token(&claims1);
let token2 = create_test_token(&claims2);
// Both should succeed since they have different JTIs
let result1 = validator.validate(&token1).await;
assert!(result1.is_ok(), "First token validation should succeed");
let result2 = validator.validate(&token2).await;
assert!(
result2.is_ok(),
"Second token validation should succeed (different JTI)"
);
}
// ============================================================================
// Malformed Token Tests
// ============================================================================
#[tokio::test]
async fn test_jwt_malformed_token() {
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config = JwtConfig::new("https://test-issuer.example.com", "test-gateway")
.with_jwks_uri(format!(
"http://127.0.0.1:{}/.well-known/jwks.json",
addr.port()
));
let config = ControlPlaneAuthConfig {
jwt: Some(jwt_config),
api_keys: vec![],
audit_enabled: false,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
let jwt_validator = state.jwt_validator.as_ref().expect("JWT validator not set");
// Test various malformed tokens
let malformed_tokens = [
"not-a-jwt",
"only.two.parts.is.not.enough",
"",
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9", // Only header
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.", // Header with dot
".....",
];
for token in malformed_tokens {
let result = jwt_validator.validate(token).await;
assert!(
result.is_err(),
"Malformed token '{}' should fail validation",
token
);
}
}
#[tokio::test]
async fn test_jwt_missing_kid_in_header() {
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config = JwtConfig::new("https://test-issuer.example.com", "test-gateway")
.with_jwks_uri(format!(
"http://127.0.0.1:{}/.well-known/jwks.json",
addr.port()
));
let config = ControlPlaneAuthConfig {
jwt: Some(jwt_config),
api_keys: vec![],
audit_enabled: false,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
// Create a token WITHOUT kid in header
let claims = create_claims("user@example.com", vec!["admin"]);
let mut header = Header::new(jsonwebtoken::Algorithm::RS256);
header.kid = None; // Explicitly no kid
let key = EncodingKey::from_rsa_pem(TEST_KEYS.private_key_pem.as_bytes())
.expect("Failed to create encoding key");
let token_without_kid = encode(&header, &claims, &key).expect("Failed to encode token");
let jwt_validator = state.jwt_validator.as_ref().expect("JWT validator not set");
let result = jwt_validator.validate(&token_without_kid).await;
assert!(result.is_err(), "Token without kid should fail validation");
let err = result.unwrap_err();
assert!(
err.to_string().contains("kid") || err.to_string().contains("Missing"),
"Error should mention missing kid: {}",
err
);
}
// ============================================================================
// Edge Cases and Security Tests
// ============================================================================
#[tokio::test]
async fn test_jwt_role_extraction_from_groups_claim() {
let (addr, _server) = start_mock_jwks_server().await;
// JWT validator checks alternate claims ("groups", "roles", etc.) as fallback
let jwt_config = JwtConfig::new("https://test-issuer.example.com", "test-gateway")
.with_jwks_uri(format!(
"http://127.0.0.1:{}/.well-known/jwks.json",
addr.port()
))
.with_role_mapping("administrators", Role::Admin);
let config = ControlPlaneAuthConfig {
jwt: Some(jwt_config),
api_keys: vec![],
audit_enabled: false,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
// Create token with 'groups' claim instead of 'roles'
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
#[derive(Serialize)]
struct GroupsClaims {
sub: String,
iss: String,
aud: String,
exp: u64,
iat: u64,
jti: String,
groups: Vec<String>, // Use 'groups' instead of 'roles'
}
let claims = GroupsClaims {
sub: "group-user@example.com".to_string(),
iss: "https://test-issuer.example.com".to_string(),
aud: "test-gateway".to_string(),
exp: now + 3600,
iat: now,
jti: uuid::Uuid::new_v4().to_string(),
groups: vec!["administrators".to_string(), "developers".to_string()],
};
let mut header = Header::new(jsonwebtoken::Algorithm::RS256);
header.kid = Some(TEST_KEY_ID.to_string());
let key = EncodingKey::from_rsa_pem(TEST_KEYS.private_key_pem.as_bytes())
.expect("Failed to create encoding key");
let token = encode(&header, &claims, &key).expect("Failed to encode token");
let jwt_validator = state.jwt_validator.as_ref().expect("JWT validator not set");
let result = jwt_validator.validate(&token).await;
assert!(
result.is_ok(),
"Token with groups claim should validate: {:?}",
result.err()
);
let validated = result.unwrap();
assert_eq!(
validated.role,
Role::Admin,
"Should map 'administrators' to Admin role"
);
}
#[tokio::test]
async fn test_jwt_no_role_defaults_to_user() {
let (addr, _server) = start_mock_jwks_server().await;
let jwt_config = JwtConfig::new("https://test-issuer.example.com", "test-gateway")
.with_jwks_uri(format!(
"http://127.0.0.1:{}/.well-known/jwks.json",
addr.port()
));
// No role mapping configured
let config = ControlPlaneAuthConfig {
jwt: Some(jwt_config),
api_keys: vec![],
audit_enabled: false,
};
let state = ControlPlaneAuthState::from_config(config)
.await
.expect("Failed to create auth state");
// Create token without any role claims
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
#[derive(Serialize)]
struct MinimalClaims {
sub: String,
iss: String,
aud: String,
exp: u64,
iat: u64,
jti: String,
// No roles, groups, or role claims
}
let claims = MinimalClaims {
sub: "minimal-user@example.com".to_string(),
iss: "https://test-issuer.example.com".to_string(),
aud: "test-gateway".to_string(),
exp: now + 3600,
iat: now,
jti: uuid::Uuid::new_v4().to_string(),
};
let mut header = Header::new(jsonwebtoken::Algorithm::RS256);
header.kid = Some(TEST_KEY_ID.to_string());
let key = EncodingKey::from_rsa_pem(TEST_KEYS.private_key_pem.as_bytes())
.expect("Failed to create encoding key");
let token = encode(&header, &claims, &key).expect("Failed to encode token");
let jwt_validator = state.jwt_validator.as_ref().expect("JWT validator not set");
let result = jwt_validator.validate(&token).await;
assert!(result.is_ok(), "Token without role should still validate");
let validated = result.unwrap();
assert_eq!(
validated.role,
Role::User,
"Should default to User role when no role found"
);
}
#[tokio::test]
async fn test_multiple_api_keys() {
let config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![
ApiKeyEntry::new("admin-1", "Admin Key 1", "sk-admin-key-1", Role::Admin),
ApiKeyEntry::new("admin-2", "Admin Key 2", "sk-admin-key-2", Role::Admin),
ApiKeyEntry::new("user-1", "User Key 1", "sk-user-key-1", Role::User),
],
audit_enabled: false,
};
// All keys should be findable
assert!(config.find_api_key("sk-admin-key-1").is_some());
assert!(config.find_api_key("sk-admin-key-2").is_some());
assert!(config.find_api_key("sk-user-key-1").is_some());
// Verify correct roles
assert_eq!(
config.find_api_key("sk-admin-key-1").unwrap().role,
Role::Admin
);
assert_eq!(
config.find_api_key("sk-user-key-1").unwrap().role,
Role::User
);
// Non-existent key should not be found
assert!(config.find_api_key("sk-nonexistent").is_none());
}
#[tokio::test]
async fn test_config_is_enabled_checks() {
// Empty config is not enabled
let empty_config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![],
audit_enabled: false,
};
assert!(!empty_config.is_enabled());
// Config with only API keys is enabled
let api_key_config = ControlPlaneAuthConfig {
jwt: None,
api_keys: vec![ApiKeyEntry::new("key-1", "Test", "sk-test", Role::Admin)],
audit_enabled: false,
};
assert!(api_key_config.is_enabled());
// Config with only JWT is enabled
let jwt_config = ControlPlaneAuthConfig {
jwt: Some(JwtConfig::new("https://issuer.example.com", "audience")),
api_keys: vec![],
audit_enabled: false,
};
assert!(jwt_config.is_enabled());
// Config with both is enabled
let full_config = ControlPlaneAuthConfig {
jwt: Some(JwtConfig::new("https://issuer.example.com", "audience")),
api_keys: vec![ApiKeyEntry::new("key-1", "Test", "sk-test", Role::Admin)],
audit_enabled: true,
};
assert!(full_config.is_enabled());
}

View File

@@ -0,0 +1,243 @@
//! Authentication and authorization integration tests
//!
//! Tests for API key enforcement and access control.
use axum::{
body::Body,
extract::Request,
http::{header::CONTENT_TYPE, StatusCode},
};
use serde_json::json;
use tower::ServiceExt;
use crate::common::{AppTestContext, TestRouterConfig, TestWorkerConfig};
const AUTH_HEADER: &str = "Authorization";
#[cfg(test)]
mod auth_tests {
use super::*;
/// Test request without API key when auth is not required
#[tokio::test]
async fn test_no_auth_required() {
let config = TestRouterConfig::round_robin(4300);
let ctx =
AppTestContext::new_with_config(config, vec![TestWorkerConfig::healthy(20300)]).await;
let app = ctx.create_app().await;
// Request without auth header should succeed when no auth required
let payload = json!({
"text": "Test without auth",
"stream": false
});
let req = Request::builder()
.method("POST")
.uri("/generate")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_string(&payload).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(
resp.status(),
StatusCode::OK,
"Request without auth should succeed when no auth required"
);
ctx.shutdown().await;
}
/// Test request with valid API key format
#[tokio::test]
async fn test_with_api_key_header() {
let config = TestRouterConfig::round_robin(4301);
let ctx =
AppTestContext::new_with_config(config, vec![TestWorkerConfig::healthy(20301)]).await;
let app = ctx.create_app().await;
// Request with Bearer token header
let payload = json!({
"text": "Test with auth header",
"stream": false
});
let req = Request::builder()
.method("POST")
.uri("/generate")
.header(CONTENT_TYPE, "application/json")
.header(AUTH_HEADER, "Bearer test-api-key-12345")
.body(Body::from(serde_json::to_string(&payload).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
// Without auth enforcement, request should succeed
assert_eq!(
resp.status(),
StatusCode::OK,
"Request with auth header should succeed"
);
ctx.shutdown().await;
}
/// Test health endpoint doesn't require authentication
#[tokio::test]
async fn test_health_endpoint_no_auth() {
let config = TestRouterConfig::round_robin(4302);
let ctx =
AppTestContext::new_with_config(config, vec![TestWorkerConfig::healthy(20302)]).await;
let app = ctx.create_app().await;
// Health endpoint should be accessible without auth
let req = Request::builder()
.method("GET")
.uri("/health")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(
resp.status(),
StatusCode::OK,
"Health endpoint should not require auth"
);
ctx.shutdown().await;
}
/// Test OpenAI-compatible API key header (X-API-Key)
#[tokio::test]
async fn test_openai_api_key_header() {
let config = TestRouterConfig::round_robin(4303);
let ctx =
AppTestContext::new_with_config(config, vec![TestWorkerConfig::healthy(20303)]).await;
let app = ctx.create_app().await;
// Request with X-API-Key header (OpenAI style)
let payload = json!({
"text": "Test with X-API-Key",
"stream": false
});
let req = Request::builder()
.method("POST")
.uri("/generate")
.header(CONTENT_TYPE, "application/json")
.header("X-API-Key", "sk-test-key-12345")
.body(Body::from(serde_json::to_string(&payload).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(
resp.status(),
StatusCode::OK,
"Request with X-API-Key should succeed"
);
ctx.shutdown().await;
}
/// Test multiple concurrent authenticated requests
#[tokio::test]
async fn test_concurrent_authenticated_requests() {
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
let config = TestRouterConfig::round_robin(4304);
let ctx =
AppTestContext::new_with_config(config, vec![TestWorkerConfig::healthy(20304)]).await;
let app = ctx.create_app().await;
let success_count = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for i in 0..20 {
let app_clone = app.clone();
let success_clone = Arc::clone(&success_count);
let handle = tokio::spawn(async move {
let payload = json!({
"text": format!("Concurrent auth test {}", i),
"stream": false
});
let req = Request::builder()
.method("POST")
.uri("/generate")
.header(CONTENT_TYPE, "application/json")
.header(AUTH_HEADER, format!("Bearer test-key-{}", i))
.body(Body::from(serde_json::to_string(&payload).unwrap()))
.unwrap();
let resp = app_clone.oneshot(req).await.unwrap();
if resp.status() == StatusCode::OK {
success_clone.fetch_add(1, Ordering::SeqCst);
}
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
assert_eq!(
success_count.load(Ordering::SeqCst),
20,
"All concurrent authenticated requests should succeed"
);
ctx.shutdown().await;
}
}
#[cfg(test)]
mod mtls_tests {
use super::*;
/// Test that TLS configuration options exist
/// Note: Actual mTLS testing would require certificate setup
#[tokio::test]
async fn test_tls_config_available() {
// This test verifies the config builder accepts TLS-related options
// Actual mTLS testing requires certificate infrastructure
let config = TestRouterConfig::round_robin(4305);
let ctx =
AppTestContext::new_with_config(config, vec![TestWorkerConfig::healthy(20305)]).await;
let app = ctx.create_app().await;
// Basic request should work (no TLS in test mode)
let payload = json!({
"text": "TLS config test",
"stream": false
});
let req = Request::builder()
.method("POST")
.uri("/generate")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_string(&payload).unwrap()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
ctx.shutdown().await;
}
}

View File

@@ -0,0 +1,5 @@
//! Security integration tests
pub mod auth_integration_test;
pub mod auth_test;
pub mod mtls_test;

View File

@@ -0,0 +1,506 @@
//! mTLS (Mutual TLS) integration tests
//!
//! Tests for TLS and mTLS communication between router and workers.
//! Covers:
//! - Successful mTLS communication with client certificates
//! - TLS failure without client certificate when required
//! - TLS-only mode (server authentication only)
//! - TLS failure without CA certificate
use std::{io::BufReader, time::Duration};
use rustls::{ClientConfig, RootCertStore};
use rustls_pemfile::{certs, pkcs8_private_keys};
use serde_json::json;
use crate::common::{
test_certs::TestCertificates,
tls_mock_worker::{TlsMockWorker, TlsMockWorkerConfig},
};
// Ensure crypto provider is installed
fn ensure_crypto_provider() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
let _ = rustls::crypto::ring::default_provider().install_default();
});
}
/// Helper to create a TLS-enabled reqwest client with client certificates
fn create_mtls_client(
test_certs: &TestCertificates,
) -> Result<reqwest::Client, Box<dyn std::error::Error>> {
ensure_crypto_provider();
// Read CA certificate
let ca_file = std::fs::File::open(&test_certs.ca_cert_path)?;
let mut ca_reader = BufReader::new(ca_file);
let ca_certs: Vec<_> = certs(&mut ca_reader).filter_map(|r| r.ok()).collect();
let mut root_store = RootCertStore::empty();
for cert in ca_certs {
root_store.add(cert)?;
}
// Read client certificate
let cert_file = std::fs::File::open(&test_certs.client_cert_path)?;
let mut cert_reader = BufReader::new(cert_file);
let client_certs: Vec<_> = certs(&mut cert_reader).filter_map(|r| r.ok()).collect();
// Read client key
let key_file = std::fs::File::open(&test_certs.client_key_path)?;
let mut key_reader = BufReader::new(key_file);
let client_key = pkcs8_private_keys(&mut key_reader)
.next()
.ok_or("No private key found")??;
// Build client config with client certificate
let client_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(client_certs, client_key.into())
.map_err(|e| format!("Failed to build client config: {}", e))?;
let client = reqwest::Client::builder()
.use_preconfigured_tls(client_config)
.timeout(Duration::from_secs(10))
.build()?;
Ok(client)
}
/// Helper to create a TLS client without client certificates (for server-auth-only)
fn create_tls_client(
test_certs: &TestCertificates,
) -> Result<reqwest::Client, Box<dyn std::error::Error>> {
ensure_crypto_provider();
// Read CA certificate
let ca_file = std::fs::File::open(&test_certs.ca_cert_path)?;
let mut ca_reader = BufReader::new(ca_file);
let ca_certs: Vec<_> = certs(&mut ca_reader).filter_map(|r| r.ok()).collect();
let mut root_store = RootCertStore::empty();
for cert in ca_certs {
root_store.add(cert)?;
}
// Build client config without client certificate
let client_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
let client = reqwest::Client::builder()
.use_preconfigured_tls(client_config)
.timeout(Duration::from_secs(10))
.build()?;
Ok(client)
}
/// Helper to create a client without CA certificate (for failure test)
fn create_client_without_ca() -> Result<reqwest::Client, Box<dyn std::error::Error>> {
ensure_crypto_provider();
// Build client with empty root store (will fail to verify server)
let root_store = RootCertStore::empty();
let client_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
let client = reqwest::Client::builder()
.use_preconfigured_tls(client_config)
.timeout(Duration::from_secs(10))
.build()?;
Ok(client)
}
#[cfg(test)]
mod mtls_tests {
use super::*;
/// Test successful mTLS communication between client and TLS worker
///
/// This test verifies that:
/// 1. TLS mock worker starts with mTLS configuration
/// 2. Client with proper certificates can connect and communicate
/// 3. Requests succeed with proper authentication
#[tokio::test]
async fn test_mtls_successful_communication() {
// Generate test certificates
let certs = TestCertificates::generate().expect("Failed to generate test certificates");
// Start mTLS-enabled mock worker
let mut worker = TlsMockWorker::new(TlsMockWorkerConfig {
port: 0, // Auto-assign port
require_client_cert: true,
response_delay_ms: 0,
fail_rate: 0.0,
});
let worker_url = worker
.start(
&certs.server_cert_path,
&certs.server_key_path,
Some(&certs.ca_cert_path),
)
.await
.expect("Failed to start mTLS worker");
// Create client with full mTLS credentials
let client = create_mtls_client(&certs).expect("Failed to create mTLS client");
// Test health endpoint
let health_resp = client
.get(format!("{}/health", worker_url))
.send()
.await
.expect("Health request failed");
assert!(
health_resp.status().is_success(),
"Health check should succeed with mTLS: {}",
health_resp.status()
);
let health_json: serde_json::Value = health_resp.json().await.unwrap();
assert_eq!(health_json["status"], "healthy");
assert_eq!(health_json["tls_enabled"], true);
// Test generate endpoint
let payload = json!({
"text": "Test mTLS request",
"stream": false
});
let gen_resp = client
.post(format!("{}/generate", worker_url))
.json(&payload)
.send()
.await
.expect("Generate request failed");
assert!(
gen_resp.status().is_success(),
"Generate should succeed with mTLS: {}",
gen_resp.status()
);
let gen_json: serde_json::Value = gen_resp.json().await.unwrap();
assert!(gen_json["text"].as_str().is_some());
assert_eq!(gen_json["meta_info"]["tls_verified"], true);
// Cleanup
worker.stop().await;
}
/// Test that mTLS worker rejects connections without client certificate
///
/// This test verifies that:
/// 1. mTLS worker requires client certificate
/// 2. Connection without client cert fails
#[tokio::test]
async fn test_mtls_failure_without_client_cert() {
// Generate test certificates
let certs = TestCertificates::generate().expect("Failed to generate test certificates");
// Start mTLS-enabled mock worker (requires client cert)
let mut worker = TlsMockWorker::new(TlsMockWorkerConfig {
port: 0,
require_client_cert: true,
response_delay_ms: 0,
fail_rate: 0.0,
});
let worker_url = worker
.start(
&certs.server_cert_path,
&certs.server_key_path,
Some(&certs.ca_cert_path),
)
.await
.expect("Failed to start mTLS worker");
// Create client WITHOUT client certificate (only has CA for server verification)
let client = create_tls_client(&certs).expect("Failed to create TLS-only client");
// Attempt to connect - should fail because no client cert provided
let result = client.get(format!("{}/health", worker_url)).send().await;
// Connection should fail or be rejected
assert!(
result.is_err(),
"Connection should fail without client certificate"
);
// Cleanup
worker.stop().await;
}
/// Test TLS-only mode (server authentication only, no client cert required)
///
/// This test verifies that:
/// 1. TLS worker can operate without requiring client certificates
/// 2. Client can connect with just CA certificate for server verification
#[tokio::test]
async fn test_tls_server_auth_only() {
// Generate test certificates
let certs = TestCertificates::generate().expect("Failed to generate test certificates");
// Start TLS-only mock worker (does NOT require client cert)
let mut worker = TlsMockWorker::new(TlsMockWorkerConfig {
port: 0,
require_client_cert: false, // TLS-only mode
response_delay_ms: 0,
fail_rate: 0.0,
});
let worker_url = worker
.start(
&certs.server_cert_path,
&certs.server_key_path,
None, // No CA needed for client verification
)
.await
.expect("Failed to start TLS worker");
// Create client with just CA cert for server verification (no client cert)
let client = create_tls_client(&certs).expect("Failed to create TLS client");
// Test health endpoint - should succeed without client cert
let health_resp = client
.get(format!("{}/health", worker_url))
.send()
.await
.expect("Health request failed");
assert!(
health_resp.status().is_success(),
"Health check should succeed in TLS-only mode: {}",
health_resp.status()
);
let health_json: serde_json::Value = health_resp.json().await.unwrap();
assert_eq!(health_json["status"], "healthy");
// Test chat completions endpoint
let payload = json!({
"model": "mock-tls-model",
"messages": [{"role": "user", "content": "Hello TLS"}]
});
let chat_resp = client
.post(format!("{}/v1/chat/completions", worker_url))
.json(&payload)
.send()
.await
.expect("Chat request failed");
assert!(
chat_resp.status().is_success(),
"Chat should succeed in TLS-only mode: {}",
chat_resp.status()
);
// Cleanup
worker.stop().await;
}
/// Test TLS failure when client doesn't have CA certificate
///
/// This test verifies that:
/// 1. Client cannot verify server without proper CA certificate
/// 2. Connection fails due to certificate verification
#[tokio::test]
async fn test_tls_failure_without_ca_cert() {
// Generate test certificates
let certs = TestCertificates::generate().expect("Failed to generate test certificates");
// Start TLS mock worker
let mut worker = TlsMockWorker::new(TlsMockWorkerConfig {
port: 0,
require_client_cert: false,
response_delay_ms: 0,
fail_rate: 0.0,
});
let worker_url = worker
.start(&certs.server_cert_path, &certs.server_key_path, None)
.await
.expect("Failed to start TLS worker");
// Create client WITHOUT CA certificate
let client = create_client_without_ca().expect("Failed to create client without CA");
// Attempt to connect - should fail because cannot verify server cert
let result = client.get(format!("{}/health", worker_url)).send().await;
// Connection should fail due to certificate verification
assert!(
result.is_err(),
"Connection should fail without CA certificate for server verification"
);
// Cleanup
worker.stop().await;
}
/// Test multiple concurrent mTLS requests
///
/// This test verifies that mTLS connections work correctly under concurrent load
#[tokio::test]
async fn test_mtls_concurrent_requests() {
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
// Generate test certificates
let certs = TestCertificates::generate().expect("Failed to generate test certificates");
// Start mTLS-enabled mock worker
let mut worker = TlsMockWorker::new(TlsMockWorkerConfig {
port: 0,
require_client_cert: true,
response_delay_ms: 10, // Small delay to simulate work
fail_rate: 0.0,
});
let worker_url = worker
.start(
&certs.server_cert_path,
&certs.server_key_path,
Some(&certs.ca_cert_path),
)
.await
.expect("Failed to start mTLS worker");
// Create mTLS client
let client = Arc::new(create_mtls_client(&certs).expect("Failed to create mTLS client"));
let success_count = Arc::new(AtomicUsize::new(0));
let worker_url = Arc::new(worker_url);
let mut handles = Vec::new();
// Spawn concurrent requests
for i in 0..10 {
let client_clone = Arc::clone(&client);
let success_clone = Arc::clone(&success_count);
let url_clone = Arc::clone(&worker_url);
let handle = tokio::spawn(async move {
let payload = json!({
"text": format!("Concurrent mTLS request {}", i),
"stream": false
});
let resp = client_clone
.post(format!("{}/generate", url_clone))
.json(&payload)
.send()
.await;
if let Ok(response) = resp {
if response.status().is_success() {
success_clone.fetch_add(1, Ordering::SeqCst);
}
}
});
handles.push(handle);
}
// Wait for all requests to complete
for handle in handles {
handle.await.unwrap();
}
// All requests should succeed
assert_eq!(
success_count.load(Ordering::SeqCst),
10,
"All concurrent mTLS requests should succeed"
);
// Cleanup
worker.stop().await;
}
}
#[cfg(test)]
mod certificate_tests {
use super::*;
/// Test that certificate generation works correctly
#[test]
fn test_certificate_generation() {
let certs = TestCertificates::generate().expect("Failed to generate certificates");
// Verify all files exist and are not empty
assert!(certs.ca_cert_path.exists());
assert!(certs.ca_key_path.exists());
assert!(certs.server_cert_path.exists());
assert!(certs.server_key_path.exists());
assert!(certs.client_cert_path.exists());
assert!(certs.client_key_path.exists());
// Verify PEM format
let ca_cert_content = std::fs::read_to_string(&certs.ca_cert_path).unwrap();
assert!(ca_cert_content.contains("-----BEGIN CERTIFICATE-----"));
assert!(ca_cert_content.contains("-----END CERTIFICATE-----"));
let server_key_content = std::fs::read_to_string(&certs.server_key_path).unwrap();
assert!(server_key_content.contains("-----BEGIN PRIVATE KEY-----"));
assert!(server_key_content.contains("-----END PRIVATE KEY-----"));
}
/// Test that generated certificates can be parsed by rustls
#[test]
fn test_certificate_parsing() {
ensure_crypto_provider();
let test_certs = TestCertificates::generate().expect("Failed to generate certificates");
// Verify CA certificate can be parsed by rustls
let ca_file = std::fs::File::open(&test_certs.ca_cert_path).unwrap();
let mut ca_reader = BufReader::new(ca_file);
let ca_certs: Vec<_> = certs(&mut ca_reader).filter_map(|r| r.ok()).collect();
assert!(!ca_certs.is_empty(), "CA certificate should be parseable");
// Verify client certificate can be parsed
let cert_file = std::fs::File::open(&test_certs.client_cert_path).unwrap();
let mut cert_reader = BufReader::new(cert_file);
let client_certs: Vec<_> = certs(&mut cert_reader).filter_map(|r| r.ok()).collect();
assert!(
!client_certs.is_empty(),
"Client certificate should be parseable"
);
// Verify client key can be parsed
let key_file = std::fs::File::open(&test_certs.client_key_path).unwrap();
let mut key_reader = BufReader::new(key_file);
let keys: Vec<_> = pkcs8_private_keys(&mut key_reader)
.filter_map(|r| r.ok())
.collect();
assert!(!keys.is_empty(), "Client key should be parseable");
// Verify we can build a complete client config
let mut root_store = RootCertStore::empty();
for cert in ca_certs {
root_store
.add(cert)
.expect("Should be able to add CA cert to store");
}
let key = keys.into_iter().next().unwrap();
let config_result = ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(client_certs, key.into());
assert!(
config_result.is_ok(),
"Should be able to create client config with certs"
);
}
}