use std::collections::BTreeMap;
use reqwest::Method;
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
use crate::{
Authenticated, Client, Error, Result,
response::{
Empty, ResponseEnvelope, deserialize_bounded_secret_string_vec,
deserialize_bounded_string_map_or_default, deserialize_bounded_string_vec,
},
};
#[derive(Debug)]
pub struct Token<'a> {
client: &'a Client<Authenticated>,
}
#[derive(Clone, Default, Serialize)]
pub struct TokenCreateRequest {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub policies: Vec<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub meta: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub explicit_max_ttl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub period: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub num_uses: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub renewable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_parent: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_default_policy: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_type: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TokenAuth {
pub client_token: SecretString,
pub accessor: SecretString,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub policies: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub token_policies: Vec<String>,
#[serde(
default,
deserialize_with = "deserialize_bounded_string_map_or_default"
)]
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub lease_duration: u64,
#[serde(default)]
pub renewable: bool,
#[serde(default)]
pub entity_id: Option<String>,
#[serde(default)]
pub token_type: Option<String>,
#[serde(default)]
pub orphan: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TokenInfo {
#[serde(default)]
pub accessor: Option<SecretString>,
#[serde(default)]
pub id: Option<SecretString>,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub entity_id: Option<String>,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub creation_time: Option<u64>,
#[serde(default)]
pub creation_ttl: Option<u64>,
#[serde(default)]
pub ttl: Option<u64>,
#[serde(default)]
pub expire_time: Option<String>,
#[serde(default)]
pub explicit_max_ttl: Option<u64>,
#[serde(default)]
pub num_uses: Option<u64>,
#[serde(default)]
pub orphan: bool,
#[serde(default)]
pub renewable: bool,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub policies: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub identity_policies: Vec<String>,
#[serde(
default,
deserialize_with = "deserialize_bounded_string_map_or_default"
)]
pub meta: BTreeMap<String, String>,
#[serde(default)]
pub token_type: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TokenAccessorList {
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
pub keys: Vec<SecretString>,
}
#[derive(Deserialize)]
struct TokenAuthEnvelope {
auth: Option<TokenAuth>,
}
#[derive(Serialize)]
struct TokenPayload<'a> {
token: &'a str,
}
#[derive(Serialize)]
struct AccessorPayload<'a> {
accessor: &'a str,
}
#[derive(Serialize)]
struct RenewPayload<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
increment: Option<&'a str>,
}
impl Client<Authenticated> {
pub fn token(&self) -> Token<'_> {
Token { client: self }
}
}
impl Token<'_> {
pub async fn create(&self, request: &TokenCreateRequest) -> Result<TokenAuth> {
self.create_at(None, request).await
}
pub async fn create_at(
&self,
role_name: Option<&str>,
request: &TokenCreateRequest,
) -> Result<TokenAuth> {
let path = match role_name {
Some(role_name) => {
let role_name = crate::path::validate_mount_path(role_name)?.join("/");
format!("auth/token/create/{role_name}")
}
None => "auth/token/create".to_owned(),
};
let envelope: TokenAuthEnvelope = self
.client
.request_json(Method::POST, &path, Some(request))
.await?;
envelope.auth.ok_or(Error::MissingField("auth"))
}
pub async fn lookup_self(&self) -> Result<TokenInfo> {
let envelope: ResponseEnvelope<TokenInfo> = self
.client
.request_json(
Method::POST,
"auth/token/lookup-self",
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn lookup(&self, token: &SecretString) -> Result<TokenInfo> {
let payload = TokenPayload {
token: token.expose_secret(),
};
let envelope: ResponseEnvelope<TokenInfo> = self
.client
.request_json(Method::POST, "auth/token/lookup", Some(&payload))
.await?;
Ok(envelope.data)
}
pub async fn lookup_accessor(&self, accessor: &SecretString) -> Result<TokenInfo> {
let payload = AccessorPayload {
accessor: accessor.expose_secret(),
};
let envelope: ResponseEnvelope<TokenInfo> = self
.client
.request_json(Method::POST, "auth/token/lookup-accessor", Some(&payload))
.await?;
Ok(envelope.data)
}
pub async fn list_accessors(&self) -> Result<TokenAccessorList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
let envelope: ResponseEnvelope<TokenAccessorList> = self
.client
.request_json(method, "auth/token/accessors", Option::<&Empty>::None)
.await?;
Ok(envelope.data)
}
pub async fn renew_self(&self, increment: Option<&str>) -> Result<TokenAuth> {
let payload = RenewPayload {
token: None,
increment,
};
let envelope: TokenAuthEnvelope = self
.client
.request_json(Method::POST, "auth/token/renew-self", Some(&payload))
.await?;
envelope.auth.ok_or(Error::MissingField("auth"))
}
pub async fn renew(&self, token: &SecretString, increment: Option<&str>) -> Result<TokenAuth> {
let payload = RenewPayload {
token: Some(token.expose_secret()),
increment,
};
let envelope: TokenAuthEnvelope = self
.client
.request_json(Method::POST, "auth/token/renew", Some(&payload))
.await?;
envelope.auth.ok_or(Error::MissingField("auth"))
}
pub async fn revoke(&self, token: &SecretString) -> Result<Empty> {
let payload = TokenPayload {
token: token.expose_secret(),
};
self.client
.request_json(Method::POST, "auth/token/revoke", Some(&payload))
.await
}
pub async fn revoke_self(&self) -> Result<Empty> {
self.client
.request_json(
Method::POST,
"auth/token/revoke-self",
Option::<&Empty>::None,
)
.await
}
pub async fn revoke_accessor(&self, accessor: &SecretString) -> Result<Empty> {
let payload = AccessorPayload {
accessor: accessor.expose_secret(),
};
self.client
.request_json(Method::POST, "auth/token/revoke-accessor", Some(&payload))
.await
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
use crate::response::ResponseEnvelope;
use super::{TokenAccessorList, TokenInfo};
#[test]
fn token_ttl_rejects_negative_values() {
let error = match serde_json::from_str::<ResponseEnvelope<TokenInfo>>(
r#"{"data":{"ttl":-1,"policies":[]}}"#,
) {
Ok(_) => panic!("negative ttl unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("invalid value"));
}
#[test]
fn token_accessor_list_is_bounded() {
let mut keys = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
keys.push(format!("accessor-{index}"));
}
let value = serde_json::json!({ "keys": keys });
let error = match serde_json::from_value::<TokenAccessorList>(value) {
Ok(_) => panic!("oversized accessor list unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
}