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 log::debug;
19use std::time::Duration;
20
21use reqsign_core::time::Timestamp;
22use reqsign_core::{Context, Error, ProvideCredential, Result};
23
24use crate::credential::{Credential, Token};
25
26#[derive(Debug, Clone)]
27enum TokenSource {
28    Inline(String),
29    Path(String),
30}
31
32#[derive(Debug, Clone, Copy)]
33enum Expiration {
34    At(Timestamp),
35    In(Duration),
36}
37
38/// TokenCredentialProvider loads a raw OAuth access token from memory or a file path.
39#[derive(Debug, Clone)]
40pub struct TokenCredentialProvider {
41    source: TokenSource,
42    expiration: Option<Expiration>,
43}
44
45impl TokenCredentialProvider {
46    /// Create a new TokenCredentialProvider from an access token string.
47    pub fn new(access_token: impl Into<String>) -> Self {
48        Self {
49            source: TokenSource::Inline(access_token.into()),
50            expiration: None,
51        }
52    }
53
54    /// Create a new TokenCredentialProvider from a token file path.
55    pub fn from_path(path: impl Into<String>) -> Self {
56        Self {
57            source: TokenSource::Path(path.into()),
58            expiration: None,
59        }
60    }
61
62    /// Set an absolute expiration time for the token.
63    pub fn with_expires_at(mut self, expires_at: Timestamp) -> Self {
64        self.expiration = Some(Expiration::At(expires_at));
65        self
66    }
67
68    /// Set a relative expiration duration for the token.
69    ///
70    /// The expiration is evaluated when credentials are loaded.
71    pub fn with_expires_in(mut self, expires_in: Duration) -> Self {
72        self.expiration = Some(Expiration::In(expires_in));
73        self
74    }
75
76    fn build_token(&self, access_token: String) -> Result<Credential> {
77        let access_token = access_token.trim().to_string();
78        if access_token.is_empty() {
79            return Err(Error::credential_invalid("access token is empty"));
80        }
81
82        let expires_at = self.expiration.map(|expiration| match expiration {
83            Expiration::At(ts) => ts,
84            Expiration::In(duration) => Timestamp::now() + duration,
85        });
86
87        Ok(Credential::with_token(Token {
88            access_token,
89            expires_at,
90        }))
91    }
92}
93
94impl ProvideCredential for TokenCredentialProvider {
95    type Credential = Credential;
96
97    async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
98        let access_token = match &self.source {
99            TokenSource::Inline(access_token) => {
100                debug!("loading access token from static content");
101                access_token.clone()
102            }
103            TokenSource::Path(path) => {
104                debug!("loading access token from file path: {path}");
105                let content = ctx.file_read(path).await?;
106                String::from_utf8(content)
107                    .map_err(|e| Error::unexpected("invalid UTF-8 in token file").with_source(e))?
108            }
109        };
110
111        self.build_token(access_token).map(Some)
112    }
113}