use std::ops::Deref;
use crate::client::Warning;
use crate::proto::api::{Scope, TokenSummary, VaultStatus};
use crate::proto::ids::TokenId;
use crate::proto::path::EnvPath;
use crate::proto::tolerant::Tolerant;
use zeroize::Zeroizing;
use crate::error::Error;
use crate::token::Vault;
use crate::vault::Handle;
pub struct Environment {
vault: Vault,
path: EnvPath,
status: VaultStatus,
}
impl std::fmt::Debug for Environment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Environment")
.field("path", &self.path)
.field("vault_id", &self.vault.vault_id())
.finish()
}
}
impl Deref for Environment {
type Target = Vault;
fn deref(&self) -> &Vault {
&self.vault
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct TokenInfo {
pub id: TokenId,
pub scope: Tolerant<Scope>,
pub created_at: i64,
pub expires_at: i64,
pub only: Option<Vec<String>>,
}
pub struct MintedToken {
id: TokenId,
scope: Scope,
expires_at: i64,
token: Zeroizing<String>,
}
impl std::fmt::Debug for MintedToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MintedToken")
.field("id", &self.id)
.field("scope", &self.scope)
.field("expires_at", &self.expires_at)
.finish_non_exhaustive()
}
}
impl MintedToken {
pub fn id(&self) -> TokenId {
self.id
}
pub fn scope(&self) -> Scope {
self.scope
}
pub fn expires_at(&self) -> i64 {
self.expires_at
}
pub fn expose(&self) -> &str {
&self.token
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Revocation {
pub scope: Tolerant<Scope>,
pub rotated_to: Option<u32>,
}
impl Revocation {
pub fn forward_only(&self) -> bool {
self.rotated_to.is_none() && self.scope != Scope::Meta
}
pub fn held_keys(&self) -> String {
match self.scope.get() {
Some(scope) => held_keys(scope),
None => "keys this client cannot name (its scope is newer than this client)".to_owned(),
}
}
}
fn held_keys(scope: Scope) -> String {
let mut keys = Vec::new();
if scope.bundle_holds_vault_key() {
keys.push("the secret key");
}
if scope.bundle_holds_config_key() {
keys.push("the config key");
}
if scope.bundle_holds_secret_writer() {
keys.push("the secret writer key");
}
if scope.bundle_holds_config_writer() {
keys.push("the config writer key");
}
keys.join(", ")
}
impl Environment {
pub(crate) fn new(handle: Handle, path: EnvPath) -> Result<Environment, Error> {
let status = handle.status().ok_or_else(|| {
Error::other(format!("{path}: the vault was not opened by its owner"))
})?;
Ok(Environment {
vault: Vault::from_handle(handle),
path,
status,
})
}
pub(crate) fn handle(&self) -> &Handle {
&self.vault.inner
}
pub fn path(&self) -> &EnvPath {
&self.path
}
pub fn status_at_open(&self) -> &VaultStatus {
&self.status
}
fn visible_tokens(&self) -> Result<&[TokenSummary], Error> {
self.status.tokens.as_deref().ok_or_else(|| {
Error::other(format!(
"{}: the server did not send this vault's token list; \
ask for status as the owner",
self.path
))
})
}
pub fn tokens(&self) -> Result<Vec<TokenInfo>, Error> {
let tokens = self.visible_tokens()?.to_vec();
let names: std::collections::HashMap<_, _> = self
.handle()
.list()?
.into_iter()
.map(|i| (i.hmac, i.name))
.collect();
Ok(tokens
.into_iter()
.map(|t| TokenInfo {
id: t.token_id,
scope: t.scope,
created_at: t.created_at,
expires_at: t.expires_at,
only: t.allow_list.map(|l| {
l.iter()
.map(|h| names.get(h).cloned().unwrap_or_else(|| "?".into()))
.collect()
}),
})
.collect())
}
pub fn mint(&self, scope: Scope, ttl_secs: u64, only: &[String]) -> Result<MintedToken, Error> {
if let Some(t) = self.visible_tokens()?.iter().find(|t| !t.scope.is_known()) {
return Err(Error::unsupported(format!(
"{}: token {} has scope {:?}, which this client does not know; upgrade before minting",
self.path,
t.token_id,
t.scope.unknown().unwrap_or("?")
)));
}
let h = self.handle();
let allow = (!only.is_empty()).then(|| only.iter().map(|n| h.hmac(n)).collect());
let (token, registered) = h.mint(scope, ttl_secs, allow)?;
Ok(MintedToken {
id: registered.token_id,
scope,
expires_at: registered.expires_at,
token: token.token_string(),
})
}
pub fn revoke(&self, id: &TokenId, rotate: bool) -> Result<Revocation, Error> {
let scope = self
.status
.tokens
.as_ref()
.and_then(|ts| ts.iter().find(|t| t.token_id == *id))
.map(|t| t.scope.clone())
.ok_or_else(|| Error::not_found(format!("{} has no token {id}", self.path)))?;
let h = self.handle();
if rotate {
let generation = h.rotate(&[*id])?;
return Ok(Revocation {
scope,
rotated_to: Some(generation),
});
}
h.revoke(id)?;
let revocation = Revocation {
scope,
rotated_to: None,
};
if revocation.forward_only() {
h.api().events().warning(&Warning::ForwardOnlyRevocation {
path: self.path.to_string(),
token: id.to_hex(),
scope: revocation.scope.to_string(),
held: revocation.held_keys(),
});
}
Ok(revocation)
}
pub fn rotate(&self) -> Result<u32, Error> {
self.handle().rotate(&[])
}
}