use std::fmt;
use serde::{Deserialize, Serialize};
use crate::client::GuacamoleClient;
use crate::error::Result;
use crate::validation::{validate_data_source, validate_token};
#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AuthResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub available_data_sources: Option<Vec<String>>,
}
impl fmt::Debug for AuthResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuthResponse")
.field("auth_token", &"<redacted>")
.field("username", &self.username)
.field("data_source", &self.data_source)
.field("available_data_sources", &self.available_data_sources)
.finish()
}
}
impl GuacamoleClient {
pub async fn login(&mut self, username: &str, password: &str) -> Result<AuthResponse> {
let url = self.url_unauth("/api/tokens");
let response = self
.http
.post(&url)
.form(&[("username", username), ("password", password)])
.send()
.await?;
let auth: AuthResponse = Self::parse_response(response, "authentication").await?;
if let Some(ref token) = auth.auth_token {
validate_token(token)?;
}
if let Some(ref ds) = auth.data_source {
validate_data_source(ds)?;
}
self.auth_token.clone_from(&auth.auth_token);
self.data_source.clone_from(&auth.data_source);
Ok(auth)
}
pub async fn logout(&mut self) -> Result<()> {
let token = self.require_token()?.to_owned();
let url = self.url_unauth(&format!("/api/tokens/{token}"));
let response = self.http.delete(&url).send().await?;
Self::handle_error(response, "logout").await?;
self.auth_token = None;
self.data_source = None;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_response_serde_roundtrip() {
let auth = AuthResponse {
auth_token: Some("ABCDEF".to_string()),
username: Some("guacadmin".to_string()),
data_source: Some("mysql".to_string()),
available_data_sources: Some(vec!["mysql".to_string(), "postgresql".to_string()]),
};
let json = serde_json::to_string(&auth).unwrap();
let deserialized: AuthResponse = serde_json::from_str(&json).unwrap();
assert_eq!(auth, deserialized);
}
#[test]
fn auth_response_camel_case_keys() {
let auth = AuthResponse {
auth_token: Some("tok".to_string()),
data_source: Some("mysql".to_string()),
available_data_sources: Some(vec!["mysql".to_string()]),
..Default::default()
};
let json = serde_json::to_value(&auth).unwrap();
assert!(json.get("authToken").is_some());
assert!(json.get("dataSource").is_some());
assert!(json.get("availableDataSources").is_some());
}
#[test]
fn auth_response_debug_redacts_token() {
let auth = AuthResponse {
auth_token: Some("super-secret-token".to_string()),
username: Some("guacadmin".to_string()),
..Default::default()
};
let debug_output = format!("{auth:?}");
assert!(
!debug_output.contains("super-secret-token"),
"Debug must not contain the auth token"
);
assert!(debug_output.contains("<redacted>"));
assert!(debug_output.contains("guacadmin"));
}
#[test]
fn auth_response_skip_none_fields() {
let auth = AuthResponse::default();
let json = serde_json::to_value(&auth).unwrap();
let obj = json.as_object().unwrap();
assert!(obj.is_empty());
}
#[test]
fn deserialize_from_api_json() {
let json = r#"{
"authToken": "168F8D0A2D68247F30B7E2E01187AEE2CF82186D",
"username": "guacadmin",
"dataSource": "mysql",
"availableDataSources": ["mysql", "mysql-shared"]
}"#;
let auth: AuthResponse = serde_json::from_str(json).unwrap();
assert_eq!(
auth.auth_token.as_deref(),
Some("168F8D0A2D68247F30B7E2E01187AEE2CF82186D")
);
assert_eq!(auth.username.as_deref(), Some("guacadmin"));
assert_eq!(auth.data_source.as_deref(), Some("mysql"));
assert_eq!(
auth.available_data_sources,
Some(vec!["mysql".to_string(), "mysql-shared".to_string()])
);
}
#[test]
fn auth_response_unknown_fields_ignored() {
let json = r#"{"authToken": "tok", "unknownField": 42}"#;
let auth: AuthResponse = serde_json::from_str(json).unwrap();
assert_eq!(auth.auth_token.as_deref(), Some("tok"));
}
}