Skip to main content

drasi_lib/identity/
mod.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Identity providers for authentication credentials.
16
17use anyhow::Result;
18use async_trait::async_trait;
19use std::collections::HashMap;
20
21/// Context information that callers provide to identity providers.
22///
23/// This allows identity providers to generate context-specific credentials
24/// (e.g., endpoint-specific tokens) without coupling their configuration
25/// to a particular resource type.
26///
27/// # Common Properties
28///
29/// | Key        | Description                         | Example                            |
30/// |------------|-------------------------------------|------------------------------------|
31/// | `hostname` | Target endpoint hostname            | `"mydb.rds.amazonaws.com"`          |
32/// | `port`     | Target endpoint port                | `"5432"`                            |
33/// | `database` | Target database name                | `"mydb"`                            |
34#[derive(Debug, Clone, Default)]
35pub struct CredentialContext {
36    /// Key-value properties that the identity provider may use.
37    pub properties: HashMap<String, String>,
38}
39
40impl CredentialContext {
41    /// Create a new empty context.
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Set a property on the context, returning self for chaining.
47    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    /// Get a property value.
53    pub fn get(&self, key: &str) -> Option<&str> {
54        self.properties.get(key).map(|s| s.as_str())
55    }
56}
57
58/// Trait for identity providers that supply authentication credentials.
59///
60/// This is a plugin trait (Layer 3) — implementations return `anyhow::Result`
61/// and should use `.context()` for error chains. The framework wraps these
62/// into `DrasiError` at the public API boundary.
63#[async_trait]
64pub trait IdentityProvider: Send + Sync {
65    /// Fetch credentials for authentication.
66    ///
67    /// The `context` parameter provides optional caller-specific information
68    /// (such as target hostname/port) that the provider may use to generate
69    /// context-specific credentials. Providers that don't need this context
70    /// can safely ignore it.
71    async fn get_credentials(&self, context: &CredentialContext) -> Result<Credentials>;
72
73    /// Clone the provider into a boxed trait object.
74    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/// Credentials returned by an identity provider.
84#[derive(Clone, PartialEq, Eq)]
85pub enum Credentials {
86    /// Traditional username and password authentication.
87    UsernamePassword { username: String, password: String },
88    /// Token-based authentication (Azure AD, AWS IAM, etc.).
89    Token { username: String, token: String },
90    /// Client certificate authentication (mTLS).
91    ///
92    /// Used for database connections that authenticate via TLS client certificates
93    /// instead of passwords or tokens.
94    Certificate {
95        /// PEM-encoded client certificate.
96        cert_pem: String,
97        /// PEM-encoded private key.
98        key_pem: String,
99        /// Optional username (some databases require it alongside certificates).
100        username: Option<String>,
101    },
102}
103
104// Manual Debug impl to redact sensitive fields (passwords, tokens, keys)
105impl 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    /// Extract username and password/token for connection string building.
130    ///
131    /// Returns `Err(self)` if this is a `Certificate` variant.
132    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    /// Extract certificate and key for TLS client authentication.
141    ///
142    /// Returns `Ok((cert_pem, key_pem, optional_username))` for `Certificate` credentials,
143    /// or `Err(self)` for other variants.
144    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    /// Extract username and password/token for connection string building.
158    ///
159    /// # Panics
160    /// Panics if called on `Certificate` credentials.
161    ///
162    /// # Deprecated
163    /// Use [`try_into_auth_pair`](Self::try_into_auth_pair) instead.
164    #[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    /// Extract certificate and key for TLS client authentication.
171    ///
172    /// # Panics
173    /// Panics if called on non-Certificate credentials.
174    ///
175    /// # Deprecated
176    /// Use [`try_into_certificate`](Self::try_into_certificate) instead.
177    #[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    /// Returns `true` if this is a `Certificate` variant.
184    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        // Verify the original credentials are returned in the Err
260        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}