smbcloud_ascapi_core/
token.rs1use crate::auth::ApiKey;
15use crate::error::Result;
16use std::fmt;
17use std::time::Duration;
18
19#[derive(Clone)]
25pub struct MintedToken {
26 pub value: String,
28 pub lifetime: Duration,
30}
31
32impl MintedToken {
33 pub fn new(value: impl Into<String>, lifetime_secs: u64) -> Self {
35 Self {
36 value: value.into(),
37 lifetime: Duration::from_secs(lifetime_secs),
38 }
39 }
40}
41
42impl fmt::Debug for MintedToken {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.debug_struct("MintedToken")
46 .field("value", &"<redacted>")
47 .field("lifetime", &self.lifetime)
48 .finish()
49 }
50}
51
52pub trait TokenSource: Send + Sync {
57 fn mint(&self) -> Result<MintedToken>;
60
61 fn describe(&self) -> String {
65 "token source".to_string()
66 }
67}
68
69impl fmt::Debug for dyn TokenSource {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.debug_struct("TokenSource")
72 .field("describe", &self.describe())
73 .finish()
74 }
75}
76
77impl TokenSource for ApiKey {
78 fn mint(&self) -> Result<MintedToken> {
79 Ok(MintedToken {
80 value: self.token()?,
81 lifetime: Duration::from_secs(self.lifetime_secs()),
82 })
83 }
84
85 fn describe(&self) -> String {
86 format!("api key {}", self.key_id())
89 }
90}
91
92#[derive(Clone)]
98pub struct StaticToken {
99 value: String,
100 lifetime: Duration,
101}
102
103impl StaticToken {
104 pub fn new(value: impl Into<String>) -> Self {
107 Self {
108 value: value.into(),
109 lifetime: Duration::from_secs(20 * 60),
110 }
111 }
112
113 pub fn with_lifetime_secs(mut self, secs: u64) -> Self {
115 self.lifetime = Duration::from_secs(secs);
116 self
117 }
118}
119
120impl fmt::Debug for StaticToken {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 f.debug_struct("StaticToken")
123 .field("value", &"<redacted>")
124 .field("lifetime", &self.lifetime)
125 .finish()
126 }
127}
128
129impl TokenSource for StaticToken {
130 fn mint(&self) -> Result<MintedToken> {
131 Ok(MintedToken {
132 value: self.value.clone(),
133 lifetime: self.lifetime,
134 })
135 }
136
137 fn describe(&self) -> String {
138 "static token".to_string()
139 }
140}
141
142pub trait IntoTokenSource {
146 fn into_token_source(self) -> Box<dyn TokenSource>;
147}
148
149impl<T: TokenSource + 'static> IntoTokenSource for T {
150 fn into_token_source(self) -> Box<dyn TokenSource> {
151 Box::new(self)
152 }
153}
154
155impl IntoTokenSource for Box<dyn TokenSource> {
156 fn into_token_source(self) -> Box<dyn TokenSource> {
157 self
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn a_static_token_mints_its_value_with_apples_twenty_minute_ceiling() {
167 let minted = StaticToken::new("eyJ.static").mint().unwrap();
168 assert_eq!(minted.value, "eyJ.static");
169 assert_eq!(minted.lifetime, Duration::from_secs(20 * 60));
170 }
171
172 #[test]
173 fn a_static_token_lifetime_can_be_overridden() {
174 let minted = StaticToken::new("eyJ.static")
175 .with_lifetime_secs(90)
176 .mint()
177 .unwrap();
178 assert_eq!(minted.lifetime, Duration::from_secs(90));
179 }
180
181 #[test]
184 fn debug_output_never_contains_the_token() {
185 let token = StaticToken::new("eyJ.secret");
186 let minted = token.mint().unwrap();
187 let boxed: Box<dyn TokenSource> = Box::new(token.clone());
188
189 for rendered in [
190 format!("{token:?}"),
191 format!("{minted:?}"),
192 format!("{boxed:?}"),
193 ] {
194 assert!(!rendered.contains("eyJ.secret"), "leaked: {rendered}");
195 }
196 assert_eq!(boxed.describe(), "static token");
197 }
198
199 #[test]
200 fn a_boxed_source_converts_into_a_token_source() {
201 let boxed: Box<dyn TokenSource> = Box::new(StaticToken::new("eyJ.boxed"));
202 let source = boxed.into_token_source();
203 assert_eq!(source.mint().unwrap().value, "eyJ.boxed");
204 }
205}