qcs_api_client_common/configuration/
secret_string.rs

1//! Helper macro and dedicated module for secret string types.
2//! Avoid exporting this macro, since defining the secret here allows
3//! us to make the inner value private and only expose the values via dedicated methods we want.
4//! Define the secret strings here and re-export them in the appropriate modules.
5#![allow(
6    non_local_definitions,
7    unreachable_pub,
8    dead_code,
9    reason = "necessary for pyo3::pymethods"
10)]
11
12use serde::{Deserialize, Serialize};
13use std::{borrow::Cow, fmt};
14
15#[cfg(feature = "python")]
16use crate::{impl_eq, impl_repr};
17
18/// Builds a type that wraps [`Cow<'static, str>`] which helps prevent values
19/// from being accidentally viewed in e.g. in debug or log output.
20macro_rules! make_secret_string {
21    (
22       $(#[$attr:meta])*
23       $name:ident
24    ) => {
25        $(#[$attr])*
26        #[cfg_attr(feature = "python", ::pyo3::pyclass)]
27        #[derive(Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
28        #[serde(transparent)]
29        pub struct $name(Cow<'static, str>);
30
31        impl fmt::Debug for $name {
32            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33                const NAME: &str = stringify!($name);
34                let len = self.0.len();
35                write!(f, "{NAME}(<REDACTED len: {len}>)")
36            }
37        }
38
39        impl<T: Into<Cow<'static, str>>> From<T> for $name {
40            fn from(value: T) -> Self {
41                Self(value.into())
42            }
43        }
44
45        impl $name {
46            #[must_use]
47            /// Check if the secret is an empty value
48            pub fn is_empty(&self) -> bool {
49                self.0.is_empty()
50            }
51
52            #[must_use]
53            /// Get the inner secret contents, which removes the protection against accidentally exposing the value.
54            pub fn secret(&self) -> &str {
55                self.0.as_ref()
56            }
57
58        }
59
60        #[cfg(feature = "python")]
61        impl_repr!($name);
62
63        #[cfg(feature = "python")]
64        impl_eq!($name);
65
66        #[cfg(feature = "python")]
67        #[::pyo3::pymethods]
68        impl $name {
69            #[new]
70            fn py_new(value: String) -> Self {
71                Self::from(value)
72            }
73
74            #[must_use]
75            #[getter]
76            #[pyo3(name = "is_empty")]
77            /// Check if the secret is an empty value
78            pub fn py_is_empty(&self) -> bool {
79                self.is_empty()
80            }
81
82            #[must_use]
83            #[getter]
84            #[pyo3(name = "secret")]
85            /// Get the inner secret contents, which removes the protection against accidentally exposing the value.
86            pub fn py_secret(&self) -> &str {
87                self.secret()
88            }
89        }
90    }
91}
92
93make_secret_string!(
94    /// An [OAuth 2.0 refresh token][https://oauth.net/2/refresh-tokens/] that is used to obtain a new [`SecretAccessToken`].
95    SecretRefreshToken
96);
97
98make_secret_string!(
99    /// An [OAuth 2.0 access token][https://oauth.net/2/access-tokens/] that is used to authenticate requests to the QCS API as a `Bearer` token.
100    SecretAccessToken
101);
102
103make_secret_string!(
104    /// The [OAuth2 Client Credentials](https://oauth.net/2/grant-types/client-credentials/) secret.
105    ClientSecret
106);
107
108#[cfg(test)]
109mod test {
110    use super::*;
111
112    make_secret_string!(TestSecret);
113
114    #[test]
115    fn test_secret_string_serialization() {
116        const SECRET_VALUE: &str = "my_secret_value";
117        const SECRET_VALUE_JSON: &str = "\"my_secret_value\"";
118
119        // Test that the secret string is a plain JSON string
120        assert_eq!(
121            serde_json::to_value(TestSecret::from(SECRET_VALUE)).unwrap(),
122            serde_json::Value::String(SECRET_VALUE.to_string()),
123        );
124
125        let test_secret: TestSecret = serde_json::from_str(SECRET_VALUE_JSON).unwrap();
126        assert_eq!(test_secret.secret(), SECRET_VALUE);
127
128        assert_eq!(
129            serde_json::to_string(&test_secret).unwrap(),
130            SECRET_VALUE_JSON
131        );
132    }
133
134    #[test]
135    fn test_secret_string_debug_does_not_leak() {
136        const SECRET_VALUE: &str = "my_secret_value";
137        let test_secret = TestSecret::from(SECRET_VALUE);
138
139        let debug_content = format!("{test_secret:?}");
140
141        assert_eq!(
142            debug_content,
143            format!("TestSecret(<REDACTED len: {}>)", SECRET_VALUE.len())
144        );
145    }
146}