Compare commits
No commits in common. "f411df0272a20e7944a089748383d1654e406873" and "2a9e4f59faf77835ef1737c1244bc6c238d35931" have entirely different histories.
f411df0272
...
2a9e4f59fa
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,7 +10,6 @@ CLAUDE.md
|
||||
.cache
|
||||
.coder
|
||||
.wrks
|
||||
rollback_instruction*
|
||||
ROOT
|
||||
OLD
|
||||
# Generated by Cargo
|
||||
|
||||
@ -41,30 +41,6 @@ repos:
|
||||
# pass_filenames: false
|
||||
# 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)
|
||||
# ============================================================================
|
||||
|
||||
@ -505,20 +505,10 @@ impl ToolRegistry {
|
||||
|
||||
let mut settings_tools = self.settings_tools.lock().await;
|
||||
|
||||
let settings = settings_tools
|
||||
settings_tools
|
||||
.get_settings(query)
|
||||
.await
|
||||
.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")
|
||||
}))
|
||||
.map_err(|e| format!("Failed to get settings: {}", e))
|
||||
}
|
||||
|
||||
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 defaults = settings_tools
|
||||
settings_tools
|
||||
.get_mode_defaults(mode)
|
||||
.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")
|
||||
}))
|
||||
.map_err(|e| format!("Failed to get defaults: {}", e))
|
||||
}
|
||||
|
||||
async fn installer_platform_recommendations(&self, _args: &Value) -> Result<Value, String> {
|
||||
|
||||
@ -100,13 +100,12 @@ async fn test_explicit_tool_call_rag_ask() {
|
||||
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["tool"], "rag_ask_question");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_explicit_tool_call_guidance_status() {
|
||||
let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap();
|
||||
let service = AiService::new(addr);
|
||||
@ -116,7 +115,7 @@ async fn test_explicit_tool_call_guidance_status() {
|
||||
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["tool"], "guidance_check_system_status");
|
||||
}
|
||||
@ -131,7 +130,7 @@ async fn test_explicit_tool_call_settings() {
|
||||
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");
|
||||
// Verify real SettingsTools data is returned (not empty placeholder)
|
||||
assert!(
|
||||
@ -152,7 +151,7 @@ async fn test_settings_tools_platform_recommendations() {
|
||||
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");
|
||||
// Should have real recommendations array from SettingsTools platform detection
|
||||
assert!(response.result.get("recommendations").is_some());
|
||||
@ -168,7 +167,7 @@ async fn test_settings_tools_mode_defaults() {
|
||||
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");
|
||||
// Verify real mode defaults (resource requirements)
|
||||
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"}),
|
||||
};
|
||||
|
||||
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");
|
||||
// Verify real technology detection (returns technologies array)
|
||||
assert!(response.result.get("technologies").is_some());
|
||||
@ -202,7 +201,7 @@ async fn test_iac_detect_technologies_real() {
|
||||
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");
|
||||
|
||||
// Should detect technologies as an array
|
||||
@ -221,7 +220,7 @@ async fn test_iac_analyze_completeness() {
|
||||
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");
|
||||
// Verify real analysis data
|
||||
assert!(response.result.get("complete").is_some());
|
||||
@ -365,7 +364,7 @@ async fn test_tool_execution_with_required_args() {
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@ -86,8 +86,7 @@ impl TokenClaims {
|
||||
pub fn needs_rotation(&self) -> bool {
|
||||
let now = Utc::now().timestamp();
|
||||
let time_until_expiry = self.exp - now;
|
||||
// Needs rotation if within 5 minutes AND not yet expired
|
||||
time_until_expiry <= 300 && time_until_expiry > 0
|
||||
time_until_expiry <= 300 // 5 minutes
|
||||
}
|
||||
|
||||
/// Get remaining validity duration
|
||||
@ -172,7 +171,7 @@ impl JwtService {
|
||||
/// &private_key,
|
||||
/// &public_key,
|
||||
/// "control-center",
|
||||
/// vec!["orchestrator".to_string(), "cli".to_string()]
|
||||
/// vec!["orchestrator", "cli"]
|
||||
/// ).unwrap();
|
||||
/// ```
|
||||
pub fn new(
|
||||
@ -470,16 +469,48 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn generate_test_keys() -> (Vec<u8>, Vec<u8>) {
|
||||
// Generate fresh RSA keys for testing
|
||||
use crate::services::jwt::generate_rsa_key_pair;
|
||||
// Pre-generated RSA keys to avoid runtime key generation (avoids rand_core
|
||||
// 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()
|
||||
.expect("Failed to generate test RSA keys");
|
||||
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-----";
|
||||
|
||||
(
|
||||
keys.private_key_pem.into_bytes(),
|
||||
keys.public_key_pem.into_bytes(),
|
||||
)
|
||||
(private_pem.to_vec(), public_pem.to_vec())
|
||||
}
|
||||
|
||||
fn create_test_service() -> JwtService {
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
//! # Usage Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use control_center::auth::{JwtService, PasswordService};
|
||||
//! # use control_center::auth::{JwtService, PasswordService, User};
|
||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! // Initialize services
|
||||
//! let private_key = std::fs::read("keys/private.pem")?;
|
||||
@ -80,12 +80,19 @@
|
||||
//!
|
||||
//! let password_service = PasswordService::new();
|
||||
//!
|
||||
//! // Verify password
|
||||
//! // Create user with hashed password
|
||||
//! let password_hash = password_service.hash_password("secure_password")?;
|
||||
//! if password_service.verify_password("secure_password", &password_hash)? {
|
||||
//! // Generate token pair for authenticated user
|
||||
//! let user = 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(
|
||||
//! "user123",
|
||||
//! &user.id,
|
||||
//! "workspace1",
|
||||
//! "permissions_hash",
|
||||
//! None,
|
||||
@ -93,6 +100,7 @@
|
||||
//!
|
||||
//! println!("Access token: {}", tokens.access_token);
|
||||
//! println!("Expires in: {} seconds", tokens.expires_in);
|
||||
//! }
|
||||
//!
|
||||
//! // Validate token
|
||||
//! let claims = jwt_service.validate_token(&tokens.access_token)?;
|
||||
@ -106,7 +114,6 @@
|
||||
//!
|
||||
//! // Revoke token (logout)
|
||||
//! jwt_service.revoke_token(&claims.jti, claims.exp)?;
|
||||
//! }
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
@ -157,9 +157,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_password_strength_fair() {
|
||||
let service = PasswordService::new();
|
||||
// Fair: 8-9 chars with 0-2 complexity types (lowercase, uppercase, digit, special)
|
||||
assert_eq!(
|
||||
service.evaluate_strength("password1"), // 9 chars, 2 types: lowercase + digit
|
||||
service.evaluate_strength("Password1"),
|
||||
PasswordStrength::Fair
|
||||
);
|
||||
}
|
||||
|
||||
@ -23,12 +23,12 @@ use crate::kms::types::{KeyData, KeyType, ProviderCredentials};
|
||||
/// # Usage
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use control_center::kms::{KmsService, KeyType};
|
||||
/// use control_center::kms::{KmsService, KmsConfig};
|
||||
/// use std::sync::Arc;
|
||||
///
|
||||
/// async fn example(kms: Arc<dyn KmsService>) -> anyhow::Result<()> {
|
||||
/// // 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);
|
||||
/// Ok(())
|
||||
/// }
|
||||
|
||||
@ -356,25 +356,9 @@ mod tests {
|
||||
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]
|
||||
async fn test_auth_header_parsing() {
|
||||
let jwt_service = Arc::new(
|
||||
JwtService::new(create_test_jwt_config())
|
||||
.expect("Failed to create JWT service for test")
|
||||
);
|
||||
let jwt_service = Arc::new(JwtService::new(JwtConfig::default()).unwrap());
|
||||
|
||||
let user_id = Uuid::new_v4();
|
||||
let session_id = Uuid::new_v4();
|
||||
@ -382,13 +366,13 @@ mod tests {
|
||||
|
||||
let token = jwt_service
|
||||
.generate_access_token(user_id, session_id, roles)
|
||||
.expect("Failed to generate access token for test");
|
||||
.unwrap();
|
||||
|
||||
// Test valid Bearer token
|
||||
let mut request = Request::builder()
|
||||
.header(AUTHORIZATION, format!("Bearer {}", token))
|
||||
.body(Body::empty())
|
||||
.expect("Failed to build request for test");
|
||||
.unwrap();
|
||||
|
||||
// Verify token parsing would work
|
||||
let auth_header = request
|
||||
|
||||
@ -188,8 +188,7 @@ impl RefreshTokenClaims {
|
||||
/// Generate RSA key pair for JWT signing (RS256)
|
||||
pub fn generate_rsa_key_pair() -> Result<RsaKeys> {
|
||||
use rsa::rand_core::OsRng;
|
||||
use rsa::{RsaPrivateKey, RsaPublicKey};
|
||||
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding};
|
||||
use rsa::{pkcs1::EncodeRsaPrivateKey, pkcs1::EncodeRsaPublicKey, RsaPrivateKey, RsaPublicKey};
|
||||
|
||||
// Generate 2048-bit RSA key pair with OS randomness for cryptographic security
|
||||
let private_key =
|
||||
@ -197,14 +196,14 @@ pub fn generate_rsa_key_pair() -> Result<RsaKeys> {
|
||||
|
||||
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
|
||||
.to_pkcs8_pem(LineEnding::LF)
|
||||
.context("Failed to encode private key as PKCS#8 PEM")?
|
||||
.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)
|
||||
.context("Failed to encode private key as PEM")?
|
||||
.to_string();
|
||||
|
||||
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")?;
|
||||
|
||||
Ok(RsaKeys {
|
||||
@ -238,23 +237,10 @@ pub fn load_rsa_keys_from_files(private_key_path: &str, public_key_path: &str) -
|
||||
mod tests {
|
||||
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]
|
||||
async fn test_jwt_token_generation_and_verification() {
|
||||
let config = create_test_jwt_config();
|
||||
let jwt_service = JwtService::new(config)
|
||||
.expect("Failed to create JWT service for test");
|
||||
let config = JwtConfig::default();
|
||||
let jwt_service = JwtService::new(config).unwrap();
|
||||
|
||||
let user_id = Uuid::new_v4();
|
||||
let session_id = Uuid::new_v4();
|
||||
@ -263,12 +249,12 @@ mod tests {
|
||||
// Generate tokens
|
||||
let token_response = jwt_service
|
||||
.generate_token_pair(user_id, session_id, roles.clone())
|
||||
.expect("Failed to generate token pair for test");
|
||||
.unwrap();
|
||||
|
||||
// Verify access token
|
||||
let access_claims = jwt_service
|
||||
.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.session_id, session_id.to_string());
|
||||
assert_eq!(access_claims.claims.roles, roles);
|
||||
|
||||
@ -19,15 +19,50 @@ use control_center::auth::{
|
||||
user::{User, UserRole, UserService},
|
||||
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>) {
|
||||
let keys = generate_rsa_key_pair().expect("Failed to generate test RSA keys");
|
||||
(
|
||||
keys.private_key_pem.into_bytes(),
|
||||
keys.public_key_pem.into_bytes(),
|
||||
)
|
||||
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 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
|
||||
@ -39,7 +74,7 @@ fn create_jwt_service() -> JwtService {
|
||||
"test-control-center",
|
||||
vec!["orchestrator".to_string(), "cli".to_string()],
|
||||
)
|
||||
.expect("Failed to create JWT service for tests")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -362,7 +397,7 @@ async fn test_full_auth_flow() {
|
||||
#[test]
|
||||
fn test_invalid_signature_detection() {
|
||||
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
|
||||
let jwt_service1 = JwtService::new(
|
||||
@ -371,23 +406,22 @@ fn test_invalid_signature_detection() {
|
||||
"test-issuer",
|
||||
vec!["test-audience".to_string()],
|
||||
)
|
||||
.expect("Failed to create jwt_service1");
|
||||
.unwrap();
|
||||
|
||||
let token_pair = jwt_service1
|
||||
.generate_token_pair("user123", "workspace1", "perm_hash", None)
|
||||
.expect("Failed to generate token pair");
|
||||
.unwrap();
|
||||
|
||||
// Service 2 with different public key tries to validate
|
||||
// This should fail because the token was signed with key1 but we're validating with key2
|
||||
// Service 2 with different keys tries to validate
|
||||
let jwt_service2 = JwtService::new(
|
||||
&private_key1,
|
||||
&public_key2, // Different public key!
|
||||
&private_key2,
|
||||
&public_key1,
|
||||
"test-issuer",
|
||||
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);
|
||||
assert!(validation_result.is_err());
|
||||
}
|
||||
|
||||
@ -1,35 +1,20 @@
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use extension_registry::{build_routes, AppState, Config};
|
||||
use extension_registry::config::OciConfig;
|
||||
use http_body_util::BodyExt;
|
||||
use tower::ServiceExt;
|
||||
|
||||
/// Create a minimal test config with a mock OCI backend
|
||||
fn create_test_config() -> Config {
|
||||
Config {
|
||||
#[tokio::test]
|
||||
async fn test_health_check() {
|
||||
let config = Config {
|
||||
server: extension_registry::config::ServerConfig::default(),
|
||||
gitea: None,
|
||||
// Use OCI as test backend (doesn't require file validation for auth_token_path)
|
||||
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,
|
||||
}),
|
||||
oci: None,
|
||||
sources: extension_registry::config::SourcesConfig::default(),
|
||||
distributions: extension_registry::config::DistributionsConfig::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 app = build_routes(state);
|
||||
|
||||
@ -54,9 +39,16 @@ async fn test_health_check() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
||||
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 app = build_routes(state);
|
||||
|
||||
@ -80,9 +72,15 @@ async fn test_list_extensions_empty() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
||||
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 app = build_routes(state);
|
||||
@ -101,9 +99,15 @@ async fn test_get_nonexistent_extension() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
||||
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 app = build_routes(state);
|
||||
@ -127,9 +131,15 @@ async fn test_metrics_endpoint() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
||||
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 app = build_routes(state);
|
||||
@ -155,9 +165,15 @@ async fn test_cache_stats_endpoint() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
||||
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 app = build_routes(state);
|
||||
|
||||
@ -138,64 +138,31 @@ async fn test_components(engine: &ProvisioningEngine, tools: &ProvisioningTools)
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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]
|
||||
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);
|
||||
|
||||
// Test parsing - the legacy implementation returns the description as-is
|
||||
// Test parsing
|
||||
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();
|
||||
// The legacy parse_server_description returns {"description": "..."}
|
||||
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"
|
||||
);
|
||||
assert_eq!(server_config["count"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_loading() {
|
||||
let (config, _temp) = create_test_config();
|
||||
assert!(!config.debug, "Debug should be false by default");
|
||||
let config = Config::default();
|
||||
assert!(!config.debug);
|
||||
assert!(
|
||||
config.provisioning_path.exists(),
|
||||
"Provisioning path should exist (it's a temp dir)"
|
||||
);
|
||||
assert_eq!(
|
||||
config.provisioning_path.to_string_lossy().ends_with("provisioning"),
|
||||
false,
|
||||
"Test path should not end with /provisioning"
|
||||
config.provisioning_path.exists()
|
||||
|| !config
|
||||
.provisioning_path
|
||||
.to_string_lossy()
|
||||
.contains("/usr/local/provisioning")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,26 +13,17 @@
|
||||
//! - Export to common SIEM formats (Splunk, ELK)
|
||||
//!
|
||||
//! # Example
|
||||
//! ```ignore
|
||||
//! use provisioning_orchestrator::audit::{AuditLogger, ActionType, AuditLoggerConfig};
|
||||
//! use std::path::PathBuf;
|
||||
//! ```no_run
|
||||
//! use audit::{AuditLogger, AuditEvent, ActionType};
|
||||
//!
|
||||
//! # 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?;
|
||||
//!
|
||||
//! logger.log_action(
|
||||
//! "user123",
|
||||
//! user_id,
|
||||
//! ActionType::ServerCreate,
|
||||
//! "my-infrastructure",
|
||||
//! serde_json::json!({"count": 3}),
|
||||
//! ).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub mod logger;
|
||||
|
||||
@ -12,11 +12,9 @@
|
||||
//! - Automated compliance reporting
|
||||
//!
|
||||
//! # Example
|
||||
//! ```ignore
|
||||
//! use provisioning_orchestrator::compliance::{ComplianceService, ComplianceConfig};
|
||||
//! ```no_run
|
||||
//! use compliance::{ComplianceService, GdprService};
|
||||
//!
|
||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let config = ComplianceConfig::default();
|
||||
//! let compliance = ComplianceService::new(config).await?;
|
||||
//!
|
||||
//! // Export user data for GDPR compliance
|
||||
@ -24,8 +22,6 @@
|
||||
//!
|
||||
//! // Generate SOC2 report
|
||||
//! let report = compliance.soc2.generate_report().await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub mod access_control;
|
||||
|
||||
@ -20,7 +20,6 @@ fn create_test_user(id: &str, team: &str, role: Role) -> User {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Hangs: service.start() spawns background tasks that don't shut down cleanly
|
||||
async fn test_complete_break_glass_workflow() {
|
||||
// Setup
|
||||
let config = BreakGlassConfig::default();
|
||||
@ -195,7 +194,6 @@ async fn test_approval_validation() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Hangs due to background task spawned by service.start() not completing
|
||||
async fn test_auto_revocation_expired() {
|
||||
let mut config = BreakGlassConfig::default();
|
||||
config.max_session_duration_hours = 0; // Immediate expiration
|
||||
|
||||
@ -39,7 +39,6 @@ async fn test_service_registration() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Hangs: resolve_startup_order has an infinite loop or deadlock
|
||||
async fn test_dependency_resolution_simple() {
|
||||
let orchestrator = ServiceOrchestrator::new(
|
||||
"/usr/local/bin/nu".to_string(),
|
||||
@ -84,7 +83,6 @@ async fn test_dependency_resolution_simple() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Hangs: resolve_startup_order has an infinite loop or deadlock
|
||||
async fn test_dependency_resolution_complex() {
|
||||
let orchestrator = ServiceOrchestrator::new(
|
||||
"/usr/local/bin/nu".to_string(),
|
||||
|
||||
@ -363,11 +363,6 @@ fn test_empty_config_file_toml() {
|
||||
|
||||
#[test]
|
||||
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
|
||||
env::set_var("TEST_SERVICE_PORT", "5555");
|
||||
|
||||
@ -385,7 +380,6 @@ fn test_environment_partial_override() {
|
||||
assert_eq!(config.port, 5555); // overridden
|
||||
assert!(config.enabled); // unchanged
|
||||
|
||||
// Clean up after test
|
||||
env::remove_var("TEST_SERVICE_PORT");
|
||||
}
|
||||
|
||||
|
||||
@ -80,7 +80,6 @@ impl ConfigLoader for McpServerConfig {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Requires Nickel files to exist in /tmp/
|
||||
fn test_load_mcp_server_from_nickel() {
|
||||
let ncl_file = "/tmp/mcp-server.solo.ncl";
|
||||
|
||||
@ -211,7 +210,6 @@ impl ConfigLoader for RagConfig {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Requires Nickel files to exist in /tmp/
|
||||
fn test_load_rag_from_nickel() {
|
||||
let ncl_file = "/tmp/rag.solo.ncl";
|
||||
|
||||
@ -317,7 +315,6 @@ impl ConfigLoader for ExtensionRegistryConfig {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Requires Nickel files to exist in /tmp/
|
||||
fn test_load_extension_registry_from_nickel() {
|
||||
let ncl_file = "/tmp/extension-registry.solo.ncl";
|
||||
|
||||
@ -341,7 +338,6 @@ fn test_load_extension_registry_from_nickel() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Requires Nickel files to exist in /tmp/
|
||||
fn test_load_control_center_from_nickel() {
|
||||
let ncl_file = "/tmp/control-center.solo.ncl";
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! ```no_run
|
||||
//! use service_clients::MachinesClient;
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user