1use anyhow::Result;
18use async_trait::async_trait;
19use std::collections::HashMap;
20
21#[derive(Debug, Clone, Default)]
35pub struct CredentialContext {
36 pub properties: HashMap<String, String>,
38}
39
40impl CredentialContext {
41 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn with_property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
48 self.properties.insert(key.into(), value.into());
49 self
50 }
51
52 pub fn get(&self, key: &str) -> Option<&str> {
54 self.properties.get(key).map(|s| s.as_str())
55 }
56}
57
58#[async_trait]
64pub trait IdentityProvider: Send + Sync {
65 async fn get_credentials(&self, context: &CredentialContext) -> Result<Credentials>;
72
73 fn clone_box(&self) -> Box<dyn IdentityProvider>;
75}
76
77impl Clone for Box<dyn IdentityProvider> {
78 fn clone(&self) -> Self {
79 self.clone_box()
80 }
81}
82
83#[derive(Clone, PartialEq, Eq)]
85pub enum Credentials {
86 UsernamePassword { username: String, password: String },
88 Token { username: String, token: String },
90 Certificate {
95 cert_pem: String,
97 key_pem: String,
99 username: Option<String>,
101 },
102}
103
104impl std::fmt::Debug for Credentials {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 match self {
108 Credentials::UsernamePassword { username, .. } => f
109 .debug_struct("UsernamePassword")
110 .field("username", username)
111 .field("password", &"[REDACTED]")
112 .finish(),
113 Credentials::Token { username, .. } => f
114 .debug_struct("Token")
115 .field("username", username)
116 .field("token", &"[REDACTED]")
117 .finish(),
118 Credentials::Certificate { username, .. } => f
119 .debug_struct("Certificate")
120 .field("cert_pem", &"[REDACTED]")
121 .field("key_pem", &"[REDACTED]")
122 .field("username", username)
123 .finish(),
124 }
125 }
126}
127
128impl Credentials {
129 pub fn try_into_auth_pair(self) -> std::result::Result<(String, String), Self> {
133 match self {
134 Credentials::UsernamePassword { username, password } => Ok((username, password)),
135 Credentials::Token { username, token } => Ok((username, token)),
136 other => Err(other),
137 }
138 }
139
140 pub fn try_into_certificate(
145 self,
146 ) -> std::result::Result<(String, String, Option<String>), Self> {
147 match self {
148 Credentials::Certificate {
149 cert_pem,
150 key_pem,
151 username,
152 } => Ok((cert_pem, key_pem, username)),
153 other => Err(other),
154 }
155 }
156
157 #[deprecated(note = "Use try_into_auth_pair() which returns Result instead of panicking")]
165 pub(crate) fn into_auth_pair(self) -> (String, String) {
166 self.try_into_auth_pair()
167 .unwrap_or_else(|_| panic!("Certificate credentials cannot be converted to an auth pair. Use try_into_auth_pair() or try_into_certificate() instead."))
168 }
169
170 #[deprecated(note = "Use try_into_certificate() which returns Result instead of panicking")]
178 pub(crate) fn into_certificate(self) -> (String, String, Option<String>) {
179 self.try_into_certificate()
180 .unwrap_or_else(|_| panic!("Not certificate credentials. Use try_into_certificate() or try_into_auth_pair() instead."))
181 }
182
183 pub fn is_certificate(&self) -> bool {
185 matches!(self, Credentials::Certificate { .. })
186 }
187}
188
189mod application;
190mod password;
191pub use application::ApplicationIdentityProvider;
192pub use password::PasswordIdentityProvider;
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[tokio::test]
199 async fn test_password_provider() {
200 let provider = PasswordIdentityProvider::new("testuser", "testpass");
201 let credentials = provider
202 .get_credentials(&CredentialContext::default())
203 .await
204 .unwrap();
205
206 match credentials {
207 Credentials::UsernamePassword { username, password } => {
208 assert_eq!(username, "testuser");
209 assert_eq!(password, "testpass");
210 }
211 _ => panic!("Expected UsernamePassword credentials"),
212 }
213 }
214
215 #[tokio::test]
216 async fn test_provider_clone() {
217 let provider: Box<dyn IdentityProvider> =
218 Box::new(PasswordIdentityProvider::new("user", "pass"));
219 let cloned = provider.clone();
220
221 let credentials = cloned
222 .get_credentials(&CredentialContext::default())
223 .await
224 .unwrap();
225 assert!(matches!(credentials, Credentials::UsernamePassword { .. }));
226 }
227
228 #[test]
229 fn test_try_into_auth_pair_username_password() {
230 let creds = Credentials::UsernamePassword {
231 username: "user".into(),
232 password: "pass".into(),
233 };
234 let (u, p) = creds.try_into_auth_pair().unwrap();
235 assert_eq!(u, "user");
236 assert_eq!(p, "pass");
237 }
238
239 #[test]
240 fn test_try_into_auth_pair_token() {
241 let creds = Credentials::Token {
242 username: "user".into(),
243 token: "tok".into(),
244 };
245 let (u, t) = creds.try_into_auth_pair().unwrap();
246 assert_eq!(u, "user");
247 assert_eq!(t, "tok");
248 }
249
250 #[test]
251 fn test_try_into_auth_pair_rejects_certificate() {
252 let creds = Credentials::Certificate {
253 cert_pem: "cert".into(),
254 key_pem: "key".into(),
255 username: None,
256 };
257 let result = creds.try_into_auth_pair();
258 assert!(result.is_err());
259 let returned = result.unwrap_err();
261 assert!(returned.is_certificate());
262 }
263
264 #[test]
265 fn test_try_into_certificate_success() {
266 let creds = Credentials::Certificate {
267 cert_pem: "cert".into(),
268 key_pem: "key".into(),
269 username: Some("user".into()),
270 };
271 let (c, k, u) = creds.try_into_certificate().unwrap();
272 assert_eq!(c, "cert");
273 assert_eq!(k, "key");
274 assert_eq!(u, Some("user".into()));
275 }
276
277 #[test]
278 fn test_try_into_certificate_rejects_password() {
279 let creds = Credentials::UsernamePassword {
280 username: "user".into(),
281 password: "pass".into(),
282 };
283 let result = creds.try_into_certificate();
284 assert!(result.is_err());
285 }
286}