Compare commits

..

No commits in common. "f411df0272a20e7944a089748383d1654e406873" and "2a9e4f59faf77835ef1737c1244bc6c238d35931" have entirely different histories.

20 changed files with 210 additions and 262 deletions

1
.gitignore vendored
View File

@ -10,7 +10,6 @@ CLAUDE.md
.cache .cache
.coder .coder
.wrks .wrks
rollback_instruction*
ROOT ROOT
OLD OLD
# Generated by Cargo # Generated by Cargo

View File

@ -41,30 +41,6 @@ repos:
# pass_filenames: false # pass_filenames: false
# stages: [pre-push] # stages: [pre-push]
# ============================================================================
# Nushell Hooks (ACTIVE)
# ============================================================================
- repo: local
hooks:
- id: nushell-check
name: Nushell IDE Check (nu --ide-check)
entry: bash -c 'for f in $(git diff --cached --name-only | grep "\.nu$"); do nu --ide-check "$f" || exit 1; done'
language: system
files: \.nu$
pass_filenames: false
stages: [pre-commit]
# ============================================================================
# Bash Hooks (ACTIVE)
# ============================================================================
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.1.1
hooks:
- id: shellcheck
name: Bash linting (shellcheck)
args: ['--severity=warning']
stages: [pre-commit]
# ============================================================================ # ============================================================================
# Nickel Hooks (ACTIVE) # Nickel Hooks (ACTIVE)
# ============================================================================ # ============================================================================

View File

@ -505,20 +505,10 @@ impl ToolRegistry {
let mut settings_tools = self.settings_tools.lock().await; let mut settings_tools = self.settings_tools.lock().await;
let settings = settings_tools settings_tools
.get_settings(query) .get_settings(query)
.await .await
.map_err(|e| format!("Failed to get settings: {}", e))?; .map_err(|e| format!("Failed to get settings: {}", e))
Ok(json!({
"status": "success",
"tool": "installer_get_settings",
"platforms": settings.get("platforms"),
"modes": settings.get("modes"),
"default_domain": settings.get("default_domain"),
"auto_generate_secrets": settings.get("auto_generate_secrets"),
"available_services": settings.get("available_services")
}))
} }
async fn installer_complete_config(&self, args: &Value) -> Result<Value, String> { async fn installer_complete_config(&self, args: &Value) -> Result<Value, String> {
@ -556,22 +546,9 @@ impl ToolRegistry {
let settings_tools = self.settings_tools.lock().await; let settings_tools = self.settings_tools.lock().await;
let defaults = settings_tools settings_tools
.get_mode_defaults(mode) .get_mode_defaults(mode)
.map_err(|e| format!("Failed to get defaults: {}", e))?; .map_err(|e| format!("Failed to get defaults: {}", e))
Ok(json!({
"status": "success",
"tool": "installer_get_defaults",
"mode": defaults.get("mode"),
"description": defaults.get("description"),
"service_count": defaults.get("service_count"),
"min_cpu_cores": defaults.get("min_cpu_cores"),
"min_memory_gb": defaults.get("min_memory_gb"),
"recommended_services": defaults.get("recommended_services"),
"auto_generate_secrets": defaults.get("auto_generate_secrets"),
"domain": defaults.get("domain")
}))
} }
async fn installer_platform_recommendations(&self, _args: &Value) -> Result<Value, String> { async fn installer_platform_recommendations(&self, _args: &Value) -> Result<Value, String> {

View File

@ -100,13 +100,12 @@ async fn test_explicit_tool_call_rag_ask() {
args: json!({"question": "What is Nushell?"}), args: json!({"question": "What is Nushell?"}),
}; };
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed"); let response = service.call_mcp_tool(req).await.unwrap();
assert_eq!(response.result["status"], "success"); assert_eq!(response.result["status"], "success");
assert_eq!(response.result["tool"], "rag_ask_question"); assert_eq!(response.result["tool"], "rag_ask_question");
} }
#[tokio::test] #[tokio::test]
#[ignore]
async fn test_explicit_tool_call_guidance_status() { async fn test_explicit_tool_call_guidance_status() {
let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap();
let service = AiService::new(addr); let service = AiService::new(addr);
@ -116,7 +115,7 @@ async fn test_explicit_tool_call_guidance_status() {
args: json!({}), args: json!({}),
}; };
let response = service.call_mcp_tool(req).await.expect("guidance_check_system_status failed"); let response = service.call_mcp_tool(req).await.unwrap();
assert_eq!(response.result["status"], "healthy"); assert_eq!(response.result["status"], "healthy");
assert_eq!(response.result["tool"], "guidance_check_system_status"); assert_eq!(response.result["tool"], "guidance_check_system_status");
} }
@ -131,7 +130,7 @@ async fn test_explicit_tool_call_settings() {
args: json!({}), args: json!({}),
}; };
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed"); let response = service.call_mcp_tool(req).await.unwrap();
assert_eq!(response.result["status"], "success"); assert_eq!(response.result["status"], "success");
// Verify real SettingsTools data is returned (not empty placeholder) // Verify real SettingsTools data is returned (not empty placeholder)
assert!( assert!(
@ -152,7 +151,7 @@ async fn test_settings_tools_platform_recommendations() {
args: json!({}), args: json!({}),
}; };
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed"); let response = service.call_mcp_tool(req).await.unwrap();
assert_eq!(response.result["status"], "success"); assert_eq!(response.result["status"], "success");
// Should have real recommendations array from SettingsTools platform detection // Should have real recommendations array from SettingsTools platform detection
assert!(response.result.get("recommendations").is_some()); assert!(response.result.get("recommendations").is_some());
@ -168,7 +167,7 @@ async fn test_settings_tools_mode_defaults() {
args: json!({"mode": "solo"}), args: json!({"mode": "solo"}),
}; };
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed"); let response = service.call_mcp_tool(req).await.unwrap();
assert_eq!(response.result["status"], "success"); assert_eq!(response.result["status"], "success");
// Verify real mode defaults (resource requirements) // Verify real mode defaults (resource requirements)
assert!(response.result.get("min_cpu_cores").is_some()); assert!(response.result.get("min_cpu_cores").is_some());
@ -185,7 +184,7 @@ async fn test_explicit_tool_call_iac() {
args: json!({"path": "/tmp/infra"}), args: json!({"path": "/tmp/infra"}),
}; };
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed"); let response = service.call_mcp_tool(req).await.unwrap();
assert_eq!(response.result["status"], "success"); assert_eq!(response.result["status"], "success");
// Verify real technology detection (returns technologies array) // Verify real technology detection (returns technologies array)
assert!(response.result.get("technologies").is_some()); assert!(response.result.get("technologies").is_some());
@ -202,7 +201,7 @@ async fn test_iac_detect_technologies_real() {
args: json!({"path": "../../provisioning"}), args: json!({"path": "../../provisioning"}),
}; };
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed"); let response = service.call_mcp_tool(req).await.unwrap();
assert_eq!(response.result["status"], "success"); assert_eq!(response.result["status"], "success");
// Should detect technologies as an array // Should detect technologies as an array
@ -221,7 +220,7 @@ async fn test_iac_analyze_completeness() {
args: json!({"path": "/tmp/test-infra"}), args: json!({"path": "/tmp/test-infra"}),
}; };
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed"); let response = service.call_mcp_tool(req).await.unwrap();
assert_eq!(response.result["status"], "success"); assert_eq!(response.result["status"], "success");
// Verify real analysis data // Verify real analysis data
assert!(response.result.get("complete").is_some()); assert!(response.result.get("complete").is_some());
@ -365,7 +364,7 @@ async fn test_tool_execution_with_required_args() {
args: json!({"query": "kubernetes"}), args: json!({"query": "kubernetes"}),
}; };
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed"); let response = service.call_mcp_tool(req).await.unwrap();
assert_eq!(response.result["status"], "success"); assert_eq!(response.result["status"], "success");
} }

