Skip to main content

reqsign_google/provide_credential/
token.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// TokenCredentialProvider loads a raw OAuth access token from memory or a file path.
40#[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    /// Create a new TokenCredentialProvider from an access token string.
55    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    /// Create a new TokenCredentialProvider from a token file path.
63    pub fn from_path(path: impl Into<String>) -> Self {
64        Self {
65            source: TokenSource::Path(path.into()),
66            expiration: None,
67        }
68    }
69
70    /// Set an absolute expiration time for the token.
71    pub fn with_expires_at(mut self, expires_at: Timestamp) -> Self {
72        self.expiration = Some(Expiration::At(expires_at));
73        self
74    }
75
76    /// Set a relative expiration duration for the token.
77    ///
78    /// The expiration is evaluated when credentials are loaded.
79    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}