Skip to main content

faucet_auth/
static_provider.rs

1//! A provider that always returns a fixed, pre-minted credential.
2
3use async_trait::async_trait;
4use faucet_core::{AuthProvider, Credential, FaucetError};
5use serde_json::Value;
6
7/// Returns a fixed [`Credential`] forever. Useful for pre-minted tokens whose
8/// lifetime exceeds the run, or for sharing one static bearer token across many
9/// connectors.
10#[derive(Debug, Clone)]
11pub struct StaticProvider {
12    credential: Credential,
13}
14
15impl StaticProvider {
16    /// A static `Authorization: Bearer <token>`.
17    pub fn bearer(token: impl Into<String>) -> Self {
18        Self {
19            credential: Credential::Bearer(token.into()),
20        }
21    }
22
23    /// A static credential of any [`Credential`] shape.
24    pub fn new(credential: Credential) -> Self {
25        Self { credential }
26    }
27
28    /// Build from a `{ token }` / `{ header, value }` / `{ username, password }`
29    /// config object.
30    pub fn from_config(config: &Value) -> Result<Self, FaucetError> {
31        if let Some(token) = config.get("token").and_then(Value::as_str) {
32            return Ok(Self::bearer(token));
33        }
34        if let (Some(name), Some(value)) = (
35            config.get("header").and_then(Value::as_str),
36            config.get("value").and_then(Value::as_str),
37        ) {
38            return Ok(Self::new(Credential::Header {
39                name: name.to_string(),
40                value: value.to_string(),
41            }));
42        }
43        if let (Some(username), Some(password)) = (
44            config.get("username").and_then(Value::as_str),
45            config.get("password").and_then(Value::as_str),
46        ) {
47            return Ok(Self::new(Credential::Basic {
48                username: username.to_string(),
49                password: password.to_string(),
50            }));
51        }
52        Err(FaucetError::Config(
53            "static auth provider: config must contain `token`, `header`+`value`, or `username`+`password`".into(),
54        ))
55    }
56}
57
58#[async_trait]
59impl AuthProvider for StaticProvider {
60    async fn credential(&self) -> Result<Credential, FaucetError> {
61        Ok(self.credential.clone())
62    }
63
64    fn provider_name(&self) -> &'static str {
65        "static"
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[tokio::test]
74    async fn bearer_round_trips() {
75        let p = StaticProvider::bearer("t");
76        assert_eq!(
77            p.credential().await.unwrap(),
78            Credential::Bearer("t".into())
79        );
80    }
81
82    #[tokio::test]
83    async fn from_config_bearer() {
84        let p = StaticProvider::from_config(&serde_json::json!({"token": "abc"})).unwrap();
85        assert_eq!(
86            p.credential().await.unwrap(),
87            Credential::Bearer("abc".into())
88        );
89    }
90
91    #[tokio::test]
92    async fn from_config_header() {
93        let p =
94            StaticProvider::from_config(&serde_json::json!({"header": "X-Api-Key", "value": "k"}))
95                .unwrap();
96        assert_eq!(
97            p.credential().await.unwrap(),
98            Credential::Header {
99                name: "X-Api-Key".into(),
100                value: "k".into()
101            }
102        );
103    }
104
105    #[test]
106    fn from_config_empty_errors() {
107        assert!(StaticProvider::from_config(&serde_json::json!({})).is_err());
108    }
109
110    #[test]
111    fn debug_does_not_leak_token() {
112        // `StaticProvider` derives `Debug`; it must inherit `Credential`'s
113        // redacting `Debug` rather than print the pre-minted token in clear.
114        let p = StaticProvider::bearer("supersecretstatic");
115        let s = format!("{p:?}");
116        assert!(!s.contains("supersecretstatic"), "static token leaked: {s}");
117    }
118}