- Add `show-arguments` recipe documenting all version update commands - Add `complete-update-interactive` recipe for manual confirmations - Maintain `complete-update` as automatic mode (no prompts) - Update `update-help` to reference new recipes and modes - Document 7-step workflow and step-by-step differences Changes: - complete-update: Automatic mode (recommended for CI/CD) - complete-update-interactive: Interactive mode (with confirmations) - show-arguments: Complete documentation of all commands and modes - Both modes share same 7-step workflow with different behavior in Step 4
328 lines
9.3 KiB
Rust
328 lines
9.3 KiB
Rust
// Helper functions for authentication
|
|
|
|
use keyring::Entry;
|
|
use reqwest::blocking::Client;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::io::{self, Write};
|
|
|
|
/// Request payload for login endpoint
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
pub struct LoginRequest {
|
|
pub username: String,
|
|
pub password: String,
|
|
}
|
|
|
|
/// Response from login endpoint containing JWT tokens
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
pub struct TokenResponse {
|
|
pub access_token: String,
|
|
pub refresh_token: String,
|
|
pub expires_in: i64,
|
|
pub user: UserInfo,
|
|
}
|
|
|
|
/// User information from login response
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
pub struct UserInfo {
|
|
pub id: String,
|
|
pub username: String,
|
|
pub email: String,
|
|
pub roles: Vec<String>,
|
|
}
|
|
|
|
/// Request payload for logout endpoint
|
|
#[derive(Serialize)]
|
|
pub struct LogoutRequest {
|
|
pub access_token: String,
|
|
}
|
|
|
|
/// Session information (used by auth sessions command - Agente 5)
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
#[allow(dead_code)] // Planned for auth sessions command implementation
|
|
pub struct SessionInfo {
|
|
pub user_id: String,
|
|
pub username: String,
|
|
pub roles: Vec<String>,
|
|
pub created_at: String,
|
|
pub expires_at: String,
|
|
pub is_active: bool,
|
|
}
|
|
|
|
/// Token verification response (used by auth verify command - Agente 4)
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
#[allow(dead_code)] // Planned for auth verify command implementation
|
|
pub struct VerifyResponse {
|
|
pub valid: bool,
|
|
pub user_id: Option<String>,
|
|
pub username: Option<String>,
|
|
pub roles: Option<Vec<String>>,
|
|
pub expires_at: Option<String>,
|
|
}
|
|
|
|
// Secure token storage using OS keyring
|
|
|
|
/// Store tokens in secure keyring
|
|
pub fn store_tokens_in_keyring(
|
|
username: &str,
|
|
access_token: &str,
|
|
refresh_token: &str,
|
|
) -> Result<(), String> {
|
|
let entry_access = Entry::new("provisioning-access", username)
|
|
.map_err(|e| format!("Keyring access error: {}", e))?;
|
|
let entry_refresh = Entry::new("provisioning-refresh", username)
|
|
.map_err(|e| format!("Keyring refresh error: {}", e))?;
|
|
|
|
entry_access
|
|
.set_password(access_token)
|
|
.map_err(|e| format!("Failed to store access token: {}", e))?;
|
|
entry_refresh
|
|
.set_password(refresh_token)
|
|
.map_err(|e| format!("Failed to store refresh token: {}", e))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Retrieve access token from keyring
|
|
pub fn get_access_token(username: &str) -> Result<String, String> {
|
|
let entry =
|
|
Entry::new("provisioning-access", username).map_err(|e| format!("Keyring error: {}", e))?;
|
|
|
|
entry
|
|
.get_password()
|
|
.map_err(|e| format!("No token found: {}", e))
|
|
}
|
|
|
|
/// Remove tokens from keyring
|
|
pub fn remove_tokens_from_keyring(username: &str) -> Result<(), String> {
|
|
let entry_access = Entry::new("provisioning-access", username)
|
|
.map_err(|e| format!("Keyring access error: {}", e))?;
|
|
let entry_refresh = Entry::new("provisioning-refresh", username)
|
|
.map_err(|e| format!("Keyring refresh error: {}", e))?;
|
|
|
|
// Keyring 3.x uses delete_credential instead of delete_password
|
|
let _ = entry_access.delete_credential();
|
|
let _ = entry_refresh.delete_credential();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Secure password input (no echo)
|
|
|
|
/// Prompt for password without echoing to terminal
|
|
pub fn prompt_password(prompt: &str) -> Result<String, String> {
|
|
print!("{}", prompt);
|
|
io::stdout()
|
|
.flush()
|
|
.map_err(|e| format!("Flush error: {}", e))?;
|
|
|
|
rpassword::read_password().map_err(|e| format!("Password read error: {}", e))
|
|
}
|
|
|
|
// HTTP API calls
|
|
|
|
/// Send login request to control center
|
|
pub fn send_login_request(
|
|
url: &str,
|
|
username: &str,
|
|
password: &str,
|
|
) -> Result<TokenResponse, String> {
|
|
let client = Client::new();
|
|
|
|
let response = client
|
|
.post(format!("{}/auth/login", url))
|
|
.json(&LoginRequest {
|
|
username: username.to_string(),
|
|
password: password.to_string(),
|
|
})
|
|
.send()
|
|
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
|
|
|
if !response.status().is_success() {
|
|
let status = response.status();
|
|
let error_text = response
|
|
.text()
|
|
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
return Err(format!("Login failed: HTTP {} - {}", status, error_text));
|
|
}
|
|
|
|
response
|
|
.json::<TokenResponse>()
|
|
.map_err(|e| format!("Failed to parse response: {}", e))
|
|
}
|
|
|
|
/// Send logout request to control center
|
|
pub fn send_logout_request(url: &str, access_token: &str) -> Result<(), String> {
|
|
let client = Client::new();
|
|
|
|
let response = client
|
|
.post(format!("{}/auth/logout", url))
|
|
.bearer_auth(access_token)
|
|
.json(&LogoutRequest {
|
|
access_token: access_token.to_string(),
|
|
})
|
|
.send()
|
|
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
|
|
|
if !response.status().is_success() {
|
|
let status = response.status();
|
|
let error_text = response
|
|
.text()
|
|
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
return Err(format!("Logout failed: HTTP {} - {}", status, error_text));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Verify token with control center (planned for auth verify command - Agente 4)
|
|
#[allow(dead_code)]
|
|
pub fn verify_token(url: &str, token: &str) -> Result<VerifyResponse, String> {
|
|
let client = Client::new();
|
|
|
|
let response = client
|
|
.get(format!("{}/auth/verify", url))
|
|
.bearer_auth(token)
|
|
.send()
|
|
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
|
|
|
if !response.status().is_success() {
|
|
return Ok(VerifyResponse {
|
|
valid: false,
|
|
user_id: None,
|
|
username: None,
|
|
roles: None,
|
|
expires_at: None,
|
|
});
|
|
}
|
|
|
|
response
|
|
.json::<VerifyResponse>()
|
|
.map_err(|e| format!("Failed to parse response: {}", e))
|
|
}
|
|
|
|
/// List active sessions (planned for auth sessions command - Agente 5)
|
|
#[allow(dead_code)]
|
|
pub fn list_sessions(url: &str, token: &str) -> Result<Vec<SessionInfo>, String> {
|
|
let client = Client::new();
|
|
|
|
let response = client
|
|
.get(format!("{}/auth/sessions", url))
|
|
.bearer_auth(token)
|
|
.send()
|
|
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
|
|
|
if !response.status().is_success() {
|
|
let status = response.status();
|
|
return Err(format!("Failed to list sessions: HTTP {}", status));
|
|
}
|
|
|
|
response
|
|
.json::<Vec<SessionInfo>>()
|
|
.map_err(|e| format!("Failed to parse response: {}", e))
|
|
}
|
|
|
|
// MFA support
|
|
|
|
/// MFA enrollment request payload
|
|
#[derive(Serialize, Debug)]
|
|
pub struct MfaEnrollRequest {
|
|
pub mfa_type: String, // "totp" or "webauthn"
|
|
}
|
|
|
|
/// MFA enrollment response with secret and QR code
|
|
#[derive(Deserialize, Debug)]
|
|
pub struct MfaEnrollResponse {
|
|
pub secret: String,
|
|
pub qr_code_uri: String,
|
|
pub backup_codes: Vec<String>,
|
|
}
|
|
|
|
/// MFA verification request payload
|
|
#[derive(Serialize, Debug)]
|
|
pub struct MfaVerifyRequest {
|
|
pub code: String,
|
|
}
|
|
|
|
/// Send MFA enrollment request to control center
|
|
pub fn send_mfa_enroll_request(
|
|
url: &str,
|
|
access_token: &str,
|
|
mfa_type: &str,
|
|
) -> Result<MfaEnrollResponse, String> {
|
|
let client = Client::new();
|
|
|
|
let response = client
|
|
.post(format!("{}/mfa/enroll/{}", url, mfa_type))
|
|
.bearer_auth(access_token)
|
|
.json(&MfaEnrollRequest {
|
|
mfa_type: mfa_type.to_string(),
|
|
})
|
|
.send()
|
|
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
|
|
|
if !response.status().is_success() {
|
|
let status = response.status();
|
|
let error_text = response
|
|
.text()
|
|
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
return Err(format!(
|
|
"MFA enroll failed: HTTP {} - {}",
|
|
status, error_text
|
|
));
|
|
}
|
|
|
|
response
|
|
.json::<MfaEnrollResponse>()
|
|
.map_err(|e| format!("Failed to parse response: {}", e))
|
|
}
|
|
|
|
/// Send MFA verification request to control center
|
|
pub fn send_mfa_verify_request(url: &str, access_token: &str, code: &str) -> Result<bool, String> {
|
|
let client = Client::new();
|
|
|
|
let response = client
|
|
.post(format!("{}/mfa/verify", url))
|
|
.bearer_auth(access_token)
|
|
.json(&MfaVerifyRequest {
|
|
code: code.to_string(),
|
|
})
|
|
.send()
|
|
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
|
|
|
Ok(response.status().is_success())
|
|
}
|
|
|
|
/// Generate QR code for TOTP enrollment
|
|
pub fn generate_qr_code(uri: &str) -> Result<String, String> {
|
|
use qrcode::render::unicode;
|
|
use qrcode::QrCode;
|
|
|
|
let code = QrCode::new(uri).map_err(|e| format!("QR code generation failed: {}", e))?;
|
|
|
|
let qr_string = code
|
|
.render::<unicode::Dense1x2>()
|
|
.dark_color(unicode::Dense1x2::Light)
|
|
.light_color(unicode::Dense1x2::Dark)
|
|
.build();
|
|
|
|
Ok(qr_string)
|
|
}
|
|
|
|
/// Display QR code in terminal with instructions
|
|
pub fn display_qr_code(uri: &str) -> Result<(), String> {
|
|
let qr = generate_qr_code(uri)?;
|
|
println!("\n{}\n", qr);
|
|
println!("Scan this QR code with your authenticator app");
|
|
println!("Or enter this secret manually: {}", extract_secret(uri)?);
|
|
Ok(())
|
|
}
|
|
|
|
/// Extract secret from TOTP URI
|
|
fn extract_secret(uri: &str) -> Result<String, String> {
|
|
uri.split("secret=")
|
|
.nth(1)
|
|
.and_then(|s| s.split('&').next())
|
|
.ok_or("Failed to extract secret from URI".to_string())
|
|
.map(|s| s.to_string())
|
|
}
|