kunobi_jev/
credentials.rs1use std::fmt;
9use std::future::Future;
10use std::pin::Pin;
11use std::time::Duration;
12
13use reqwest::header::HeaderValue;
14pub use secrecy::{ExposeSecret, SecretString};
15use zeroize::Zeroizing;
16
17use crate::error::{Error, Result};
18
19pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
21
22pub type TokenFuture<'a> =
24 Pin<Box<dyn Future<Output = std::result::Result<SecretString, BoxError>> + Send + 'a>>;
25
26pub trait CredentialProvider: Send + Sync + 'static {
33 fn bearer_token(&self) -> TokenFuture<'_>;
35}
36
37pub(crate) struct FnProvider<F>(pub(crate) F);
39
40impl<F, Fut, T, E> CredentialProvider for FnProvider<F>
41where
42 F: Fn() -> Fut + Send + Sync + 'static,
43 Fut: Future<Output = std::result::Result<T, E>> + Send + 'static,
44 T: Into<SecretString>,
45 E: Into<BoxError>,
46{
47 fn bearer_token(&self) -> TokenFuture<'_> {
48 let token = (self.0)();
49 Box::pin(async move { token.await.map(Into::into).map_err(Into::into) })
50 }
51}
52
53pub(crate) enum Credentials {
55 ApiKey(SecretString),
56 Provider(Box<dyn CredentialProvider>),
57}
58
59impl Credentials {
60 pub(crate) async fn authorization(&self, timeout: Duration) -> Result<HeaderValue> {
65 match self {
66 Credentials::ApiKey(key) => bearer_header(key)
67 .map_err(|message| Error::Config(format!("The API key {message}."))),
68 Credentials::Provider(provider) => {
69 let token = tokio::time::timeout(timeout, provider.bearer_token())
70 .await
71 .map_err(|_| Error::Credentials {
72 source: format!(
73 "the credential provider did not return a token within {}ms",
74 timeout.as_millis()
75 )
76 .into(),
77 })?
78 .map_err(|source| Error::Credentials { source })?;
79 bearer_header(&token).map_err(|message| Error::Credentials {
80 source: format!("the token {message}").into(),
81 })
82 }
83 }
84 }
85}
86
87pub(crate) fn bearer_header(
89 token: &SecretString,
90) -> std::result::Result<HeaderValue, &'static str> {
91 let token = token.expose_secret().trim();
92 if token.is_empty() {
93 return Err("is blank");
94 }
95 let value = Zeroizing::new(format!("Bearer {token}"));
97 let mut header = HeaderValue::from_str(&value)
98 .map_err(|_| "contains characters that are not valid in an HTTP header")?;
99 header.set_sensitive(true);
100 Ok(header)
101}
102
103impl fmt::Debug for Credentials {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Credentials::ApiKey(_) => f.write_str("ApiKey(***)"),
107 Credentials::Provider(_) => f.write_str("Provider"),
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn bearer_headers_are_sensitive_and_trimmed() {
118 let header = bearer_header(&SecretString::from(" sk-1 ")).unwrap();
119 assert!(header.is_sensitive());
120 assert_eq!(header.to_str().unwrap(), "Bearer sk-1");
121 }
122
123 #[test]
124 fn bearer_headers_reject_blank_and_invalid_tokens() {
125 assert_eq!(bearer_header(&SecretString::from(" ")), Err("is blank"));
126 assert!(bearer_header(&SecretString::from("a\nb")).is_err());
127 }
128
129 #[test]
130 fn debug_output_hides_the_key() {
131 let debug = format!("{:?}", Credentials::ApiKey(SecretString::from("sk-secret")));
132 assert_eq!(debug, "ApiKey(***)");
133 }
134
135 #[tokio::test]
136 async fn provider_failures_and_hangs_are_credential_errors() {
137 let failing = Credentials::Provider(Box::new(FnProvider(|| async {
138 Err::<String, _>(std::io::Error::other("refresh token expired"))
139 })));
140 let err = failing
141 .authorization(Duration::from_secs(1))
142 .await
143 .unwrap_err();
144 assert!(matches!(err, Error::Credentials { .. }));
145 assert!(err.to_string().contains("refresh token expired"), "{err}");
146
147 let hanging = Credentials::Provider(Box::new(FnProvider(|| async {
148 tokio::time::sleep(Duration::from_secs(60)).await;
149 Ok::<_, BoxError>("late".to_owned())
150 })));
151 let err = hanging
152 .authorization(Duration::from_millis(20))
153 .await
154 .unwrap_err();
155 assert!(
156 err.to_string()
157 .contains("did not return a token within 20ms"),
158 "{err}"
159 );
160
161 let invalid = Credentials::Provider(Box::new(FnProvider(|| async {
162 Ok::<_, BoxError>("bad\ntoken".to_owned())
163 })));
164 let err = invalid
165 .authorization(Duration::from_secs(1))
166 .await
167 .unwrap_err();
168 assert!(
169 err.to_string().contains("not valid in an HTTP header"),
170 "{err}"
171 );
172 assert!(
173 !err.to_string().contains("bad"),
174 "the token must not leak: {err}"
175 );
176 }
177}