storage_kit/credentials.rs
1use std::fmt;
2
3use secrecy::{ExposeSecret, SecretString};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use crate::StorageError;
7
8/// Validated password used by storage backend configuration.
9///
10/// A present password must be non-empty and not whitespace-only. Leading and
11/// trailing whitespace are preserved because passwords are opaque caller-owned
12/// values. Use this wrapper for SurrealDB root passwords and PostgreSQL
13/// StateStore authentication.
14///
15/// # Examples
16///
17/// ```
18/// let password = storage_kit::state::config::Password::from_string("postgres secret")?;
19/// let config = storage_kit::state::config::PostgresStateStoreConfig::localhost("db", "user")
20/// .with_password(password.clone());
21///
22/// assert!(!format!("{password:?}").contains("postgres secret"));
23/// assert!(config.password().is_some());
24/// # Ok::<(), storage_kit::StorageError>(())
25/// ```
26#[derive(Clone)]
27pub struct Password {
28 secret: SecretString,
29}
30
31impl Password {
32 /// Validates and stores a password.
33 ///
34 /// # Errors
35 /// Returns a configuration error when the secret is empty or whitespace-only.
36 ///
37 /// # Examples
38 ///
39 /// ```
40 /// let password = storage_kit::state::config::Password::new(secrecy::SecretString::new("secret".into()))?;
41 /// let _ = password;
42 /// # Ok::<(), storage_kit::StorageError>(())
43 /// ```
44 pub fn new(secret: SecretString) -> Result<Self, StorageError> {
45 reject_empty_secret("password", &secret)?;
46 Ok(Self { secret })
47 }
48
49 /// Validates and stores a password from an owned string-like value.
50 ///
51 /// # Errors
52 /// Returns a configuration error when the value is empty or whitespace-only.
53 ///
54 /// # Examples
55 ///
56 /// ```
57 /// let password = storage_kit::state::config::Password::from_string("secret")?;
58 /// let _ = password;
59 /// # Ok::<(), storage_kit::StorageError>(())
60 /// ```
61 pub fn from_string(secret: impl Into<String>) -> Result<Self, StorageError> {
62 Self::new(SecretString::new(secret.into().into()))
63 }
64
65 #[cfg(any(feature = "postgres", feature = "surrealdb"))]
66 pub(crate) fn as_secret(&self) -> &SecretString {
67 &self.secret
68 }
69}
70
71impl fmt::Debug for Password {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 f.debug_struct("Password")
74 .field("secret", &"[redacted]")
75 .finish()
76 }
77}
78
79impl Serialize for Password {
80 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81 where
82 S: Serializer,
83 {
84 serializer.serialize_str(self.secret.expose_secret())
85 }
86}
87
88impl<'de> Deserialize<'de> for Password {
89 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90 where
91 D: Deserializer<'de>,
92 {
93 Self::from_string(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
94 }
95}
96
97fn reject_empty_secret(label: &str, secret: &SecretString) -> Result<(), StorageError> {
98 if secret.expose_secret().trim().is_empty() {
99 return Err(StorageError::InvalidConfig({
100 let mut message = String::from(label);
101 message.push_str(" must not be empty");
102 message
103 }));
104 }
105 Ok(())
106}