View File

@ -86,8 +86,7 @@ impl TokenClaims {
pub fn needs_rotation(&self) -> bool { pub fn needs_rotation(&self) -> bool {
let now = Utc::now().timestamp(); let now = Utc::now().timestamp();
let time_until_expiry = self.exp - now; let time_until_expiry = self.exp - now;
// Needs rotation if within 5 minutes AND not yet expired time_until_expiry <= 300 // 5 minutes
time_until_expiry <= 300 && time_until_expiry > 0
} }
/// Get remaining validity duration /// Get remaining validity duration
@ -172,7 +171,7 @@ impl JwtService {
/// &private_key, /// &private_key,
/// &public_key, /// &public_key,
/// "control-center", /// "control-center",
/// vec!["orchestrator".to_string(), "cli".to_string()] /// vec!["orchestrator", "cli"]
/// ).unwrap(); /// ).unwrap();
/// ``` /// ```
pub fn new( pub fn new(
@ -470,16 +469,48 @@ mod tests {
use super::*; use super::*;
fn generate_test_keys() -> (Vec<u8>, Vec<u8>) { fn generate_test_keys() -> (Vec<u8>, Vec<u8>) {
// Generate fresh RSA keys for testing // Pre-generated RSA keys to avoid runtime key generation (avoids rand_core
use crate::services::jwt::generate_rsa_key_pair; // conflict)
let private_pem = b"-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7F43HxrVfJJ+k
DQMEjENGqJLnBn6MvJnCu93A4ZNKEEpPGX1Y6V+qiqLH5B7wNMIJ2QVnLjYjKZu5
K5LkVCH8N7N1s9QXiLI7TzQVEH2TQx3MzQFRmVzGlyVFxF0qMHf/mNlZvE0Hb9oI
YdYnBW5k4TRmzrN4THEbWqLcGVVfMVFQqG1j/V3G5dTpV1sN8a9xqJG0dKNmVP2M
H5JG3kQ8CxFqp7pRsGvqH9IVjB6KXj8L3VPQYJxIBKHGZMlsP9SVqH3vwqLLKcBj
G2UvhkVYJ6ZLvYIj7LvZKrCBrP3h5Q7hYpXZqDGYd5dDwWFJ3xkVMkJMGR3D3VrK
R1jvJTEhAgMBAAECggEAXV2TZjfRbhR0IaqU3bpSZvb+6uL+5OgWRO5VbG0XGFmv
1v+2F5hQEZYKKxZvTlF4IxN1J0YQRKt4BG6PZqN9+Uj8z7DVEDqiWLIZSEhUPzVu
E3IKZaEqvGKvEwBglmfqnE6cEZ2Kb9aOx1a5N8UH5q7xQ2d4YxKfYwRBkDYEZmZp
R4tEb0qfKFgIGmCfK5fNvJqXvH/f1xq9QW1qrGYqGAqh7bkxvXGfkRBXh7Q8sAXW
B2XQ8L9CZV5lsP0F7n8nKvDVKfqZfGXlZJZQvJPF5L8p6g7V7uE+6bZlF4YZLRvW
2aq2xZR6bGCWxBpLb2lG1QU0aJLqVmVjAQYDM9PB0QKBgQDfhKn8F5aITVFJhVVu
BN8F2hLRtFDfSPTN0JM1WZq/EqYdLjnlAY8L7kBBzNaKgOGBRqfO1lVv7DG4vPtX
nrLaM3SU3F0lC8tVwqJTUvZS5/Pl3wqAZRlWbWm4Yp6gWXlGPTaWRpGRBOqB+rWP
b8rX2SnZjvFlQZlZPfvHGWpkFQKBgQDYtvXAGNdj8hMZ6mMUKYv3YbF7F2xUlf1x
H0f8I5QGPiZoqzNGJXCEGvNVNEUgR7LY6AeqWtLqRKJsQ5NYb2XzLhA2BVOV1BV2
Z1Y3s0V5XjFE4q7gWmEcLlWyEiKiRCRQC4T7qB+gUQ8lFBrGqQ4vZqE+7pQyYfCt
1yFQnvqrQQKBgQCcvCvE4wMhF2lYFxM4v8qCDXJ9nLMKjYPFn3C0YrP6Zn2Pm7r3
fJGDVBmN5wVLDXqZnSrmh0iBs7PdBqeHALF7jTj9X8tNe+hpXwQ/GWSF8b3q1b6k
wZLhKpJQGpYH0M3l7B6rrM3Vr1E/DQjXvXdqXrL3vKfHQrEk3c9zVCkfTQKBgGVl
qdvBYvdgWHvPzL3+vqvLPVKDnLjN6hXA2cJGaBNpOlm+bI5cJLLnWhJrjCkK+POo
0oPKvnO5qKQjRDWLVCJH3z8MxPjqV6Yk1hAg/4vqV3oQsJDfU3qiDPAHrBbKBlRm
4KYmEqIhqNYJqXN7nnQFOLYFxOaXKqe6HBh1g4QBAoGBAIRXPwKJhwIUfLJ8DQAO
9z9YH3eIiGkLxF8f7WZvVnNwMEpXl4IqN8N3qGXKJfXJI1YfBKXN7LOqCOVFGkVq
JkVN8qPTwAKU8E2U3dGzQVPP2JmGPPNyZJxQJ3sYtKnLkz2YdEbKDhkG5wPDHKKu
LfEK1YMqL/VvAJdIXxqZqDXf
-----END PRIVATE KEY-----";
let keys = generate_rsa_key_pair() let public_pem = b"-----BEGIN PUBLIC KEY-----
.expect("Failed to generate test RSA keys"); MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuxeNx8a1XySfpA0DBIxD
RqiS5wZ+jLyZwrvdwOGTShBKTxl9WOlfqoqix+Qe8DTCCdkFZy42IymbhSuS5FQh
/DezdLPUF4iyO080FRB9k0MdzM0BUZlcxpclRcRdKjB3/5jZWbxNB2/aCGHWJwVu
ZOE0Zs6zeExxG1qi3BlVXzFRUKhtY/1dxuXU6Vdbl/GvcaiRtHSjZlT9jB+SRt5E
PAsTaqe6UbBr6h/SFYweil4/C91T0GCcSAShxmTJbD/Ulah978KizynAYxtlL4ZF
WCemS72CI+y72SqwgaZ94eUO4WKV2agxmHeXQ8FhSd8ZFTJCTBKDW91Kykdo7yUx
IQIDAQAB
-----END PUBLIC KEY-----";
( (private_pem.to_vec(), public_pem.to_vec())
keys.private_key_pem.into_bytes(),
keys.public_key_pem.into_bytes(),
)
} }
fn create_test_service() -> JwtService { fn create_test_service() -> JwtService {

View File

@ -65,7 +65,7 @@
//! # Usage Example //! # Usage Example
//! //!
//! ```no_run //! ```no_run
//! # use control_center::auth::{JwtService, PasswordService}; //! # use control_center::auth::{JwtService, PasswordService, User};
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> { //! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! // Initialize services //! // Initialize services
//! let private_key = std::fs::read("keys/private.pem")?; //! let private_key = std::fs::read("keys/private.pem")?;
@ -80,12 +80,19 @@
//! //!
//! let password_service = PasswordService::new(); //! let password_service = PasswordService::new();
//! //!
//! // Verify password //! // Create user with hashed password
//! let password_hash = password_service.hash_password("secure_password")?; //! let password_hash = password_service.hash_password("secure_password")?;
//! if password_service.verify_password("secure_password", &password_hash)? { //! let user = User {
//! // Generate token pair for authenticated user //! id: "user123".to_string(),
//! username: "alice".to_string(),
//! password_hash,
//! // ... other fields
//! };
//!
//! // Login: verify password and generate tokens
//! if password_service.verify_password("secure_password", &user.password_hash)? {
//! let tokens = jwt_service.generate_token_pair( //! let tokens = jwt_service.generate_token_pair(
//! "user123", //! &user.id,
//! "workspace1", //! "workspace1",
//! "permissions_hash", //! "permissions_hash",
//! None, //! None,
@ -93,20 +100,20 @@
//! //!
//! println!("Access token: {}", tokens.access_token); //! println!("Access token: {}", tokens.access_token);
//! println!("Expires in: {} seconds", tokens.expires_in); //! println!("Expires in: {} seconds", tokens.expires_in);
//!
//! // Validate token
//! let claims = jwt_service.validate_token(&tokens.access_token)?;
//! println!("User: {}, Workspace: {}", claims.sub, claims.workspace);
//!
//! // Rotate token before expiry
//! if claims.needs_rotation() {
//! let new_tokens = jwt_service.rotate_token(&tokens.refresh_token)?;
//! println!("New access token: {}", new_tokens.access_token);
//! }
//!
//! // Revoke token (logout)
//! jwt_service.revoke_token(&claims.jti, claims.exp)?;
//! } //! }
//!
//! // Validate token
//! let claims = jwt_service.validate_token(&tokens.access_token)?;
//! println!("User: {}, Workspace: {}", claims.sub, claims.workspace);
//!
//! // Rotate token before expiry
//! if claims.needs_rotation() {
//! let new_tokens = jwt_service.rotate_token(&tokens.refresh_token)?;
//! println!("New access token: {}", new_tokens.access_token);
//! }
//!
//! // Revoke token (logout)
//! jwt_service.revoke_token(&claims.jti, claims.exp)?;
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```

View File

@ -157,9 +157,8 @@ mod tests {
#[test] #[test]
fn test_password_strength_fair() { fn test_password_strength_fair() {
let service = PasswordService::new(); let service = PasswordService::new();
// Fair: 8-9 chars with 0-2 complexity types (lowercase, uppercase, digit, special)
assert_eq!( assert_eq!(
service.evaluate_strength("password1"), // 9 chars, 2 types: lowercase + digit service.evaluate_strength("Password1"),
PasswordStrength::Fair PasswordStrength::Fair
); );
} }

View File

@ -23,12 +23,12 @@ use crate::kms::types::{KeyData, KeyType, ProviderCredentials};
/// # Usage /// # Usage
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use control_center::kms::{KmsService, KeyType}; /// use control_center::kms::{KmsService, KmsConfig};
/// use std::sync::Arc; /// use std::sync::Arc;
/// ///
/// async fn example(kms: Arc<dyn KmsService>) -> anyhow::Result<()> { /// async fn example(kms: Arc<dyn KmsService>) -> anyhow::Result<()> {
/// // Use the trait, not the concrete type /// // Use the trait, not the concrete type
/// let key = kms.generate_key(KeyType::Asymmetric).await?; /// let key = kms.generate_key(/* params */).await?;
/// println!("Generated key: {}", key.key_id); /// println!("Generated key: {}", key.key_id);
/// Ok(()) /// Ok(())
/// } /// }

View File

@ -356,25 +356,9 @@ mod tests {
assert!(admin_context.has_permission("system:manage")); assert!(admin_context.has_permission("system:manage"));
} }
fn create_test_jwt_config() -> JwtConfig {
use crate::services::jwt::generate_rsa_key_pair;
let keys = generate_rsa_key_pair().expect("Failed to generate test keys");
JwtConfig {
issuer: "test-issuer".to_string(),
audience: "test-audience".to_string(),
access_token_expiration_hours: 1,
refresh_token_expiration_hours: 24,
private_key_pem: keys.private_key_pem,
public_key_pem: keys.public_key_pem,
}
}
#[tokio::test] #[tokio::test]
async fn test_auth_header_parsing() { async fn test_auth_header_parsing() {
let jwt_service = Arc::new( let jwt_service = Arc::new(JwtService::new(JwtConfig::default()).unwrap());
JwtService::new(create_test_jwt_config())
.expect("Failed to create JWT service for test")
);
let user_id = Uuid::new_v4(); let user_id = Uuid::new_v4();
let session_id = Uuid::new_v4(); let session_id = Uuid::new_v4();
@ -382,13 +366,13 @@ mod tests {
let token = jwt_service let token = jwt_service
.generate_access_token(user_id, session_id, roles) .generate_access_token(user_id, session_id, roles)
.expect("Failed to generate access token for test"); .unwrap();
// Test valid Bearer token // Test valid Bearer token
let mut request = Request::builder() let mut request = Request::builder()
.header(AUTHORIZATION, format!("Bearer {}", token)) .header(AUTHORIZATION, format!("Bearer {}", token))
.body(Body::empty()) .body(Body::empty())
.expect("Failed to build request for test"); .unwrap();
// Verify token parsing would work // Verify token parsing would work
let auth_header = request let auth_header = request

View File

@ -188,8 +188,7 @@ impl RefreshTokenClaims {
/// Generate RSA key pair for JWT signing (RS256) /// Generate RSA key pair for JWT signing (RS256)
pub fn generate_rsa_key_pair() -> Result<RsaKeys> { pub fn generate_rsa_key_pair() -> Result<RsaKeys> {
use rsa::rand_core::OsRng; use rsa::rand_core::OsRng;
use rsa::{RsaPrivateKey, RsaPublicKey}; use rsa::{pkcs1::EncodeRsaPrivateKey, pkcs1::EncodeRsaPublicKey, RsaPrivateKey, RsaPublicKey};
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding};
// Generate 2048-bit RSA key pair with OS randomness for cryptographic security // Generate 2048-bit RSA key pair with OS randomness for cryptographic security
let private_key = let private_key =
@ -197,14 +196,14 @@ pub fn generate_rsa_key_pair() -> Result<RsaKeys> {
let public_key = RsaPublicKey::from(&private_key); let public_key = RsaPublicKey::from(&private_key);
// Convert to PKCS#8 PEM format (required by jsonwebtoken and signature crates) // Convert to PEM format
let private_key_pem = private_key let private_key_pem = private_key
.to_pkcs8_pem(LineEnding::LF) .to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)
.context("Failed to encode private key as PKCS#8 PEM")? .context("Failed to encode private key as PEM")?
.to_string(); .to_string();
let public_key_pem = public_key let public_key_pem = public_key
.to_public_key_pem(LineEnding::LF) .to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)
.context("Failed to encode public key as PEM")?; .context("Failed to encode public key as PEM")?;
Ok(RsaKeys { Ok(RsaKeys {
@ -238,23 +237,10 @@ pub fn load_rsa_keys_from_files(private_key_path: &str, public_key_path: &str) -
mod tests { mod tests {
use super::*; use super::*;
fn create_test_jwt_config() -> JwtConfig {
let keys = generate_rsa_key_pair().expect("Failed to generate test keys");
JwtConfig {
issuer: "test-issuer".to_string(),
audience: "test-audience".to_string(),
access_token_expiration_hours: 1,
refresh_token_expiration_hours: 24,
private_key_pem: keys.private_key_pem,
public_key_pem: keys.public_key_pem,
}
}
#[tokio::test] #[tokio::test]
async fn test_jwt_token_generation_and_verification() { async fn test_jwt_token_generation_and_verification() {
let config = create_test_jwt_config(); let config = JwtConfig::default();
let jwt_service = JwtService::new(config) let jwt_service = JwtService::new(config).unwrap();
.expect("Failed to create JWT service for test");
let user_id = Uuid::new_v4(); let user_id = Uuid::new_v4();
let session_id = Uuid::new_v4(); let session_id = Uuid::new_v4();
@ -263,12 +249,12 @@ mod tests {
// Generate tokens // Generate tokens
let token_response = jwt_service let token_response = jwt_service
.generate_token_pair(user_id, session_id, roles.clone()) .generate_token_pair(user_id, session_id, roles.clone())
.expect("Failed to generate token pair for test"); .unwrap();
// Verify access token // Verify access token
let access_claims = jwt_service let access_claims = jwt_service
.verify_access_token(&token_response.access_token) .verify_access_token(&token_response.access_token)
.expect("Failed to verify access token for test"); .unwrap();
assert_eq!(access_claims.claims.sub, user_id.to_string()); assert_eq!(access_claims.claims.sub, user_id.to_string());
assert_eq!(access_claims.claims.session_id, session_id.to_string()); assert_eq!(access_claims.claims.session_id, session_id.to_string());
assert_eq!(access_claims.claims.roles, roles); assert_eq!(access_claims.claims.roles, roles);

View File

@ -19,15 +19,50 @@ use control_center::auth::{
user::{User, UserRole, UserService}, user::{User, UserRole, UserService},
AuthService, AuthService,
}; };
use control_center::services::jwt::generate_rsa_key_pair;
/// Generate RSA key pair for testing /// Generate RSA key pair for testing (pre-generated to avoid rand_core
/// conflict)
fn generate_test_keys() -> (Vec<u8>, Vec<u8>) { fn generate_test_keys() -> (Vec<u8>, Vec<u8>) {
let keys = generate_rsa_key_pair().expect("Failed to generate test RSA keys"); let private_pem = b"-----BEGIN PRIVATE KEY-----
( MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7F43HxrVfJJ+k
keys.private_key_pem.into_bytes(), DQMEjENGqJLnBn6MvJnCu93A4ZNKEEpPGX1Y6V+qiqLH5B7wNMIJ2QVnLjYjKZu5
keys.public_key_pem.into_bytes(), K5LkVCH8N7N1s9QXiLI7TzQVEH2TQx3MzQFRmVzGlyVFxF0qMHf/mNlZvE0Hb9oI
) YdYnBW5k4TRmzrN4THEbWqLcGVVfMVFQqG1j/V3G5dTpV1sN8a9xqJG0dKNmVP2M
H5JG3kQ8CxFqp7pRsGvqH9IVjB6KXj8L3VPQYJxIBKHGZMlsP9SVqH3vwqLLKcBj
G2UvhkVYJ6ZLvYIj7LvZKrCBrP3h5Q7hYpXZqDGYd5dDwWFJ3xkVMkJMGR3D3VrK
R1jvJTEhAgMBAAECggEAXV2TZjfRbhR0IaqU3bpSZvb+6uL+5OgWRO5VbG0XGFmv
1v+2F5hQEZYKKxZvTlF4IxN1J0YQRKt4BG6PZqN9+Uj8z7DVEDqiWLIZSEhUPzVu
E3IKZaEqvGKvEwBglmfqnE6cEZ2Kb9aOx1a5N8UH5q7xQ2d4YxKfYwRBkDYEZmZp
R4tEb0qfKFgIGmCfK5fNvJqXvH/f1xq9QW1qrGYqGAqh7bkxvXGfkRBXh7Q8sAXW
B2XQ8L9CZV5lsP0F7n8nKvDVKfqZfGXlZJZQvJPF5L8p6g7V7uE+6bZlF4YZLRvW
2aq2xZR6bGCWxBpLb2lG1QU0aJLqVmVjAQYDM9PB0QKBgQDfhKn8F5aITVFJhVVu
BN8F2hLRtFDfSPTN0JM1WZq/EqYdLjnlAY8L7kBBzNaKgOGBRqfO1lVv7DG4vPtX
nrLaM3SU3F0lC8tVwqJTUvZS5/Pl3wqAZRlWbWm4Yp6gWXlGPTaWRpGRBOqB+rWP
b8rX2SnZjvFlQZlZPfvHGWpkFQKBgQDYtvXAGNdj8hMZ6mMUKYv3YbF7F2xUlf1x
H0f8I5QGPiZoqzNGJXCEGvNVNEUgR7LY6AeqWtLqRKJsQ5NYb2XzLhA2BVOV1BV2
Z1Y3s0V5XjFE4q7gWmEcLlWyEiKiRCRQC4T7qB+gUQ8lFBrGqQ4vZqE+7pQyYfCt
1yFQnvqrQQKBgQCcvCvE4wMhF2lYFxM4v8qCDXJ9nLMKjYPFn3C0YrP6Zn2Pm7r3
fJGDVBmN5wVLDXqZnSrmh0iBs7PdBqeHALF7jTj9X8tNe+hpXwQ/GWSF8b3q1b6k
wZLhKpJQGpYH0M3l7B6rrM3Vr1E/DQjXvXdqXrL3vKfHQrEk3c9zVCkfTQKBgGVl
qdvBYvdgWHvPzL3+vqvLPVKDnLjN6hXA2cJGaBNpOlm+bI5cJLLnWhJrjCkK+POo
0oPKvnO5qKQjRDWLVCJH3z8MxPjqV6Yk1hAg/4vqV3oQsJDfU3qiDPAHrBbKBlRm
4KYmEqIhqNYJqXN7nnQFOLYFxOaXKqe6HBh1g4QBAoGBAIRXPwKJhwIUfLJ8DQAO
9z9YH3eIiGkLxF8f7WZvVnNwMEpXl4IqN8N3qGXKJfXJI1YfBKXN7LOqCOVFGkVq
JkVN8qPTwAKU8E2U3dGzQVPP2JmGPPNyZJxQJ3sYtKnLkz2YdEbKDhkG5wPDHKKu
LfEK1YMqL/VvAJdIXxqZqDXf
-----END PRIVATE KEY-----";
let public_pem = b"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuxeNx8a1XySfpA0DBIxD
RqiS5wZ+jLyZwrvdwOGTShBKTxl9WOlfqoqix+Qe8DTCCdkFZy42IymbhSuS5FQh
/DezdLPUF4iyO080FRB9k0MdzM0BUZlcxpclRcRdKjB3/5jZWbxNB2/aCGHWJwVu
ZOE0Zs6zeExxG1qi3BlVXzFRUKhtY/1dxuXU6Vdbl/GvcaiRtHSjZlT9jB+SRt5E
PAsTaqe6UbBr6h/SFYweil4/C91T0GCcSAShxmTJbD/Ulah978KizynAYxtlL4ZF
WCemS72CI+y72SqwgaZ94eUO4WKV2agxmHeXQ8FhSd8ZFTJCTBKDW91Kykdo7yUx
IQIDAQAB
-----END PUBLIC KEY-----";
(private_pem.to_vec(), public_pem.to_vec())
} }
/// Create JWT service for testing /// Create JWT service for testing
@ -39,7 +74,7 @@ fn create_jwt_service() -> JwtService {
"test-control-center", "test-control-center",
vec!["orchestrator".to_string(), "cli".to_string()], vec!["orchestrator".to_string(), "cli".to_string()],
) )
.expect("Failed to create JWT service for tests") .unwrap()
} }
#[test] #[test]
@ -362,7 +397,7 @@ async fn test_full_auth_flow() {
#[test] #[test]
fn test_invalid_signature_detection() { fn test_invalid_signature_detection() {
let (private_key1, public_key1) = generate_test_keys(); let (private_key1, public_key1) = generate_test_keys();
let (_private_key2, public_key2) = generate_test_keys(); // Different key pair let (private_key2, _) = generate_test_keys(); // Different key pair
// Service 1 generates token // Service 1 generates token
let jwt_service1 = JwtService::new( let jwt_service1 = JwtService::new(
@ -371,23 +406,22 @@ fn test_invalid_signature_detection() {
"test-issuer", "test-issuer",
vec!["test-audience".to_string()], vec!["test-audience".to_string()],
) )
.expect("Failed to create jwt_service1"); .unwrap();
let token_pair = jwt_service1 let token_pair = jwt_service1
.generate_token_pair("user123", "workspace1", "perm_hash", None) .generate_token_pair("user123", "workspace1", "perm_hash", None)
.expect("Failed to generate token pair"); .unwrap();
// Service 2 with different public key tries to validate // Service 2 with different keys tries to validate
// This should fail because the token was signed with key1 but we're validating with key2
let jwt_service2 = JwtService::new( let jwt_service2 = JwtService::new(
&private_key1, &private_key2,
&public_key2, // Different public key! &public_key1,
"test-issuer", "test-issuer",
vec!["test-audience".to_string()], vec!["test-audience".to_string()],
) )
.expect("Failed to create jwt_service2"); .unwrap();
// Should fail signature verification because public keys don't match // Should fail signature verification
let validation_result = jwt_service2.validate_token(&token_pair.access_token); let validation_result = jwt_service2.validate_token(&token_pair.access_token);
assert!(validation_result.is_err()); assert!(validation_result.is_err());
} }

View File

@ -1,35 +1,20 @@
use axum::body::Body; use axum::body::Body;
use axum::http::{Request, StatusCode}; use axum::http::{Request, StatusCode};
use extension_registry::{build_routes, AppState, Config}; use extension_registry::{build_routes, AppState, Config};
use extension_registry::config::OciConfig;
use http_body_util::BodyExt; use http_body_util::BodyExt;
use tower::ServiceExt; use tower::ServiceExt;
/// Create a minimal test config with a mock OCI backend #[tokio::test]
fn create_test_config() -> Config { async fn test_health_check() {
Config { let config = Config {
server: extension_registry::config::ServerConfig::default(), server: extension_registry::config::ServerConfig::default(),
gitea: None, gitea: None,
// Use OCI as test backend (doesn't require file validation for auth_token_path) oci: None,
oci: Some(OciConfig {
id: Some("test-oci".to_string()),
registry: "localhost:5000".to_string(),
namespace: "test".to_string(),
auth_token_path: None,
timeout_seconds: 30,
verify_ssl: false,
}),
sources: extension_registry::config::SourcesConfig::default(), sources: extension_registry::config::SourcesConfig::default(),
distributions: extension_registry::config::DistributionsConfig::default(), distributions: extension_registry::config::DistributionsConfig::default(),
cache: extension_registry::config::CacheConfig::default(), cache: extension_registry::config::CacheConfig::default(),
} };
}
#[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
#[ignore] // Requires OCI registry service to be running
async fn test_health_check() {
let config = create_test_config();
let state = AppState::new(config).expect("Failed to create app state"); let state = AppState::new(config).expect("Failed to create app state");
let app = build_routes(state); let app = build_routes(state);
@ -54,9 +39,16 @@ async fn test_health_check() {
} }
#[tokio::test] #[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
async fn test_list_extensions_empty() { async fn test_list_extensions_empty() {
let config = create_test_config(); let config = Config {
server: extension_registry::config::ServerConfig::default(),
gitea: None,
oci: None,
sources: extension_registry::config::SourcesConfig::default(),
distributions: extension_registry::config::DistributionsConfig::default(),
cache: extension_registry::config::CacheConfig::default(),
};
let state = AppState::new(config).expect("Failed to create app state"); let state = AppState::new(config).expect("Failed to create app state");
let app = build_routes(state); let app = build_routes(state);
@ -80,9 +72,15 @@ async fn test_list_extensions_empty() {
} }
#[tokio::test] #[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
async fn test_get_nonexistent_extension() { async fn test_get_nonexistent_extension() {
let config = create_test_config(); let config = Config {
server: extension_registry::config::ServerConfig::default(),
gitea: None,
oci: None,
sources: extension_registry::config::SourcesConfig::default(),
distributions: extension_registry::config::DistributionsConfig::default(),
cache: extension_registry::config::CacheConfig::default(),
};
let state = AppState::new(config).expect("Failed to create app state"); let state = AppState::new(config).expect("Failed to create app state");
let app = build_routes(state); let app = build_routes(state);
@ -101,9 +99,15 @@ async fn test_get_nonexistent_extension() {
} }
#[tokio::test] #[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
async fn test_metrics_endpoint() { async fn test_metrics_endpoint() {
let config = create_test_config(); let config = Config {
server: extension_registry::config::ServerConfig::default(),
gitea: None,
oci: None,
sources: extension_registry::config::SourcesConfig::default(),
distributions: extension_registry::config::DistributionsConfig::default(),
cache: extension_registry::config::CacheConfig::default(),
};
let state = AppState::new(config).expect("Failed to create app state"); let state = AppState::new(config).expect("Failed to create app state");
let app = build_routes(state); let app = build_routes(state);
@ -127,9 +131,15 @@ async fn test_metrics_endpoint() {
} }
#[tokio::test] #[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
async fn test_cache_stats_endpoint() { async fn test_cache_stats_endpoint() {
let config = create_test_config(); let config = Config {
server: extension_registry::config::ServerConfig::default(),
gitea: None,
oci: None,
sources: extension_registry::config::SourcesConfig::default(),
distributions: extension_registry::config::DistributionsConfig::default(),
cache: extension_registry::config::CacheConfig::default(),
};
let state = AppState::new(config).expect("Failed to create app state"); let state = AppState::new(config).expect("Failed to create app state");
let app = build_routes(state); let app = build_routes(state);
@ -155,9 +165,15 @@ async fn test_cache_stats_endpoint() {
} }
#[tokio::test] #[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
async fn test_invalid_extension_type() { async fn test_invalid_extension_type() {
let config = create_test_config(); let config = Config {
server: extension_registry::config::ServerConfig::default(),
gitea: None,
oci: None,
sources: extension_registry::config::SourcesConfig::default(),
distributions: extension_registry::config::DistributionsConfig::default(),
cache: extension_registry::config::CacheConfig::default(),
};
let state = AppState::new(config).expect("Failed to create app state"); let state = AppState::new(config).expect("Failed to create app state");
let app = build_routes(state); let app = build_routes(state);

View File

@ -138,64 +138,31 @@ async fn test_components(engine: &ProvisioningEngine, tools: &ProvisioningTools)
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::fs;
use tempfile::TempDir;
/// Create a test config with a temporary provisioning path
fn create_test_config() -> (Config, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let temp_path = temp_dir.path();
// Create minimal provisioning directory structure for tests
let core_path = temp_path.join("core/nulib");
fs::create_dir_all(&core_path).expect("Failed to create test provisioning dir");
// Create the provisioning script file
let script_path = core_path.join("provisioning");
fs::write(&script_path, "#!/bin/bash\necho 'test provisioning script'")
.expect("Failed to create test provisioning script");
let config = Config {
provisioning_path: temp_path.to_path_buf(),
ai: config::AIConfig::default(),
server: config::ServerConfig::default(),
debug: false,
};
(config, temp_dir)
}
#[tokio::test] #[tokio::test]
async fn test_server_creation() { async fn test_server_creation() {
let (config, _temp) = create_test_config(); let config = Config::default();
let engine = ProvisioningEngine::new(&config).expect("Failed to create engine");
let tools = ProvisioningTools::new(&config); let tools = ProvisioningTools::new(&config);
// Test parsing - the legacy implementation returns the description as-is // Test parsing
let result = tools.parse_server_description("Create 1 server for web hosting"); let result = tools.parse_server_description("Create 1 server for web hosting");
assert!(result.is_ok(), "Failed to parse server description"); assert!(result.is_ok());
let server_config = result.unwrap(); let server_config = result.unwrap();
// The legacy parse_server_description returns {"description": "..."} assert_eq!(server_config["count"], 1);
assert!(server_config.get("description").is_some(), "Missing description field");
assert_eq!(
server_config["description"].as_str(),
Some("Create 1 server for web hosting"),
"Description mismatch"
);
} }
#[test] #[test]
fn test_config_loading() { fn test_config_loading() {
let (config, _temp) = create_test_config(); let config = Config::default();
assert!(!config.debug, "Debug should be false by default"); assert!(!config.debug);
assert!( assert!(
config.provisioning_path.exists(), config.provisioning_path.exists()
"Provisioning path should exist (it's a temp dir)" || !config
); .provisioning_path
assert_eq!( .to_string_lossy()
config.provisioning_path.to_string_lossy().ends_with("provisioning"), .contains("/usr/local/provisioning")
false,
"Test path should not end with /provisioning"
); );
} }
} }

View File

@ -13,26 +13,17 @@
//! - Export to common SIEM formats (Splunk, ELK) //! - Export to common SIEM formats (Splunk, ELK)
//! //!
//! # Example //! # Example
//! ```ignore //! ```no_run
//! use provisioning_orchestrator::audit::{AuditLogger, ActionType, AuditLoggerConfig}; //! use audit::{AuditLogger, AuditEvent, ActionType};
//! use std::path::PathBuf;
//! //!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! let config = AuditLoggerConfig {
//! log_dir: PathBuf::from("/var/log/audit"),
//! enable_syslog: false,
//! ..Default::default()
//! };
//! let logger = AuditLogger::new(config).await?; //! let logger = AuditLogger::new(config).await?;
//! //!
//! logger.log_action( //! logger.log_action(
//! "user123", //! user_id,
//! ActionType::ServerCreate, //! ActionType::ServerCreate,
//! "my-infrastructure", //! "my-infrastructure",
//! serde_json::json!({"count": 3}), //! serde_json::json!({"count": 3}),
//! ).await?; //! ).await?;
//! # Ok(())
//! # }
//! ``` //! ```
pub mod logger; pub mod logger;

View File

@ -12,11 +12,9 @@
//! - Automated compliance reporting //! - Automated compliance reporting
//! //!
//! # Example //! # Example
//! ```ignore //! ```no_run
//! use provisioning_orchestrator::compliance::{ComplianceService, ComplianceConfig}; //! use compliance::{ComplianceService, GdprService};
//! //!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! let config = ComplianceConfig::default();
//! let compliance = ComplianceService::new(config).await?; //! let compliance = ComplianceService::new(config).await?;
//! //!
//! // Export user data for GDPR compliance //! // Export user data for GDPR compliance
@ -24,8 +22,6 @@
//! //!
//! // Generate SOC2 report //! // Generate SOC2 report
//! let report = compliance.soc2.generate_report().await?; //! let report = compliance.soc2.generate_report().await?;
//! # Ok(())
//! # }
//! ``` //! ```
pub mod access_control; pub mod access_control;

View File

@ -20,7 +20,6 @@ fn create_test_user(id: &str, team: &str, role: Role) -> User {
} }
#[tokio::test] #[tokio::test]
#[ignore] // Hangs: service.start() spawns background tasks that don't shut down cleanly
async fn test_complete_break_glass_workflow() { async fn test_complete_break_glass_workflow() {
// Setup // Setup
let config = BreakGlassConfig::default(); let config = BreakGlassConfig::default();
@ -195,7 +194,6 @@ async fn test_approval_validation() {
} }
#[tokio::test] #[tokio::test]
#[ignore] // Hangs due to background task spawned by service.start() not completing
async fn test_auto_revocation_expired() { async fn test_auto_revocation_expired() {
let mut config = BreakGlassConfig::default(); let mut config = BreakGlassConfig::default();
config.max_session_duration_hours = 0; // Immediate expiration config.max_session_duration_hours = 0; // Immediate expiration

View File

@ -39,7 +39,6 @@ async fn test_service_registration() {
} }
#[tokio::test] #[tokio::test]
#[ignore] // Hangs: resolve_startup_order has an infinite loop or deadlock
async fn test_dependency_resolution_simple() { async fn test_dependency_resolution_simple() {
let orchestrator = ServiceOrchestrator::new( let orchestrator = ServiceOrchestrator::new(
"/usr/local/bin/nu".to_string(), "/usr/local/bin/nu".to_string(),
@ -84,7 +83,6 @@ async fn test_dependency_resolution_simple() {
} }
#[tokio::test] #[tokio::test]
#[ignore] // Hangs: resolve_startup_order has an infinite loop or deadlock
async fn test_dependency_resolution_complex() { async fn test_dependency_resolution_complex() {
let orchestrator = ServiceOrchestrator::new( let orchestrator = ServiceOrchestrator::new(
"/usr/local/bin/nu".to_string(), "/usr/local/bin/nu".to_string(),

View File

@ -363,11 +363,6 @@ fn test_empty_config_file_toml() {
#[test] #[test]
fn test_environment_partial_override() { fn test_environment_partial_override() {
// Clean up all relevant environment variables to ensure test isolation
env::remove_var("TEST_SERVICE_NAME");
env::remove_var("TEST_SERVICE_PORT");
env::remove_var("TEST_SERVICE_ENABLED");
// Only override one field // Only override one field
env::set_var("TEST_SERVICE_PORT", "5555"); env::set_var("TEST_SERVICE_PORT", "5555");
@ -385,7 +380,6 @@ fn test_environment_partial_override() {
assert_eq!(config.port, 5555); // overridden assert_eq!(config.port, 5555); // overridden
assert!(config.enabled); // unchanged assert!(config.enabled); // unchanged
// Clean up after test
env::remove_var("TEST_SERVICE_PORT"); env::remove_var("TEST_SERVICE_PORT");
} }

View File

@ -80,7 +80,6 @@ impl ConfigLoader for McpServerConfig {
} }
#[test] #[test]
#[ignore] // Requires Nickel files to exist in /tmp/
fn test_load_mcp_server_from_nickel() { fn test_load_mcp_server_from_nickel() {
let ncl_file = "/tmp/mcp-server.solo.ncl"; let ncl_file = "/tmp/mcp-server.solo.ncl";
@ -211,7 +210,6 @@ impl ConfigLoader for RagConfig {
} }
#[test] #[test]
#[ignore] // Requires Nickel files to exist in /tmp/
fn test_load_rag_from_nickel() { fn test_load_rag_from_nickel() {
let ncl_file = "/tmp/rag.solo.ncl"; let ncl_file = "/tmp/rag.solo.ncl";
@ -317,7 +315,6 @@ impl ConfigLoader for ExtensionRegistryConfig {
} }
#[test] #[test]
#[ignore] // Requires Nickel files to exist in /tmp/
fn test_load_extension_registry_from_nickel() { fn test_load_extension_registry_from_nickel() {
let ncl_file = "/tmp/extension-registry.solo.ncl"; let ncl_file = "/tmp/extension-registry.solo.ncl";
@ -341,7 +338,6 @@ fn test_load_extension_registry_from_nickel() {
} }
#[test] #[test]
#[ignore] // Requires Nickel files to exist in /tmp/
fn test_load_control_center_from_nickel() { fn test_load_control_center_from_nickel() {
let ncl_file = "/tmp/control-center.solo.ncl"; let ncl_file = "/tmp/control-center.solo.ncl";

View File

@ -15,7 +15,7 @@
//! //!
//! # Example //! # Example
//! //!
//! ```ignore //! ```no_run
//! use service_clients::MachinesClient; //! use service_clients::MachinesClient;
//! //!
//! #[tokio::main] //! #[tokio::main]