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>,
}
impl TokenCreateRequest {
#[must_use]
pub fn with_policies<I, P>(mut self, policies: I) -> Self
where
I: IntoIterator<Item = P>,
P: Into<String>,
{
self.policies = policies.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn without_default_policy(mut self) -> Self {
self.no_default_policy = Some(true);
self
}
pub fn with_ttl(mut self, ttl: impl Into<String>) -> Result<Self> {
let ttl = ttl.into();
crate::validation::validate_duration_parameter(&ttl, "token ttl")?;
self.ttl = Some(ttl);
Ok(self)
}
pub fn with_ttl_duration(self, ttl: std::time::Duration) -> Result<Self> {
self.with_ttl(crate::duration_to_bao_string(ttl))
}
pub fn with_explicit_max_ttl(mut self, explicit_max_ttl: impl Into<String>) -> Result<Self> {
let explicit_max_ttl = explicit_max_ttl.into();
crate::validation::validate_duration_parameter(
&explicit_max_ttl,
"token explicit_max_ttl",
)?;
self.explicit_max_ttl = Some(explicit_max_ttl);
Ok(self)
}
pub fn with_explicit_max_ttl_duration(
self,
explicit_max_ttl: std::time::Duration,
) -> Result<Self> {
self.with_explicit_max_ttl(crate::duration_to_bao_string(explicit_max_ttl))
}
pub fn with_period(mut self, period: impl Into<String>) -> Result<Self> {
let period = period.into();
crate::validation::validate_duration_parameter(&period, "token period")?;
self.period = Some(period);
Ok(self)
}
pub fn with_period_duration(self, period: std::time::Duration) -> Result<Self> {
self.with_period(crate::duration_to_bao_string(period))
}
fn validate(&self) -> Result<()> {
if let Some(ttl) = &self.ttl {
crate::validation::validate_duration_parameter(ttl, "token ttl")?;
}
if let Some(explicit_max_ttl) = &self.explicit_max_ttl {
crate::validation::validate_duration_parameter(
explicit_max_ttl,
"token explicit_max_ttl",
)?;
}
if let Some(period) = &self.period {
crate::validation::validate_duration_parameter(period, "token period")?;
}
Ok(())
}
}
#[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(Clone, Debug, Default, Deserialize, Serialize)]
pub struct TokenRole {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_policies: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub disallowed_policies: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub token_policies: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub orphan: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub renewable: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path_suffix: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_entity_aliases: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_ttl: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_max_ttl: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_explicit_max_ttl: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_period: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_num_uses: Option<u64>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub token_bound_cidrs: Vec<String>,
}
impl TokenRole {
#[must_use]
pub fn with_allowed_policies<I, P>(mut self, policies: I) -> Self
where
I: IntoIterator<Item = P>,
P: Into<String>,
{
self.allowed_policies = policies.into_iter().map(Into::into).collect();
self
}
pub fn with_token_ttl(mut self, token_ttl: impl Into<String>) -> Result<Self> {
let token_ttl = token_ttl.into();
crate::validation::validate_duration_parameter(&token_ttl, "token role token_ttl")?;
self.token_ttl = Some(token_ttl);
Ok(self)
}
fn validate(&self) -> Result<()> {
if let Some(token_ttl) = &self.token_ttl {
crate::validation::validate_duration_parameter(token_ttl, "token role token_ttl")?;
}
if let Some(token_max_ttl) = &self.token_max_ttl {
crate::validation::validate_duration_parameter(
token_max_ttl,
"token role token_max_ttl",
)?;
}
if let Some(token_explicit_max_ttl) = &self.token_explicit_max_ttl {
crate::validation::validate_duration_parameter(
token_explicit_max_ttl,
"token role token_explicit_max_ttl",
)?;
}
if let Some(token_period) = &self.token_period {
crate::validation::validate_duration_parameter(
token_period,
"token role token_period",
)?;
}
crate::validation::validate_cidr_list(&self.token_bound_cidrs, "token role bound CIDR")
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct TokenRoleList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl crate::response::ListEntries for TokenRoleList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[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> {
request.validate()?;
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> {
validate_renew_increment(increment)?;
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> {
validate_renew_increment(increment)?;
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_orphan(&self, token: &SecretString) -> Result<Empty> {
let payload = TokenPayload {
token: token.expose_secret(),
};
self.client
.request_json(Method::POST, "auth/token/revoke-orphan", 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
}
pub async fn write_role(&self, role_name: &str, role: &TokenRole) -> Result<Empty> {
role.validate()?;
let role_name = crate::path::validate_mount_path(role_name)?.join("/");
self.client
.request_json(
Method::POST,
&format!("auth/token/roles/{role_name}"),
Some(role),
)
.await
}
pub async fn read_role(&self, role_name: &str) -> Result<TokenRole> {
let role_name = crate::path::validate_mount_path(role_name)?.join("/");
let envelope: ResponseEnvelope<TokenRole> = self
.client
.request_json(
Method::GET,
&format!("auth/token/roles/{role_name}"),
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn list_roles(&self) -> Result<TokenRoleList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
let envelope: ResponseEnvelope<TokenRoleList> = self
.client
.request_json(method, "auth/token/roles", Option::<&Empty>::None)
.await?;
Ok(envelope.data)
}
pub async fn delete_role(&self, role_name: &str) -> Result<Empty> {
let role_name = crate::path::validate_mount_path(role_name)?.join("/");
self.client
.request_json_accepting(
Method::DELETE,
&format!("auth/token/roles/{role_name}"),
Option::<&Empty>::None,
&[reqwest::StatusCode::OK, reqwest::StatusCode::NO_CONTENT],
)
.await
}
pub async fn tidy(&self) -> Result<Empty> {
self.client
.request_json(Method::POST, "auth/token/tidy", Option::<&Empty>::None)
.await
}
}
fn validate_renew_increment(increment: Option<&str>) -> Result<()> {
if let Some(increment) = increment {
crate::validation::validate_duration_parameter(increment, "token renewal increment")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
use crate::response::ResponseEnvelope;
use super::{TokenAccessorList, TokenCreateRequest, TokenInfo, validate_renew_increment};
#[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_create_duration_fields_are_validated() {
let request = TokenCreateRequest::default()
.with_policies(["app-read", "infra-common"])
.without_default_policy()
.with_ttl("30m")
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(request.policies, ["app-read", "infra-common"]);
assert_eq!(request.no_default_policy, Some(true));
assert!(TokenCreateRequest::default().with_ttl("never").is_err());
assert!(TokenCreateRequest::default().with_ttl("1h\r\nbad").is_err());
assert!(
TokenCreateRequest::default()
.with_explicit_max_ttl("1h")
.is_ok()
);
assert!(TokenCreateRequest::default().with_period("60s").is_ok());
assert!(validate_renew_increment(Some("30m")).is_ok());
assert!(validate_renew_increment(Some("1 hour")).is_err());
}
#[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"));
}
}