reqsign_google/provide_credential/
token.rs1use std::fmt::{self, Debug};
19use std::time::Duration;
20
21use log::debug;
22use reqsign_core::time::Timestamp;
23use reqsign_core::{Context, Error, ProvideCredential, Result};
24
25use crate::credential::{Credential, Token};
26
27#[derive(Clone)]
28enum TokenSource {
29 Inline(String),
30 Path(String),
31}
32
33#[derive(Debug, Clone, Copy)]
34enum Expiration {
35 At(Timestamp),
36 In(Duration),
37}
38
39#[derive(Clone)]
41pub struct TokenCredentialProvider {
42 source: TokenSource,
43 expiration: Option<Expiration>,
44}
45
46impl Debug for TokenCredentialProvider {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.debug_struct("TokenCredentialProvider")
49 .finish_non_exhaustive()
50 }
51}
52
53impl TokenCredentialProvider {
54 pub fn new(access_token: impl Into<String>) -> Self {
56 Self {
57 source: TokenSource::Inline(access_token.into()),
58 expiration: None,
59 }
60 }
61
62 pub fn from_path(path: impl Into<String>) -> Self {
64 Self {
65 source: TokenSource::Path(path.into()),
66 expiration: None,
67 }
68 }
69
70 pub fn with_expires_at(mut self, expires_at: Timestamp) -> Self {
72 self.expiration = Some(Expiration::At(expires_at));
73 self
74 }
75
76 pub fn with_expires_in(mut self, expires_in: Duration) -> Self {
80 self.expiration = Some(Expiration::In(expires_in));
81 self
82 }
83
84 fn build_token(&self, access_token: String) -> Result<Credential> {
85 let access_token = access_token.trim().to_string();
86 if access_token.is_empty() {
87 return Err(Error::credential_invalid("access token is empty"));
88 }
89
90 let expires_at = self.expiration.map(|expiration| match expiration {
91 Expiration::At(ts) => ts,
92 Expiration::In(duration) => Timestamp::now() + duration,
93 });
94
95 Ok(Credential::with_token(Token {
96 access_token,
97 expires_at,
98 }))
99 }
100}
101
102impl ProvideCredential for TokenCredentialProvider {
103 type Credential = Credential;
104
105 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
106 let access_token = match &self.source {
107 TokenSource::Inline(access_token) => {
108 debug!("loading access token from static content");
109 access_token.clone()
110 }
111 TokenSource::Path(path) => {
112 debug!("loading access token from file path: {path}");
113 let content = ctx.file_read(path).await?;
114 String::from_utf8(content)
115 .map_err(|e| Error::unexpected("invalid UTF-8 in token file").with_source(e))?
116 }
117 };
118
119 self.build_token(access_token).map(Some)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn debug_redacts_inline_token_and_path() {
129 let inline_secret = "inline-source-token-secret";
130 let path_secret = "/secret/token/path";
131 let inline =
132 TokenCredentialProvider::new(inline_secret).with_expires_in(Duration::from_secs(3600));
133 let path = TokenCredentialProvider::from_path(path_secret)
134 .with_expires_at(Timestamp::now() + Duration::from_secs(3600));
135
136 assert!(!format!("{inline:?}").contains(inline_secret));
137 assert!(!format!("{path:?}").contains(path_secret));
138 }
139}