use core::{fmt, marker::PhantomData};
use std::{collections::BTreeMap, net::IpAddr};
use reqwest::{
Method, StatusCode,
header::{HeaderName, HeaderValue},
};
use secrecy::{ExposeSecret, SecretString};
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{Error as DeError, IgnoredAny, MapAccess, SeqAccess, Visitor},
};
use crate::{
Authenticated, Client, Error, Result, Unauthenticated,
path::{validate_mount_path, validate_secret_path},
response::{
Empty, ResponseEnvelope, WrapInfo, deserialize_bounded_secret_string_vec,
deserialize_bounded_string_map, deserialize_bounded_string_vec,
deserialize_optional_bounded_string_map, deserialize_optional_bounded_string_vec,
},
};
#[derive(Debug)]
pub struct Sys<'a, State> {
client: &'a Client<State>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Health {
pub initialized: bool,
pub sealed: bool,
#[serde(default)]
pub standby: bool,
pub version: String,
#[serde(default)]
pub cluster_name: Option<String>,
#[serde(default)]
pub cluster_id: Option<String>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub struct InitStatus {
pub initialized: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SealStatus {
#[serde(rename = "type")]
pub seal_type: String,
pub initialized: bool,
pub sealed: bool,
#[serde(default)]
pub n: Option<u64>,
#[serde(default)]
pub t: Option<u64>,
#[serde(default)]
pub progress: Option<u64>,
pub version: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UnsealStatus {
pub sealed: bool,
#[serde(default)]
pub n: Option<u64>,
#[serde(default)]
pub t: Option<u64>,
#[serde(default)]
pub progress: Option<u64>,
pub version: String,
#[serde(default)]
pub cluster_name: Option<String>,
#[serde(default)]
pub cluster_id: Option<String>,
}
#[cfg(feature = "operator-ops")]
#[derive(Clone, Debug, Default, Serialize)]
pub struct OperatorInitRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub secret_shares: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secret_threshold: Option<u8>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub pgp_keys: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub root_token_pgp_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recovery_shares: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recovery_threshold: Option<u8>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub recovery_pgp_keys: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stored_shares: Option<u8>,
}
#[cfg(feature = "operator-ops")]
#[derive(Clone, Deserialize)]
pub struct OperatorInitResponse {
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
pub keys: Vec<SecretString>,
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
pub keys_base64: Vec<SecretString>,
pub root_token: SecretString,
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
pub recovery_keys: Vec<SecretString>,
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
pub recovery_keys_base64: Vec<SecretString>,
}
#[cfg(feature = "operator-ops")]
impl fmt::Debug for OperatorInitResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OperatorInitResponse")
.field("keys_count", &self.keys.len())
.field("keys_base64_count", &self.keys_base64.len())
.field("root_token", &"<redacted>")
.field("recovery_keys_count", &self.recovery_keys.len())
.field(
"recovery_keys_base64_count",
&self.recovery_keys_base64.len(),
)
.finish()
}
}
#[cfg(feature = "operator-ops")]
#[derive(Clone)]
pub struct OperatorUnsealRequest {
pub key: SecretString,
pub reset: Option<bool>,
pub migrate: Option<bool>,
}
#[cfg(feature = "operator-ops")]
impl OperatorUnsealRequest {
pub fn new(key: SecretString) -> Self {
Self {
key,
reset: None,
migrate: None,
}
}
}
#[cfg(feature = "operator-ops")]
impl fmt::Debug for OperatorUnsealRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OperatorUnsealRequest")
.field("key", &"<redacted>")
.field("reset", &self.reset)
.field("migrate", &self.migrate)
.finish()
}
}
#[cfg(feature = "operator-ops")]
#[derive(Clone, Debug, Default, Serialize)]
pub struct OperatorKeySharesRequest {
pub secret_shares: u8,
pub secret_threshold: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub stored_shares: Option<u8>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub pgp_keys: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub backup: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_verification: Option<bool>,
}
#[cfg(feature = "operator-ops")]
impl OperatorKeySharesRequest {
pub fn new(secret_shares: u8, secret_threshold: u8) -> Result<Self> {
validate_key_share_options(secret_shares, secret_threshold)?;
Ok(Self {
secret_shares,
secret_threshold,
..Self::default()
})
}
}
#[cfg(feature = "operator-ops")]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct OperatorKeySharesStatus {
#[serde(default)]
pub started: bool,
#[serde(default)]
pub nonce: Option<String>,
#[serde(default)]
pub t: Option<u64>,
#[serde(default)]
pub n: Option<u64>,
#[serde(default)]
pub progress: Option<u64>,
#[serde(default)]
pub required: Option<u64>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub pgp_fingerprints: Vec<String>,
#[serde(default)]
pub backup: bool,
#[serde(default)]
pub verification_required: bool,
}
#[cfg(feature = "operator-ops")]
#[derive(Clone)]
pub struct OperatorKeyShareUpdateRequest {
pub key: SecretString,
pub nonce: String,
}
#[cfg(feature = "operator-ops")]
impl OperatorKeyShareUpdateRequest {
pub fn new(key: SecretString, nonce: impl Into<String>) -> Self {
Self {
key,
nonce: nonce.into(),
}
}
}
#[cfg(feature = "operator-ops")]
impl fmt::Debug for OperatorKeyShareUpdateRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OperatorKeyShareUpdateRequest")
.field("key", &"<redacted>")
.field("nonce", &self.nonce)
.finish()
}
}
#[cfg(feature = "operator-ops")]
#[derive(Clone, Deserialize)]
pub struct OperatorKeyShareUpdateResponse {
#[serde(default)]
pub complete: bool,
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
pub keys: Vec<SecretString>,
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
pub keys_base64: Vec<SecretString>,
#[serde(default)]
pub nonce: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub pgp_fingerprints: Vec<String>,
#[serde(default)]
pub backup: bool,
#[serde(default)]
pub verification_required: bool,
#[serde(default)]
pub verification_nonce: Option<String>,
#[serde(default)]
pub progress: Option<u64>,
#[serde(default)]
pub required: Option<u64>,
}
#[cfg(feature = "operator-ops")]
impl fmt::Debug for OperatorKeyShareUpdateResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OperatorKeyShareUpdateResponse")
.field("complete", &self.complete)
.field("keys_count", &self.keys.len())
.field("keys_base64_count", &self.keys_base64.len())
.field("nonce", &self.nonce)
.field("pgp_fingerprints", &self.pgp_fingerprints)
.field("backup", &self.backup)
.field("verification_required", &self.verification_required)
.field("verification_nonce", &self.verification_nonce)
.field("progress", &self.progress)
.field("required", &self.required)
.finish()
}
}
#[cfg(feature = "operator-ops")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OperatorRotateTarget {
Root,
Recovery,
}
#[cfg(feature = "operator-ops")]
impl OperatorRotateTarget {
fn path_segment(self) -> &'static str {
match self {
Self::Root => "root",
Self::Recovery => "recovery",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DevBootstrapOptions {
pub secret_shares: u8,
pub secret_threshold: u8,
}
impl DevBootstrapOptions {
pub fn new(secret_shares: u8, secret_threshold: u8) -> Result<Self> {
validate_dev_bootstrap_options(secret_shares, secret_threshold)?;
Ok(Self {
secret_shares,
secret_threshold,
})
}
pub const fn single_key() -> Self {
Self {
secret_shares: 1,
secret_threshold: 1,
}
}
}
impl Default for DevBootstrapOptions {
fn default() -> Self {
Self::single_key()
}
}
pub struct DevBootstrap {
pub client: Client<Authenticated>,
pub root_token: SecretString,
pub unseal_keys: Vec<SecretString>,
pub unseal_keys_base64: Vec<SecretString>,
pub unseal_status: UnsealStatus,
}
impl fmt::Debug for DevBootstrap {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DevBootstrap")
.field("client", &self.client)
.field("root_token", &"<redacted>")
.field("unseal_key_count", &self.unseal_keys.len())
.field("unseal_key_base64_count", &self.unseal_keys_base64.len())
.field("unseal_status", &self.unseal_status)
.finish()
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct MountInfo {
#[serde(rename = "type")]
pub backend_type: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub accessor: Option<SecretString>,
#[serde(default, deserialize_with = "deserialize_null_default")]
pub config: MountConfig,
#[serde(default, deserialize_with = "deserialize_optional_bounded_string_map")]
pub options: Option<BTreeMap<String, String>>,
#[serde(default)]
pub local: bool,
#[serde(default)]
pub seal_wrap: bool,
#[serde(default)]
pub external_entropy_access: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct MountConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_lease_ttl: Option<LeaseDuration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_lease_ttl: Option<LeaseDuration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub force_no_cache: Option<bool>,
#[serde(
default,
deserialize_with = "deserialize_optional_bounded_string_vec",
skip_serializing_if = "Option::is_none"
)]
pub audit_non_hmac_request_keys: Option<Vec<String>>,
#[serde(
default,
deserialize_with = "deserialize_optional_bounded_string_vec",
skip_serializing_if = "Option::is_none"
)]
pub audit_non_hmac_response_keys: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub listing_visibility: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_optional_bounded_string_vec",
skip_serializing_if = "Option::is_none"
)]
pub passthrough_request_headers: Option<Vec<String>>,
#[serde(
default,
deserialize_with = "deserialize_optional_bounded_string_vec",
skip_serializing_if = "Option::is_none"
)]
pub allowed_response_headers: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plugin_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_lockout_config: Option<UserLockoutConfig>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LeaseDuration {
Seconds(u64),
Duration(String),
}
impl Serialize for LeaseDuration {
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Seconds(seconds) => serializer.serialize_u64(*seconds),
Self::Duration(duration) => serializer.serialize_str(duration),
}
}
}
impl<'de> Deserialize<'de> for LeaseDuration {
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(LeaseDurationVisitor)
}
}
struct LeaseDurationVisitor;
impl Visitor<'_> for LeaseDurationVisitor {
type Value = LeaseDuration;
fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str("a non-negative second count or a duration string")
}
fn visit_u64<E>(self, value: u64) -> core::result::Result<Self::Value, E> {
Ok(LeaseDuration::Seconds(value))
}
fn visit_i64<E>(self, value: i64) -> core::result::Result<Self::Value, E>
where
E: DeError,
{
u64::try_from(value)
.map(LeaseDuration::Seconds)
.map_err(|_| E::custom("duration seconds must not be negative"))
}
fn visit_str<E>(self, value: &str) -> core::result::Result<Self::Value, E>
where
E: DeError,
{
crate::validation::validate_duration_string(value, true)
.then(|| LeaseDuration::Duration(value.to_owned()))
.ok_or_else(|| E::custom("invalid duration string"))
}
fn visit_string<E>(self, value: String) -> core::result::Result<Self::Value, E>
where
E: DeError,
{
crate::validation::validate_duration_string(&value, true)
.then_some(LeaseDuration::Duration(value))
.ok_or_else(|| E::custom("invalid duration string"))
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct UserLockoutConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lockout_threshold: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lockout_duration: Option<LeaseDuration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lockout_counter_reset_duration: Option<LeaseDuration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lockout_disable: Option<bool>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct MountEnableRequest {
#[serde(rename = "type")]
pub backend_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<MountConfig>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub options: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub local: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seal_wrap: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_entropy_access: Option<bool>,
}
impl MountEnableRequest {
pub fn new(backend_type: impl Into<String>) -> Self {
Self {
backend_type: backend_type.into(),
..Self::default()
}
}
pub fn kv2() -> Self {
let mut options = BTreeMap::new();
options.insert("version".to_owned(), "2".to_owned());
Self {
backend_type: "kv".to_owned(),
options,
..Self::default()
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_default_lease_ttl(mut self, ttl: impl Into<String>) -> Result<Self> {
let ttl = ttl.into();
crate::validation::validate_duration_parameter(&ttl, "mount default_lease_ttl")?;
self.config
.get_or_insert_with(MountConfig::default)
.default_lease_ttl = Some(LeaseDuration::Duration(ttl));
Ok(self)
}
pub fn with_max_lease_ttl(mut self, ttl: impl Into<String>) -> Result<Self> {
let ttl = ttl.into();
crate::validation::validate_duration_parameter(&ttl, "mount max_lease_ttl")?;
self.config
.get_or_insert_with(MountConfig::default)
.max_lease_ttl = Some(LeaseDuration::Duration(ttl));
Ok(self)
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct AuthEnableRequest {
#[serde(rename = "type")]
pub backend_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<MountConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub local: Option<bool>,
}
impl AuthEnableRequest {
pub fn new(backend_type: impl Into<String>) -> Self {
Self {
backend_type: backend_type.into(),
..Self::default()
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_default_lease_ttl(mut self, ttl: impl Into<String>) -> Result<Self> {
let ttl = ttl.into();
crate::validation::validate_duration_parameter(&ttl, "auth default_lease_ttl")?;
self.config
.get_or_insert_with(MountConfig::default)
.default_lease_ttl = Some(LeaseDuration::Duration(ttl));
Ok(self)
}
pub fn with_max_lease_ttl(mut self, ttl: impl Into<String>) -> Result<Self> {
let ttl = ttl.into();
crate::validation::validate_duration_parameter(&ttl, "auth max_lease_ttl")?;
self.config
.get_or_insert_with(MountConfig::default)
.max_lease_ttl = Some(LeaseDuration::Duration(ttl));
Ok(self)
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct WrappingLookup {
#[serde(default)]
pub creation_time: Option<String>,
#[serde(default)]
pub creation_path: Option<String>,
#[serde(default)]
pub creation_ttl: u64,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PolicyList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub policies: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PolicyInfo {
pub name: String,
pub rules: String,
#[serde(default)]
pub modified: Option<String>,
#[serde(default)]
pub version: Option<u64>,
#[serde(default)]
pub cas_required: bool,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PolicyWriteRequest {
pub policy: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiration: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cas: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cas_required: Option<bool>,
}
impl PolicyWriteRequest {
pub fn new(policy: impl Into<String>) -> Self {
Self {
policy: policy.into(),
..Self::default()
}
}
#[must_use]
pub fn with_ttl(mut self, ttl: impl Into<String>) -> Self {
self.ttl = Some(ttl.into());
self
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Capabilities {
pub capabilities: Vec<String>,
#[serde(flatten)]
pub by_path: BTreeMap<String, Vec<String>>,
}
impl<'de> Deserialize<'de> for Capabilities {
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(CapabilitiesVisitor)
}
}
struct CapabilitiesVisitor;
impl<'de> Visitor<'de> for CapabilitiesVisitor {
type Value = Capabilities;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a bounded OpenBao capabilities object")
}
fn visit_map<A>(self, mut map: A) -> core::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut capabilities = None;
let mut by_path = BTreeMap::new();
while let Some(key) = map.next_key::<String>()? {
if key == "capabilities" {
if capabilities.is_some() {
return Err(A::Error::custom("duplicate capabilities field"));
}
capabilities = Some(map.next_value::<BoundedStringList>()?.0);
continue;
}
if by_path.len() >= crate::response::MAX_RESPONSE_STRINGS {
let _ignored = map.next_value::<IgnoredAny>()?;
return Err(A::Error::custom(
"OpenBao capabilities map exceeds item limit",
));
}
by_path.insert(key, map.next_value::<BoundedStringList>()?.0);
}
Ok(Capabilities {
capabilities: capabilities.unwrap_or_default(),
by_path,
})
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct AuditDevice {
#[serde(rename = "type")]
pub backend_type: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_map")]
pub options: BTreeMap<String, String>,
#[serde(default)]
pub local: bool,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct AuditEnableRequest {
#[serde(rename = "type")]
pub backend_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub options: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub local: Option<bool>,
}
impl AuditEnableRequest {
pub fn new(backend_type: impl Into<String>) -> Self {
Self {
backend_type: backend_type.into(),
..Self::default()
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct AuditHash {
pub hash: String,
}
#[derive(Clone, Deserialize)]
pub struct LeaseLookup {
pub id: SecretString,
pub issue_time: String,
pub expire_time: String,
#[serde(default)]
pub last_renewal: Option<String>,
#[serde(default)]
pub renewable: bool,
#[serde(default)]
pub ttl: u64,
}
impl fmt::Debug for LeaseLookup {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LeaseLookup")
.field("id", &"<redacted>")
.field("issue_time", &self.issue_time)
.field("expire_time", &self.expire_time)
.field("last_renewal", &self.last_renewal)
.field("renewable", &self.renewable)
.field("ttl", &self.ttl)
.finish()
}
}
#[derive(Clone)]
pub struct LeaseRenewal {
pub lease_id: SecretString,
pub lease_duration: u64,
pub renewable: bool,
}
impl fmt::Debug for LeaseRenewal {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LeaseRenewal")
.field("lease_id", &"<redacted>")
.field("lease_duration", &self.lease_duration)
.field("renewable", &self.renewable)
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginType {
Auth,
Database,
Secret,
}
impl PluginType {
fn as_path_segment(self) -> &'static str {
match self {
Self::Auth => "auth",
Self::Database => "database",
Self::Secret => "secret",
}
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PluginCatalog {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub auth: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub database: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub secret: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_plugin_detail_vec")]
pub detailed: Vec<PluginDetail>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PluginDetail {
pub name: String,
#[serde(rename = "type")]
pub plugin_type: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub builtin: bool,
#[serde(default)]
pub deprecation_status: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PluginList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
#[derive(Clone)]
pub struct PluginRegisterRequest {
pub version: Option<String>,
pub sha256: String,
pub command: String,
pub args: Vec<SecretString>,
pub env: Vec<SecretString>,
pub oci: Option<bool>,
}
impl fmt::Debug for PluginRegisterRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PluginRegisterRequest")
.field("version", &self.version)
.field("sha256", &self.sha256)
.field("command", &self.command)
.field("args", &format_args!("<{} redacted>", self.args.len()))
.field("env", &format_args!("<{} redacted>", self.env.len()))
.field("oci", &self.oci)
.finish()
}
}
#[derive(Clone, Deserialize)]
pub struct PluginInfo {
pub name: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub builtin: bool,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub sha256: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
pub args: Vec<SecretString>,
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
pub env: Vec<SecretString>,
#[serde(default)]
pub deprecation_status: Option<String>,
}
impl fmt::Debug for PluginInfo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PluginInfo")
.field("name", &self.name)
.field("version", &self.version)
.field("builtin", &self.builtin)
.field("command", &self.command)
.field("sha256", &self.sha256)
.field("args", &format_args!("<{} redacted>", self.args.len()))
.field("env", &format_args!("<{} redacted>", self.env.len()))
.field("deprecation_status", &self.deprecation_status)
.finish()
}
}
#[derive(Clone, Debug, Default)]
pub struct PluginReloadRequest {
pub plugin: Option<String>,
pub mounts: Vec<String>,
pub scope: Option<String>,
}
#[derive(Serialize)]
struct WrappingTokenPayload<'a> {
token: &'a str,
}
#[derive(Serialize)]
struct AuditHashPayload<'a> {
input: &'a str,
}
#[derive(Serialize)]
struct LeaseLookupPayload<'a> {
lease_id: &'a str,
}
#[derive(Serialize)]
struct LeaseRenewPayload<'a> {
lease_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
increment: Option<u64>,
}
#[derive(Serialize)]
struct LeaseRevokePayload<'a> {
lease_id: &'a str,
}
#[derive(Serialize)]
struct PluginRegisterPayload<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<&'a str>,
sha256: &'a str,
command: &'a str,
#[serde(skip_serializing_if = "Vec::is_empty")]
args: Vec<&'a str>,
#[serde(skip_serializing_if = "Vec::is_empty")]
env: Vec<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
oci: Option<bool>,
}
#[derive(Serialize)]
struct PluginReloadPayload<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
plugin: Option<&'a str>,
#[serde(skip_serializing_if = "Vec::is_empty")]
mounts: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
scope: Option<&'a str>,
}
#[derive(Serialize)]
struct CapabilitiesPayload<'a> {
paths: &'a [String],
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
accessor: Option<&'a str>,
}
#[derive(Serialize)]
struct InitPayload {
secret_shares: u8,
secret_threshold: u8,
}
#[derive(Deserialize)]
struct InitResponse {
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
keys: Vec<SecretString>,
#[serde(default, deserialize_with = "deserialize_bounded_secret_string_vec")]
keys_base64: Vec<SecretString>,
root_token: SecretString,
}
#[derive(Serialize)]
struct UnsealPayload<'a> {
key: &'a str,
}
#[cfg(feature = "operator-ops")]
#[derive(Serialize)]
struct OperatorUnsealPayload<'a> {
key: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
reset: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
migrate: Option<bool>,
}
#[cfg(feature = "operator-ops")]
#[derive(Serialize)]
struct OperatorKeyShareUpdatePayload<'a> {
key: &'a str,
nonce: &'a str,
}
impl<State> Client<State> {
pub fn sys(&self) -> Sys<'_, State> {
Sys { client: self }
}
}
impl<State> Sys<'_, State> {
pub async fn init_status(&self) -> Result<InitStatus> {
self.client
.request_json(Method::GET, "sys/init", Option::<&Empty>::None)
.await
}
pub async fn health(&self) -> Result<Health> {
self.client
.request_json_accepting(
Method::GET,
"sys/health",
Option::<&Empty>::None,
&[
StatusCode::OK,
StatusCode::NO_CONTENT,
StatusCode::TOO_MANY_REQUESTS,
StatusCode::NOT_IMPLEMENTED,
StatusCode::SERVICE_UNAVAILABLE,
openbao_status(472)?,
openbao_status(473)?,
],
)
.await
}
pub async fn seal_status(&self) -> Result<SealStatus> {
self.client
.request_json(Method::GET, "sys/seal-status", Option::<&Empty>::None)
.await
}
}
impl Sys<'_, Unauthenticated> {
#[cfg(feature = "operator-ops")]
pub async fn operator_init(
&self,
request: &OperatorInitRequest,
) -> Result<OperatorInitResponse> {
if let (Some(shares), Some(threshold)) = (request.secret_shares, request.secret_threshold) {
validate_key_share_options(shares, threshold)?;
}
if let (Some(shares), Some(threshold)) =
(request.recovery_shares, request.recovery_threshold)
{
validate_key_share_options(shares, threshold)?;
}
self.client
.request_json(Method::POST, "sys/init", Some(request))
.await
}
#[cfg(feature = "operator-ops")]
pub async fn operator_unseal(&self, request: &OperatorUnsealRequest) -> Result<UnsealStatus> {
self.client
.request_json(
Method::POST,
"sys/unseal",
Some(&OperatorUnsealPayload {
key: request.key.expose_secret(),
reset: request.reset,
migrate: request.migrate,
}),
)
.await
}
pub async fn bootstrap_dev(&self, options: &DevBootstrapOptions) -> Result<DevBootstrap> {
validate_dev_bootstrap_options(options.secret_shares, options.secret_threshold)?;
require_loopback_dev_target(self.client)?;
let init_status = self.init_status().await?;
if init_status.initialized {
return Err(Error::InvalidParameter(
"dev bootstrap refuses to run against an already initialized OpenBao instance"
.into(),
));
}
let init_response: InitResponse = self
.client
.request_json(
Method::POST,
"sys/init",
Some(&InitPayload {
secret_shares: options.secret_shares,
secret_threshold: options.secret_threshold,
}),
)
.await?;
if init_response.root_token.expose_secret().is_empty() {
return Err(Error::MissingField("root_token"));
}
if init_response.keys.len() < usize::from(options.secret_threshold) {
return Err(Error::MissingField("keys"));
}
let mut unseal_status = None;
for key in init_response
.keys
.iter()
.take(usize::from(options.secret_threshold))
{
let status = self.unseal_once(key).await?;
let sealed = status.sealed;
unseal_status = Some(status);
if !sealed {
break;
}
}
let unseal_status = unseal_status.ok_or(Error::MissingField("unseal status"))?;
if unseal_status.sealed {
return Err(Error::Decode(
"OpenBao remained sealed after submitting the configured dev threshold".into(),
));
}
let client = Client {
config: self.client.config.clone(),
http: self.client.http.clone(),
sensitive_http: self.client.sensitive_http.clone(),
token: None,
token_header: None,
token_header_error: None,
_state: PhantomData,
}
.try_with_token(init_response.root_token.clone())?;
Ok(DevBootstrap {
client,
root_token: init_response.root_token,
unseal_keys: init_response.keys,
unseal_keys_base64: init_response.keys_base64,
unseal_status,
})
}
async fn unseal_once(&self, key: &SecretString) -> Result<UnsealStatus> {
self.client
.request_json(
Method::POST,
"sys/unseal",
Some(&UnsealPayload {
key: key.expose_secret(),
}),
)
.await
}
}
impl Sys<'_, Authenticated> {
#[cfg(feature = "operator-ops")]
pub async fn operator_seal(&self) -> Result<Empty> {
self.client
.request_json(Method::PUT, "sys/seal", Option::<&Empty>::None)
.await
}
#[cfg(feature = "operator-ops")]
pub async fn operator_rotate_keyring(&self) -> Result<Empty> {
self.client
.request_json(Method::POST, "sys/rotate/keyring", Option::<&Empty>::None)
.await
}
#[cfg(feature = "operator-ops")]
pub async fn operator_rekey_status(&self) -> Result<OperatorKeySharesStatus> {
self.client
.request_json(Method::GET, "sys/rekey/init", Option::<&Empty>::None)
.await
}
#[cfg(feature = "operator-ops")]
pub async fn operator_rekey_start(
&self,
request: &OperatorKeySharesRequest,
) -> Result<OperatorKeySharesStatus> {
validate_key_share_options(request.secret_shares, request.secret_threshold)?;
self.client
.request_json(Method::POST, "sys/rekey/init", Some(request))
.await
}
#[cfg(feature = "operator-ops")]
pub async fn operator_rekey_cancel(&self) -> Result<Empty> {
self.client
.request_json(Method::DELETE, "sys/rekey/init", Option::<&Empty>::None)
.await
}
#[cfg(feature = "operator-ops")]
pub async fn operator_rekey_update(
&self,
request: &OperatorKeyShareUpdateRequest,
) -> Result<OperatorKeyShareUpdateResponse> {
self.client
.request_json(
Method::POST,
"sys/rekey/update",
Some(&OperatorKeyShareUpdatePayload {
key: request.key.expose_secret(),
nonce: &request.nonce,
}),
)
.await
}
#[cfg(feature = "operator-ops")]
pub async fn operator_rotate_status(
&self,
target: OperatorRotateTarget,
) -> Result<OperatorKeySharesStatus> {
self.client
.request_json(
Method::GET,
&rotate_init_path(target),
Option::<&Empty>::None,
)
.await
}
#[cfg(feature = "operator-ops")]
pub async fn operator_rotate_start(
&self,
target: OperatorRotateTarget,
request: &OperatorKeySharesRequest,
) -> Result<OperatorKeySharesStatus> {
validate_key_share_options(request.secret_shares, request.secret_threshold)?;
self.client
.request_json(Method::POST, &rotate_init_path(target), Some(request))
.await
}
#[cfg(feature = "operator-ops")]
pub async fn operator_rotate_cancel(&self, target: OperatorRotateTarget) -> Result<Empty> {
self.client
.request_json(
Method::DELETE,
&rotate_init_path(target),
Option::<&Empty>::None,
)
.await
}
#[cfg(feature = "operator-ops")]
pub async fn operator_rotate_update(
&self,
target: OperatorRotateTarget,
request: &OperatorKeyShareUpdateRequest,
) -> Result<OperatorKeyShareUpdateResponse> {
self.client
.request_json(
Method::POST,
&rotate_update_path(target),
Some(&OperatorKeyShareUpdatePayload {
key: request.key.expose_secret(),
nonce: &request.nonce,
}),
)
.await
}
pub async fn list_mounts(&self) -> Result<BTreeMap<String, MountInfo>> {
let envelope: ResponseEnvelope<MountInfoMap> = self
.client
.request_json(Method::GET, "sys/mounts", Option::<&Empty>::None)
.await?;
Ok(envelope.data.0)
}
pub async fn read_mount(&self, mount_path: &str) -> Result<MountInfo> {
let envelope: ResponseEnvelope<MountInfo> = self
.client
.request_json(
Method::GET,
&sys_path("sys/mounts", mount_path, None)?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn enable_mount(
&self,
mount_path: &str,
request: &MountEnableRequest,
) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&sys_path("sys/mounts", mount_path, None)?,
Some(request),
)
.await
}
pub async fn enable_kv2(&self, mount_path: &str, description: Option<&str>) -> Result<Empty> {
let mut request = MountEnableRequest::kv2();
if let Some(description) = description {
request.description = Some(description.to_owned());
}
self.enable_mount(mount_path, &request).await
}
pub async fn disable_mount(&self, mount_path: &str) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&sys_path("sys/mounts", mount_path, None)?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn read_mount_tune(&self, mount_path: &str) -> Result<MountConfig> {
self.client
.request_json(
Method::GET,
&sys_path("sys/mounts", mount_path, Some("tune"))?,
Option::<&Empty>::None,
)
.await
}
pub async fn tune_mount(&self, mount_path: &str, config: &MountConfig) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&sys_path("sys/mounts", mount_path, Some("tune"))?,
Some(config),
)
.await
}
pub async fn list_auth_methods(&self) -> Result<BTreeMap<String, MountInfo>> {
let envelope: ResponseEnvelope<MountInfoMap> = self
.client
.request_json(Method::GET, "sys/auth", Option::<&Empty>::None)
.await?;
Ok(envelope.data.0)
}
pub async fn enable_auth_method(
&self,
mount_path: &str,
request: &AuthEnableRequest,
) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&sys_path("sys/auth", mount_path, None)?,
Some(request),
)
.await
}
pub async fn disable_auth_method(&self, mount_path: &str) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&sys_path("sys/auth", mount_path, None)?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn read_auth_tune(&self, mount_path: &str) -> Result<MountConfig> {
self.client
.request_json(
Method::GET,
&sys_path("sys/auth", mount_path, Some("tune"))?,
Option::<&Empty>::None,
)
.await
}
pub async fn tune_auth_method(&self, mount_path: &str, config: &MountConfig) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&sys_path("sys/auth", mount_path, Some("tune"))?,
Some(config),
)
.await
}
pub async fn list_policies(&self) -> Result<PolicyList> {
self.client
.request_json(Method::GET, "sys/policy", Option::<&Empty>::None)
.await
}
pub async fn list_policies_with_prefix(&self, prefix: &str) -> Result<PolicyList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
self.client
.request_json(
method,
&sys_path("sys/policy", prefix, None)?,
Option::<&Empty>::None,
)
.await
}
pub async fn read_policy(&self, name: &str) -> Result<PolicyInfo> {
self.client
.request_json(
Method::GET,
&sys_path("sys/policy", name, None)?,
Option::<&Empty>::None,
)
.await
}
pub async fn write_policy(&self, name: &str, request: &PolicyWriteRequest) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&sys_path("sys/policy", name, None)?,
Some(request),
)
.await
}
pub async fn delete_policy(&self, name: &str) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&sys_path("sys/policy", name, None)?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn capabilities_self<I, P>(&self, paths: I) -> Result<Capabilities>
where
I: IntoIterator<Item = P>,
P: AsRef<str>,
{
let paths = validate_capability_paths(paths)?;
let payload = CapabilitiesPayload {
paths: &paths,
token: None,
accessor: None,
};
let envelope: ResponseEnvelope<Capabilities> = self
.client
.request_json(Method::POST, "sys/capabilities-self", Some(&payload))
.await?;
Ok(envelope.data)
}
pub async fn capabilities<I, P>(&self, token: &SecretString, paths: I) -> Result<Capabilities>
where
I: IntoIterator<Item = P>,
P: AsRef<str>,
{
let paths = validate_capability_paths(paths)?;
let payload = CapabilitiesPayload {
paths: &paths,
token: Some(token.expose_secret()),
accessor: None,
};
let envelope: ResponseEnvelope<Capabilities> = self
.client
.request_json(Method::POST, "sys/capabilities", Some(&payload))
.await?;
Ok(envelope.data)
}
pub async fn capabilities_accessor<I, P>(
&self,
accessor: &SecretString,
paths: I,
) -> Result<Capabilities>
where
I: IntoIterator<Item = P>,
P: AsRef<str>,
{
let paths = validate_capability_paths(paths)?;
let payload = CapabilitiesPayload {
paths: &paths,
token: None,
accessor: Some(accessor.expose_secret()),
};
let envelope: ResponseEnvelope<Capabilities> = self
.client
.request_json(Method::POST, "sys/capabilities-accessor", Some(&payload))
.await?;
Ok(envelope.data)
}
pub async fn list_audit_devices(&self) -> Result<BTreeMap<String, AuditDevice>> {
let devices: AuditDeviceMap = self
.client
.request_json(Method::GET, "sys/audit", Option::<&Empty>::None)
.await?;
Ok(devices.0)
}
pub async fn enable_audit_device(
&self,
path: &str,
request: &AuditEnableRequest,
) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&sys_path("sys/audit", path, None)?,
Some(request),
)
.await
}
pub async fn disable_audit_device(&self, path: &str) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&sys_path("sys/audit", path, None)?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn audit_hash(&self, path: &str, input: &SecretString) -> Result<AuditHash> {
let payload = AuditHashPayload {
input: input.expose_secret(),
};
self.client
.request_json(
Method::POST,
&sys_path("sys/audit-hash", path, None)?,
Some(&payload),
)
.await
}
pub async fn lookup_lease(&self, lease_id: &SecretString) -> Result<LeaseLookup> {
let payload = LeaseLookupPayload {
lease_id: validate_lease_id(lease_id)?,
};
let envelope: ResponseEnvelope<LeaseLookup> = self
.client
.request_json(Method::POST, "sys/leases/lookup", Some(&payload))
.await?;
Ok(envelope.data)
}
pub async fn renew_lease(
&self,
lease_id: &SecretString,
increment_seconds: Option<u64>,
) -> Result<LeaseRenewal> {
let payload = LeaseRenewPayload {
lease_id: validate_lease_id(lease_id)?,
increment: increment_seconds,
};
let envelope: ResponseEnvelope<Option<Empty>> = self
.client
.request_json(Method::POST, "sys/leases/renew", Some(&payload))
.await?;
Ok(LeaseRenewal {
lease_id: envelope.lease_id,
lease_duration: envelope.lease_duration,
renewable: envelope.renewable,
})
}
pub async fn revoke_lease(&self, lease_id: &SecretString) -> Result<Empty> {
let payload = LeaseRevokePayload {
lease_id: validate_lease_id(lease_id)?,
};
self.client
.request_json_accepting(
Method::POST,
"sys/leases/revoke",
Some(&payload),
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn list_plugins(&self) -> Result<PluginCatalog> {
let envelope: ResponseEnvelope<PluginCatalog> = self
.client
.request_json(Method::GET, "sys/plugins/catalog", Option::<&Empty>::None)
.await?;
Ok(envelope.data)
}
pub async fn list_plugins_by_type(&self, plugin_type: PluginType) -> Result<PluginList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
let envelope: ResponseEnvelope<PluginList> = self
.client
.request_json(
method,
&plugin_catalog_type_path(plugin_type)?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn register_plugin(
&self,
plugin_type: PluginType,
name: &str,
request: &PluginRegisterRequest,
) -> Result<Empty> {
validate_sha256_hex(&request.sha256, "plugin SHA-256")?;
let payload = PluginRegisterPayload {
version: request.version.as_deref(),
sha256: &request.sha256,
command: &request.command,
args: request
.args
.iter()
.map(|value| value.expose_secret())
.collect(),
env: request
.env
.iter()
.map(|value| value.expose_secret())
.collect(),
oci: request.oci,
};
self.client
.request_json(
Method::POST,
&plugin_catalog_entry_path(plugin_type, name)?,
Some(&payload),
)
.await
}
pub async fn read_plugin(
&self,
plugin_type: PluginType,
name: &str,
version: Option<&str>,
) -> Result<PluginInfo> {
let query = plugin_version_query(version)?;
let envelope: ResponseEnvelope<PluginInfo> = self
.client
.request_json_query_accepting(
Method::GET,
&plugin_catalog_entry_path(plugin_type, name)?,
&query,
Option::<&Empty>::None,
&[StatusCode::OK],
)
.await?;
Ok(envelope.data)
}
pub async fn delete_plugin(
&self,
plugin_type: PluginType,
name: &str,
version: Option<&str>,
) -> Result<Empty> {
let query = plugin_version_query(version)?;
self.client
.request_json_query_accepting(
Method::DELETE,
&plugin_catalog_entry_path(plugin_type, name)?,
&query,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn reload_plugin_backend(&self, request: &PluginReloadRequest) -> Result<Empty> {
let payload = validate_plugin_reload_request(request)?;
self.client
.request_json(Method::POST, "sys/plugins/reload/backend", Some(&payload))
.await
}
pub async fn wrapping_lookup(&self, token: &SecretString) -> Result<WrappingLookup> {
let payload = WrappingTokenPayload {
token: token.expose_secret(),
};
let envelope: ResponseEnvelope<WrappingLookup> = self
.client
.request_json(Method::POST, "sys/wrapping/lookup", Some(&payload))
.await?;
Ok(envelope.data)
}
pub async fn wrapping_wrap<T>(&self, ttl: &str, data: &T) -> Result<WrapInfo>
where
T: Serialize + ?Sized,
{
validate_wrapping_ttl(ttl)?;
let ttl =
HeaderValue::from_str(ttl).map_err(|error| Error::InvalidHeader(error.to_string()))?;
let envelope: ResponseEnvelope<Option<Empty>> = self
.client
.request_json_headers_accepting(
Method::POST,
"sys/wrapping/wrap",
&[(HeaderName::from_static("x-vault-wrap-ttl"), ttl)],
Some(data),
&[StatusCode::OK],
)
.await?;
envelope.wrap_info.ok_or(Error::MissingField("wrap_info"))
}
pub async fn wrapping_unwrap<T>(&self, token: Option<&SecretString>) -> Result<T>
where
T: for<'de> Deserialize<'de>,
{
match token {
Some(token) => {
let payload = WrappingTokenPayload {
token: token.expose_secret(),
};
let envelope: ResponseEnvelope<T> = self
.client
.request_json(Method::POST, "sys/wrapping/unwrap", Some(&payload))
.await?;
Ok(envelope.data)
}
None => {
let envelope: ResponseEnvelope<T> = self
.client
.request_json(Method::POST, "sys/wrapping/unwrap", Option::<&Empty>::None)
.await?;
Ok(envelope.data)
}
}
}
pub async fn wrapping_rewrap(&self, token: &SecretString) -> Result<WrapInfo> {
let payload = WrappingTokenPayload {
token: token.expose_secret(),
};
let envelope: ResponseEnvelope<Option<Empty>> = self
.client
.request_json(Method::POST, "sys/wrapping/rewrap", Some(&payload))
.await?;
envelope.wrap_info.ok_or(Error::MissingField("wrap_info"))
}
}
fn validate_dev_bootstrap_options(secret_shares: u8, secret_threshold: u8) -> Result<()> {
if secret_shares == 0 {
return Err(Error::InvalidParameter(
"secret_shares must be greater than zero".into(),
));
}
if secret_threshold == 0 {
return Err(Error::InvalidParameter(
"secret_threshold must be greater than zero".into(),
));
}
if secret_threshold > secret_shares {
return Err(Error::InvalidParameter(
"secret_threshold must be less than or equal to secret_shares".into(),
));
}
Ok(())
}
#[cfg(feature = "operator-ops")]
fn validate_key_share_options(secret_shares: u8, secret_threshold: u8) -> Result<()> {
if secret_shares == 0 {
return Err(Error::InvalidParameter(
"secret_shares must be greater than zero".into(),
));
}
if secret_threshold == 0 {
return Err(Error::InvalidParameter(
"secret_threshold must be greater than zero".into(),
));
}
if secret_threshold > secret_shares {
return Err(Error::InvalidParameter(
"secret_threshold must be less than or equal to secret_shares".into(),
));
}
Ok(())
}
#[cfg(feature = "operator-ops")]
fn rotate_init_path(target: OperatorRotateTarget) -> String {
format!("sys/rotate/{}/init", target.path_segment())
}
#[cfg(feature = "operator-ops")]
fn rotate_update_path(target: OperatorRotateTarget) -> String {
format!("sys/rotate/{}/update", target.path_segment())
}
fn require_loopback_dev_target<State>(client: &Client<State>) -> Result<()> {
let url = client.base_url();
let Some(host) = url.host_str() else {
return Err(Error::InvalidBaseUrl(
"dev bootstrap requires a numeric loopback OpenBao host".into(),
));
};
if !host
.parse::<IpAddr>()
.is_ok_and(|address| address.is_loopback())
{
return Err(Error::InvalidBaseUrl(
"dev bootstrap is restricted to numeric loopback OpenBao hosts".into(),
));
}
Ok(())
}
fn openbao_status(code: u16) -> Result<StatusCode> {
StatusCode::from_u16(code)
.map_err(|_| crate::Error::Internal("invalid OpenBao health status code"))
}
fn sys_path(prefix: &str, mount_path: &str, suffix: Option<&str>) -> Result<String> {
let mut segments = vec![prefix.to_owned()];
segments.extend(validate_mount_path(mount_path)?);
if let Some(suffix) = suffix {
segments.push(suffix.to_owned());
}
Ok(segments.join("/"))
}
fn validate_wrapping_ttl(ttl: &str) -> Result<()> {
if crate::validation::validate_duration_string(ttl, false) {
return Ok(());
}
Err(Error::InvalidHeader(
"wrapping TTL must be a positive duration such as 30s, 5m, or 1h".into(),
))
}
fn validate_capability_paths<I, P>(paths: I) -> Result<Vec<String>>
where
I: IntoIterator<Item = P>,
P: AsRef<str>,
{
let mut validated = Vec::new();
for path in paths {
let path = path.as_ref();
if path.trim_matches('/').is_empty() {
return Err(Error::InvalidPath(
"capability path must not be empty".into(),
));
}
validated.push(validate_secret_path(path)?.join("/"));
}
if validated.is_empty() {
return Err(Error::InvalidPath(
"at least one capability path is required".into(),
));
}
Ok(validated)
}
fn validate_lease_id(lease_id: &SecretString) -> Result<&str> {
let lease_id = lease_id.expose_secret();
if lease_id.is_empty() {
return Err(Error::InvalidPath("lease ID must not be empty".into()));
}
if lease_id.as_bytes().iter().any(u8::is_ascii_control) {
return Err(Error::InvalidPath(
"lease ID must not contain control characters".into(),
));
}
Ok(lease_id)
}
fn plugin_catalog_type_path(plugin_type: PluginType) -> Result<String> {
Ok(["sys/plugins/catalog", plugin_type.as_path_segment()].join("/"))
}
fn plugin_catalog_entry_path(plugin_type: PluginType, name: &str) -> Result<String> {
let mut segments = vec![
"sys/plugins/catalog".to_owned(),
plugin_type.as_path_segment().to_owned(),
];
segments.extend(validate_mount_path(name)?);
Ok(segments.join("/"))
}
fn validate_sha256_hex(value: &str, field: &'static str) -> Result<()> {
if value.len() != 64 {
return Err(Error::InvalidPath(format!(
"{field} must be a 64-character SHA-256 hex digest"
)));
}
if !value
.bytes()
.all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
{
return Err(Error::InvalidPath(format!(
"{field} must contain only lowercase hexadecimal characters"
)));
}
Ok(())
}
fn plugin_version_query(version: Option<&str>) -> Result<Vec<(&'static str, String)>> {
match version {
Some(version) => {
validate_query_string_value(version, "plugin version")?;
Ok(vec![("version", version.to_owned())])
}
None => Ok(Vec::new()),
}
}
fn validate_plugin_reload_request<'a>(
request: &'a PluginReloadRequest,
) -> Result<PluginReloadPayload<'a>> {
let has_plugin = request
.plugin
.as_deref()
.is_some_and(|value| !value.is_empty());
let has_mounts = !request.mounts.is_empty();
match (has_plugin, has_mounts) {
(true, false) | (false, true) => {}
(false, false) => {
return Err(Error::InvalidPath(
"plugin reload requires a plugin name or mount paths".into(),
));
}
(true, true) => {
return Err(Error::InvalidPath(
"plugin reload accepts either plugin or mounts, not both".into(),
));
}
}
let plugin = match request.plugin.as_deref() {
Some(plugin) if !plugin.is_empty() => {
let _segments = validate_mount_path(plugin)?;
Some(plugin)
}
_ => None,
};
let mut mounts = Vec::new();
for mount in &request.mounts {
mounts.push(validate_mount_path(mount)?.join("/"));
}
if let Some(scope) = request.scope.as_deref() {
validate_query_string_value(scope, "plugin reload scope")?;
}
Ok(PluginReloadPayload {
plugin,
mounts,
scope: request.scope.as_deref(),
})
}
fn validate_query_string_value(value: &str, kind: &'static str) -> Result<()> {
if value.is_empty() {
return Err(Error::InvalidPath(format!("{kind} must not be empty")));
}
if value.as_bytes().iter().any(u8::is_ascii_control) {
return Err(Error::InvalidPath(format!(
"{kind} must not contain control characters"
)));
}
Ok(())
}
fn deserialize_null_default<'de, D, T>(deserializer: D) -> core::result::Result<T, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Default,
{
Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
}
fn deserialize_bounded_plugin_detail_vec<'de, D>(
deserializer: D,
) -> core::result::Result<Vec<PluginDetail>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_seq(
BoundedPluginDetailListVisitor::<{ crate::response::MAX_RESPONSE_STRINGS }>,
)
}
#[derive(Deserialize)]
struct BoundedStringList(#[serde(deserialize_with = "deserialize_bounded_string_vec")] Vec<String>);
#[derive(Deserialize)]
struct MountInfoMap(
#[serde(deserialize_with = "deserialize_bounded_mount_info_map")] BTreeMap<String, MountInfo>,
);
#[derive(Deserialize)]
struct AuditDeviceMap(
#[serde(deserialize_with = "deserialize_bounded_audit_device_map")]
BTreeMap<String, AuditDevice>,
);
fn deserialize_bounded_mount_info_map<'de, D>(
deserializer: D,
) -> core::result::Result<BTreeMap<String, MountInfo>, D::Error>
where
D: Deserializer<'de>,
{
deserializer
.deserialize_map(BoundedMountInfoMapVisitor::<{ crate::response::MAX_RESPONSE_STRINGS }>)
}
struct BoundedMountInfoMapVisitor<const MAX: usize>;
impl<'de, const MAX: usize> Visitor<'de> for BoundedMountInfoMapVisitor<MAX> {
type Value = BTreeMap<String, MountInfo>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a map of at most {MAX} mount entries")
}
fn visit_map<A>(self, mut map: A) -> core::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut values = BTreeMap::new();
while values.len() < MAX {
let Some((key, value)) = map.next_entry::<String, MountInfo>()? else {
return Ok(values);
};
values.insert(key, value);
}
if map.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {
return Err(A::Error::custom("OpenBao mount map exceeds item limit"));
}
Ok(values)
}
}
fn deserialize_bounded_audit_device_map<'de, D>(
deserializer: D,
) -> core::result::Result<BTreeMap<String, AuditDevice>, D::Error>
where
D: Deserializer<'de>,
{
deserializer
.deserialize_map(BoundedAuditDeviceMapVisitor::<{ crate::response::MAX_RESPONSE_STRINGS }>)
}
struct BoundedAuditDeviceMapVisitor<const MAX: usize>;
impl<'de, const MAX: usize> Visitor<'de> for BoundedAuditDeviceMapVisitor<MAX> {
type Value = BTreeMap<String, AuditDevice>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a map of at most {MAX} audit devices")
}
fn visit_map<A>(self, mut map: A) -> core::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut values = BTreeMap::new();
while values.len() < MAX {
let Some((key, value)) = map.next_entry::<String, AuditDevice>()? else {
return Ok(values);
};
values.insert(key, value);
}
if map.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {
return Err(A::Error::custom(
"OpenBao audit device map exceeds item limit",
));
}
Ok(values)
}
}
struct BoundedPluginDetailListVisitor<const MAX: usize>;
impl<'de, const MAX: usize> Visitor<'de> for BoundedPluginDetailListVisitor<MAX> {
type Value = Vec<PluginDetail>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a list of at most {MAX} plugin details")
}
fn visit_seq<A>(self, mut seq: A) -> core::result::Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut values = Vec::new();
while values.len() < MAX {
let Some(value) = seq.next_element::<PluginDetail>()? else {
return Ok(values);
};
values.push(value);
}
if seq.next_element::<IgnoredAny>()?.is_some() {
return Err(A::Error::custom(
"OpenBao plugin detail list exceeds item limit",
));
}
Ok(values)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
use secrecy::SecretString;
use super::{
AuditEnableRequest, AuthEnableRequest, LeaseDuration, MountEnableRequest, PolicyList,
PolicyWriteRequest, sys_path, validate_capability_paths, validate_dev_bootstrap_options,
validate_lease_id, validate_sha256_hex, validate_wrapping_ttl,
};
#[cfg(feature = "operator-ops")]
use super::{OperatorInitResponse, OperatorKeyShareUpdateResponse, OperatorKeySharesRequest};
#[test]
fn sys_paths_are_validated() {
assert_eq!(
sys_path("sys/mounts", "secret", Some("tune"))
.unwrap_or_else(|error| panic!("{error}")),
"sys/mounts/secret/tune"
);
assert!(sys_path("sys/mounts", "../secret", None).is_err());
}
#[test]
fn capability_paths_are_validated() {
let paths = validate_capability_paths(["secret/data/app", "/sys/policy/default"])
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(paths, ["secret/data/app", "sys/policy/default"]);
assert!(validate_capability_paths([""]).is_err());
assert!(validate_capability_paths(["../secret"]).is_err());
}
#[test]
fn wrapping_ttl_is_validated() {
assert!(validate_wrapping_ttl("30s").is_ok());
assert!(validate_wrapping_ttl("5m").is_ok());
assert!(validate_wrapping_ttl("1h").is_ok());
assert!(validate_wrapping_ttl("1h30m").is_ok());
assert!(validate_wrapping_ttl("").is_err());
assert!(validate_wrapping_ttl("0s").is_err());
assert!(validate_wrapping_ttl("1h1h").is_err());
assert!(validate_wrapping_ttl("1m1h").is_err());
assert!(validate_wrapping_ttl("999999999999h").is_err());
assert!(validate_wrapping_ttl("-1h").is_err());
assert!(validate_wrapping_ttl("forever").is_err());
}
#[test]
fn dev_bootstrap_options_are_validated() {
assert!(validate_dev_bootstrap_options(1, 1).is_ok());
assert!(validate_dev_bootstrap_options(3, 2).is_ok());
assert!(validate_dev_bootstrap_options(0, 0).is_err());
assert!(validate_dev_bootstrap_options(1, 0).is_err());
assert!(validate_dev_bootstrap_options(1, 2).is_err());
}
#[cfg(feature = "operator-ops")]
#[test]
fn operator_key_share_options_are_validated() {
assert!(OperatorKeySharesRequest::new(1, 1).is_ok());
assert!(OperatorKeySharesRequest::new(0, 1).is_err());
assert!(OperatorKeySharesRequest::new(1, 0).is_err());
assert!(OperatorKeySharesRequest::new(1, 2).is_err());
}
#[cfg(feature = "operator-ops")]
#[test]
fn operator_secret_debug_is_redacted() {
let init = OperatorInitResponse {
keys: vec![SecretString::from(["unseal-", "share"].concat())],
keys_base64: vec![SecretString::from(["base64-", "share"].concat())],
root_token: SecretString::from(["root-", "token"].concat()),
recovery_keys: vec![SecretString::from(["recovery-", "share"].concat())],
recovery_keys_base64: Vec::new(),
};
let init_debug = format!("{init:?}");
assert!(!init_debug.contains(&["root-", "token"].concat()));
assert!(!init_debug.contains(&["unseal-", "share"].concat()));
assert!(init_debug.contains("keys_count"));
let update = OperatorKeyShareUpdateResponse {
complete: true,
keys: vec![SecretString::from(["new-", "share"].concat())],
keys_base64: Vec::new(),
nonce: Some("nonce".to_owned()),
pgp_fingerprints: Vec::new(),
backup: false,
verification_required: false,
verification_nonce: None,
progress: None,
required: None,
};
let update_debug = format!("{update:?}");
assert!(!update_debug.contains(&["new-", "share"].concat()));
assert!(update_debug.contains("keys_count"));
}
#[test]
fn lease_ids_are_validated_for_json_body_use() {
assert!(validate_lease_id(&SecretString::from("database/creds/ro/abc")).is_ok());
assert!(validate_lease_id(&SecretString::from("")).is_err());
assert!(validate_lease_id(&SecretString::from("database/creds/ro\nabc")).is_err());
}
#[test]
fn lease_duration_rejects_untyped_json() {
assert_eq!(
serde_json::from_str::<LeaseDuration>("3600").unwrap_or_else(|error| panic!("{error}")),
LeaseDuration::Seconds(3600)
);
assert_eq!(
serde_json::from_str::<LeaseDuration>(r#""30m""#)
.unwrap_or_else(|error| panic!("{error}")),
LeaseDuration::Duration("30m".to_owned())
);
assert!(serde_json::from_str::<LeaseDuration>("-1").is_err());
assert!(serde_json::from_str::<LeaseDuration>(r#""never""#).is_err());
assert!(serde_json::from_str::<LeaseDuration>(r#"{"ttl":3600}"#).is_err());
}
#[test]
fn policy_list_is_bounded() {
let mut policies = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
policies.push(format!("policy-{index}"));
}
let value = serde_json::json!({ "policies": policies });
let error = match serde_json::from_value::<PolicyList>(value) {
Ok(_) => panic!("oversized policy list unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn mount_config_header_lists_are_bounded() {
let mut headers = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
headers.push(format!("x-header-{index}"));
}
let value = serde_json::json!({ "allowed_response_headers": headers });
let error = match serde_json::from_value::<super::MountConfig>(value) {
Ok(_) => panic!("oversized mount header list unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn audit_device_options_are_bounded() {
let mut options = serde_json::Map::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
options.insert(format!("option-{index}"), serde_json::json!("value"));
}
let value = serde_json::json!({
"type": "file",
"options": options,
});
let error = match serde_json::from_value::<super::AuditDevice>(value) {
Ok(_) => panic!("oversized audit options unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn capabilities_path_map_is_bounded() {
let mut value = serde_json::Map::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
value.insert(format!("secret/data/{index}"), serde_json::json!(["read"]));
}
let error =
match serde_json::from_value::<super::Capabilities>(serde_json::Value::Object(value)) {
Ok(_) => panic!("oversized capabilities map unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn mount_and_audit_maps_are_bounded() {
let mut mounts = serde_json::Map::new();
let mut audits = serde_json::Map::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
mounts.insert(
format!("secret-{index}/"),
serde_json::json!({ "type": "kv", "config": {} }),
);
audits.insert(
format!("file-{index}/"),
serde_json::json!({ "type": "file", "options": {} }),
);
}
let error = match serde_json::from_value::<super::MountInfoMap>(serde_json::Value::Object(
mounts,
)) {
Ok(_) => panic!("oversized mount map unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
let error = match serde_json::from_value::<super::AuditDeviceMap>(
serde_json::Value::Object(audits),
) {
Ok(_) => panic!("oversized audit device map unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn plugin_sha256_is_validated() {
assert!(
validate_sha256_hex(
"d130b9a0fbfddef9709d8ff92e5e6053ccd246b78632fc03b8548457026961e9",
"sha256"
)
.is_ok()
);
assert!(validate_sha256_hex("", "sha256").is_err());
assert!(validate_sha256_hex("not-a-sha256", "sha256").is_err());
assert!(
validate_sha256_hex(
"g130b9a0fbfddef9709d8ff92e5e6053ccd246b78632fc03b8548457026961e9",
"sha256"
)
.is_err()
);
assert!(
validate_sha256_hex(
"D130B9A0FBFDDEF9709D8FF92E5E6053CCD246B78632FC03B8548457026961E9",
"sha256"
)
.is_err()
);
}
#[test]
fn request_constructors_fill_required_fields() {
assert_eq!(MountEnableRequest::new("pki").backend_type, "pki");
assert_eq!(MountEnableRequest::kv2().backend_type, "kv");
assert_eq!(
MountEnableRequest::kv2()
.options
.get("version")
.map(String::as_str),
Some("2")
);
let mount = MountEnableRequest::kv2()
.with_default_lease_ttl("1h")
.and_then(|request| request.with_max_lease_ttl("24h"))
.unwrap_or_else(|error| panic!("{error}"));
assert!(matches!(
mount.config.as_ref().and_then(|config| config.default_lease_ttl.as_ref()),
Some(LeaseDuration::Duration(ttl)) if ttl == "1h"
));
assert!(
MountEnableRequest::kv2()
.with_default_lease_ttl("never")
.is_err()
);
assert_eq!(
AuthEnableRequest::new("kubernetes")
.with_description("cluster auth")
.description
.as_deref(),
Some("cluster auth")
);
let auth = AuthEnableRequest::new("approle")
.with_default_lease_ttl("30m")
.and_then(|request| request.with_max_lease_ttl("2h"))
.unwrap_or_else(|error| panic!("{error}"));
assert!(matches!(
auth.config.as_ref().and_then(|config| config.max_lease_ttl.as_ref()),
Some(LeaseDuration::Duration(ttl)) if ttl == "2h"
));
assert_eq!(
AuditEnableRequest::new("file")
.with_description("audit log")
.description
.as_deref(),
Some("audit log")
);
assert_eq!(
PolicyWriteRequest::new("path \"secret/*\" { capabilities = [\"read\"] }").ttl,
None
);
}
#[test]
fn plugin_detail_list_is_bounded() {
let mut detailed = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
detailed.push(serde_json::json!({
"name": format!("plugin-{index}"),
"type": "secret",
}));
}
let value = serde_json::json!({ "detailed": detailed });
let error = match serde_json::from_value::<super::PluginCatalog>(value) {
Ok(_) => panic!("oversized plugin detail list unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn plugin_reload_request_is_validated() {
assert!(
super::validate_plugin_reload_request(&super::PluginReloadRequest {
plugin: Some("database-plugin".to_owned()),
mounts: Vec::new(),
scope: Some("global".to_owned()),
})
.is_ok()
);
assert!(
super::validate_plugin_reload_request(&super::PluginReloadRequest {
plugin: None,
mounts: vec!["secret".to_owned()],
scope: None,
})
.is_ok()
);
assert!(
super::validate_plugin_reload_request(&super::PluginReloadRequest {
plugin: None,
mounts: Vec::new(),
scope: None,
})
.is_err()
);
assert!(
super::validate_plugin_reload_request(&super::PluginReloadRequest {
plugin: Some("database-plugin".to_owned()),
mounts: vec!["secret".to_owned()],
scope: None,
})
.is_err()
);
}
}