alopex_cli/client/
auth.rs1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4use reqwest::header::{HeaderValue, AUTHORIZATION};
5use reqwest::{ClientBuilder, RequestBuilder};
6
7use crate::profile::config::{AuthType, ServerConfig};
8
9#[derive(thiserror::Error, Debug)]
10pub enum AuthError {
11 #[error("missing authentication configuration: {0}")]
12 MissingConfig(String),
13 #[error("invalid authentication configuration: {0}")]
14 InvalidConfig(String),
15 #[error("failed to execute password command: {0}")]
16 PasswordCommand(String),
17 #[error("failed to load certificate: {0}")]
18 Certificate(String),
19}
20
21#[derive(Debug, Clone)]
22pub enum AuthConfig {
23 None,
24 Token {
25 token: String,
26 },
27 Basic {
28 username: String,
29 password_command: String,
30 },
31 MTls {
32 cert_path: PathBuf,
33 key_path: PathBuf,
34 },
35}
36
37impl AuthConfig {
38 pub fn from_server_config(config: &ServerConfig) -> Result<Self, AuthError> {
39 let auth_type = config.auth.unwrap_or(AuthType::None);
40 match auth_type {
41 AuthType::None => Ok(Self::None),
42 AuthType::Token => {
43 let token = config
44 .token
45 .clone()
46 .ok_or_else(|| AuthError::MissingConfig("token is required".to_string()))?;
47 Ok(Self::Token { token })
48 }
49 AuthType::Basic => {
50 let username = config
51 .username
52 .clone()
53 .ok_or_else(|| AuthError::MissingConfig("username is required".to_string()))?;
54 let password_command = config.password_command.clone().ok_or_else(|| {
55 AuthError::MissingConfig("password_command is required".to_string())
56 })?;
57 Ok(Self::Basic {
58 username,
59 password_command,
60 })
61 }
62 AuthType::MTls => {
63 let cert_path = config
64 .cert_path
65 .clone()
66 .ok_or_else(|| AuthError::MissingConfig("cert_path is required".to_string()))?;
67 let key_path = config
68 .key_path
69 .clone()
70 .ok_or_else(|| AuthError::MissingConfig("key_path is required".to_string()))?;
71 Ok(Self::MTls {
72 cert_path,
73 key_path,
74 })
75 }
76 }
77 }
78
79 pub fn apply_to_builder(&self, builder: ClientBuilder) -> Result<ClientBuilder, AuthError> {
80 match self {
81 AuthConfig::MTls {
82 cert_path,
83 key_path,
84 } => {
85 let identity = load_identity(cert_path, key_path)?;
86 Ok(builder.identity(identity))
87 }
88 _ => Ok(builder),
89 }
90 }
91
92 pub fn apply_to_request(&self, request: RequestBuilder) -> Result<RequestBuilder, AuthError> {
93 match self {
94 AuthConfig::None => Ok(request),
95 AuthConfig::Token { token } => {
96 let value = HeaderValue::from_str(&format!("Bearer {}", token))
97 .map_err(|err| AuthError::InvalidConfig(err.to_string()))?;
98 Ok(request.header(AUTHORIZATION, value))
99 }
100 AuthConfig::Basic {
101 username,
102 password_command,
103 } => {
104 let password = execute_password_command(password_command)?;
105 Ok(request.basic_auth(username, Some(password)))
106 }
107 AuthConfig::MTls { .. } => Ok(request),
108 }
109 }
110}
111
112fn execute_password_command(command: &str) -> Result<String, AuthError> {
113 let mut parts = command.split_whitespace();
114 let Some(program) = parts.next() else {
115 return Err(AuthError::InvalidConfig(
116 "password_command cannot be empty".to_string(),
117 ));
118 };
119 let output = Command::new(program)
120 .args(parts)
121 .output()
122 .map_err(|err| AuthError::PasswordCommand(err.to_string()))?;
123 if !output.status.success() {
124 return Err(AuthError::PasswordCommand(format!(
125 "command exited with status {}",
126 output.status
127 )));
128 }
129 let password = String::from_utf8_lossy(&output.stdout).trim().to_string();
130 if password.is_empty() {
131 return Err(AuthError::PasswordCommand(
132 "password command returned empty output".to_string(),
133 ));
134 }
135 Ok(password)
136}
137
138fn load_identity(cert_path: &Path, key_path: &Path) -> Result<reqwest::Identity, AuthError> {
139 let cert = std::fs::read(cert_path)
140 .map_err(|err| AuthError::Certificate(format!("cert read failed: {err}")))?;
141 let key = std::fs::read(key_path)
142 .map_err(|err| AuthError::Certificate(format!("key read failed: {err}")))?;
143 let mut combined = Vec::with_capacity(cert.len() + key.len() + 1);
144 combined.extend_from_slice(&cert);
145 if !combined.ends_with(b"\n") {
146 combined.push(b'\n');
147 }
148 combined.extend_from_slice(&key);
149 reqwest::Identity::from_pem(&combined).map_err(|err| AuthError::Certificate(err.to_string()))
150}