Skip to main content

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}