alopex_cli/client/
auth.rs

1use 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}