use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Deserializer, Serialize};
use crate::{Authenticated, Client, Error, Result, Unauthenticated, path::validate_mount_path};
#[derive(Debug)]
pub struct AppRole<'a> {
client: &'a Client<Unauthenticated>,
mount: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct LoginMetadata {
pub accessor: SecretString,
#[serde(default)]
pub policies: Vec<String>,
#[serde(default)]
pub lease_duration: u64,
#[serde(default)]
pub renewable: bool,
}
#[derive(Serialize)]
struct LoginRequest<'a> {
role_id: &'a str,
secret_id: &'a str,
}
#[derive(Deserialize)]
struct LoginResponse {
auth: Option<LoginAuth>,
}
#[derive(Deserialize)]
struct LoginAuth {
#[serde(deserialize_with = "deserialize_secret")]
client_token: SecretString,
#[serde(deserialize_with = "deserialize_secret")]
accessor: SecretString,
#[serde(default)]
policies: Vec<String>,
#[serde(default)]
lease_duration: u64,
#[serde(default)]
renewable: bool,
}
impl Client<Unauthenticated> {
pub fn approle(&self) -> Result<AppRole<'_>> {
self.approle_at("approle")
}
pub fn approle_at(&self, mount: impl Into<String>) -> Result<AppRole<'_>> {
let mount = mount.into();
let mount = validate_mount_path(&mount)?.join("/");
Ok(AppRole {
client: self,
mount,
})
}
pub async fn login_approle(
self,
role_id: SecretString,
secret_id: SecretString,
) -> Result<(Client<Authenticated>, LoginMetadata)> {
let response = self
.approle()?
.login_response(&role_id, &secret_id)
.await?
.auth
.ok_or(Error::MissingField("auth"))?;
let (token, metadata) = split_login_auth(response);
Ok((self.with_token(token), metadata))
}
}
impl AppRole<'_> {
pub async fn login(
self,
role_id: SecretString,
secret_id: SecretString,
) -> Result<(Client<Authenticated>, LoginMetadata)> {
let response = self
.login_response(&role_id, &secret_id)
.await?
.auth
.ok_or(Error::MissingField("auth"))?;
let (token, metadata) = split_login_auth(response);
Ok((
self.client.clone_without_state().with_token(token),
metadata,
))
}
async fn login_response(
&self,
role_id: &SecretString,
secret_id: &SecretString,
) -> Result<LoginResponse> {
let request = LoginRequest {
role_id: role_id.expose_secret(),
secret_id: secret_id.expose_secret(),
};
self.client
.request_json(
reqwest::Method::POST,
&format!("auth/{}/login", self.mount),
Some(&request),
)
.await
}
}
fn split_login_auth(auth: LoginAuth) -> (SecretString, LoginMetadata) {
let LoginAuth {
client_token,
accessor,
policies,
lease_duration,
renewable,
} = auth;
let metadata = LoginMetadata {
accessor,
policies,
lease_duration,
renewable,
};
(client_token, metadata)
}
fn deserialize_secret<'de, D>(deserializer: D) -> core::result::Result<SecretString, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(SecretString::from(value))
}
impl Client<Unauthenticated> {
fn clone_without_state(&self) -> Client<Unauthenticated> {
Client {
config: self.config.clone(),
http: self.http.clone(),
token: None,
_state: core::marker::PhantomData,
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
use secrecy::ExposeSecret;
use super::LoginResponse;
#[test]
fn login_auth_deserializes_secrets_into_secret_strings() {
let response: LoginResponse = serde_json::from_str(
r#"{"auth":{"client_token":"token-value","accessor":"accessor-value"}}"#,
)
.unwrap_or_else(|error| panic!("{error}"));
let auth = response.auth.unwrap_or_else(|| panic!("auth missing"));
assert_eq!(auth.client_token.expose_secret(), "token-value");
assert_eq!(auth.accessor.expose_secret(), "accessor-value");
}
}