Skip to main content

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 rigetti_pyo3::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        #[derive(Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
26        #[serde(transparent)]
27        #[cfg_attr(not(feature = "python"), ::optipy::strip_pyo3)]
28        #[cfg_attr(feature = "stubs", ::pyo3_stub_gen::derive::gen_stub_pyclass)]
29        #[cfg_attr(feature = "python",
30            ::pyo3::pyclass(module = "qcs_api_client_common.configuration", eq, frozen, skip_from_py_object))]
31        $(#[$attr])*
32        pub struct $name(Cow<'static, str>);
33
34        impl fmt::Debug for $name {
35            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36                const NAME: &str = stringify!($name);
37                let len = self.0.len();
38                write!(f, "{NAME}(<REDACTED len: {len}>)")
39            }
40        }
41
42        impl<T: Into<Cow<'static, str>>> From<T> for $name {
43            fn from(value: T) -> Self {
44                Self(value.into())
45            }
46        }
47
48        #[cfg_attr(not(feature = "python"), ::optipy::strip_pyo3)]
49        #[cfg_attr(feature = "stubs", ::pyo3_stub_gen::derive::gen_stub_pymethods)]
50        #[cfg_attr(feature = "python", ::pyo3::pymethods)]
51        impl $name {
52            #[must_use]
53            #[getter(is_empty)]
54            /// Check if the secret is an empty value
55            pub fn is_empty(&self) -> bool {
56                self.0.is_empty()
57            }
58
59            #[must_use]
60            #[getter(secret)]
61            /// Get the inner secret contents, which removes the protection against accidentally exposing the value.
62            pub fn secret(&self) -> &str {
63                self.0.as_ref()
64            }
65        }
66
67        #[cfg(feature = "python")]
68        impl_repr!($name);
69
70        #[cfg(feature = "python")]
71        #[cfg_attr(feature = "stubs", ::pyo3_stub_gen::derive::gen_stub_pymethods)]
72        #[::pyo3::pymethods]
73        impl $name {
74            #[new]
75            pub(crate) fn __new__(value: String) -> Self {
76                Self::from(value)
77            }
78        }
79    }
80}
81
82make_secret_string!(
83    /// An [OAuth 2.0 refresh token][https://oauth.net/2/refresh-tokens/] that is used to obtain a new [`SecretAccessToken`].
84    SecretRefreshToken
85);
86
87make_secret_string!(
88    /// 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.
89    SecretAccessToken
90);
91
92make_secret_string!(
93    /// The [OAuth2 Client Credentials](https://oauth.net/2/grant-types/client-credentials/) secret.
94    ClientSecret
95);
96
97#[cfg(test)]
98mod test {
99    use super::*;
100
101    make_secret_string!(TestSecret);
102
103    #[test]
104    fn test_secret_string_serialization() {
105        const SECRET_VALUE: &str = "my_secret_value";
106        const SECRET_VALUE_JSON: &str = "\"my_secret_value\"";
107
108        // Test that the secret string is a plain JSON string
109        assert_eq!(
110            serde_json::to_value(TestSecret::from(SECRET_VALUE)).unwrap(),
111            serde_json::Value::String(SECRET_VALUE.to_string()),
112        );
113
114        let test_secret: TestSecret = serde_json::from_str(SECRET_VALUE_JSON).unwrap();
115        assert_eq!(test_secret.secret(), SECRET_VALUE);
116
117        assert_eq!(
118            serde_json::to_string(&test_secret).unwrap(),
119            SECRET_VALUE_JSON
120        );
121    }
122
123    #[test]
124    fn test_secret_string_debug_does_not_leak() {
125        const SECRET_VALUE: &str = "my_secret_value";
126        let test_secret = TestSecret::from(SECRET_VALUE);
127
128        let debug_content = format!("{test_secret:?}");
129
130        assert_eq!(
131            debug_content,
132            format!("TestSecret(<REDACTED len: {}>)", SECRET_VALUE.len())
133        );
134    }
135}