1use parking_lot::RwLock;
61use ring::rand::{SecureRandom, SystemRandom};
62use serde::{Deserialize, Serialize};
63use sha2::{Digest, Sha256};
64use std::collections::HashMap;
65use std::time::{Duration, Instant, SystemTime};
66use thiserror::Error;
67use tracing::{debug, info, warn};
68
69#[derive(Error, Debug)]
74pub enum ServiceAuthError {
75 #[error("Invalid API key format")]
76 InvalidKeyFormat,
77
78 #[error("API key not found: {0}")]
79 KeyNotFound(String),
80
81 #[error("API key expired")]
82 KeyExpired,
83
84 #[error("API key revoked")]
85 KeyRevoked,
86
87 #[error("Invalid credentials")]
88 InvalidCredentials,
89
90 #[error("Certificate error: {0}")]
91 CertificateError(String),
92
93 #[error("Service account not found: {0}")]
94 ServiceAccountNotFound(String),
95
96 #[error("Service account disabled: {0}")]
97 ServiceAccountDisabled(String),
98
99 #[error("Permission denied: {0}")]
100 PermissionDenied(String),
101
102 #[error("Rate limited: too many authentication attempts")]
103 RateLimited,
104
105 #[error("Internal error: {0}")]
106 Internal(String),
107}
108
109pub type ServiceAuthResult<T> = Result<T, ServiceAuthError>;
110
111#[derive(Clone, Serialize, Deserialize)]
122pub struct ApiKey {
123 pub key_id: String,
125
126 pub secret_hash: String,
128
129 pub service_account: String,
131
132 pub description: Option<String>,
134
135 pub created_at: SystemTime,
137
138 pub expires_at: Option<SystemTime>,
140
141 pub last_used_at: Option<SystemTime>,
143
144 pub revoked: bool,
146
147 pub allowed_ips: Vec<String>,
149
150 pub permissions: Vec<String>,
152}
153
154impl std::fmt::Debug for ApiKey {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 f.debug_struct("ApiKey")
157 .field("key_id", &self.key_id)
158 .field("secret_hash", &"[REDACTED]")
159 .field("service_account", &self.service_account)
160 .field("description", &self.description)
161 .field("created_at", &self.created_at)
162 .field("expires_at", &self.expires_at)
163 .field("last_used_at", &self.last_used_at)
164 .field("revoked", &self.revoked)
165 .field("allowed_ips", &self.allowed_ips)
166 .field("permissions", &self.permissions)
167 .finish()
168 }
169}
170
171impl ApiKey {
172 pub fn generate(
174 service_account: &str,
175 description: Option<&str>,
176 expires_in: Option<Duration>,
177 permissions: Vec<String>,
178 ) -> ServiceAuthResult<(Self, String)> {
179 let rng = SystemRandom::new();
180
181 let mut key_id_bytes = [0u8; 8];
183 rng.fill(&mut key_id_bytes)
184 .map_err(|_| ServiceAuthError::Internal("RNG failed".into()))?;
185 let key_id = hex::encode(key_id_bytes);
186
187 let mut secret_bytes = [0u8; 32];
189 rng.fill(&mut secret_bytes)
190 .map_err(|_| ServiceAuthError::Internal("RNG failed".into()))?;
191 let secret = base64::Engine::encode(
192 &base64::engine::general_purpose::STANDARD_NO_PAD,
193 secret_bytes,
194 );
195
196 let secret_hash = Self::hash_secret(&secret);
198
199 let full_key = format!("rvn.v1.{}.{}", key_id, secret);
201
202 let now = SystemTime::now();
203 let expires_at = expires_in.map(|d| now + d);
204
205 let api_key = Self {
206 key_id,
207 secret_hash,
208 service_account: service_account.to_string(),
209 description: description.map(|s| s.to_string()),
210 created_at: now,
211 expires_at,
212 last_used_at: None,
213 revoked: false,
214 allowed_ips: vec![],
215 permissions,
216 };
217
218 Ok((api_key, full_key))
219 }
220
221 fn hash_secret(secret: &str) -> String {
223 let mut hasher = Sha256::new();
224 hasher.update(secret.as_bytes());
225 hex::encode(hasher.finalize())
226 }
227
228 pub fn parse_key(key: &str) -> ServiceAuthResult<(String, String)> {
230 let parts: Vec<&str> = key.split('.').collect();
231 if parts.len() != 4 || parts[0] != "rvn" || parts[1] != "v1" {
232 return Err(ServiceAuthError::InvalidKeyFormat);
233 }
234
235 let key_id = parts[2].to_string();
236 let secret = parts[3].to_string();
237
238 Ok((key_id, secret))
239 }
240
241 pub fn verify_secret(&self, secret: &str) -> bool {
243 let provided_hash = Self::hash_secret(secret);
244 if provided_hash.len() != self.secret_hash.len() {
247 return false;
248 }
249
250 let mut result = 0u8;
251 for (a, b) in provided_hash
252 .as_bytes()
253 .iter()
254 .zip(self.secret_hash.as_bytes())
255 {
256 result |= a ^ b;
257 }
258 result == 0
259 }
260
261 pub fn is_valid(&self) -> bool {
263 if self.revoked {
264 return false;
265 }
266
267 if let Some(expires_at) = self.expires_at {
268 if SystemTime::now() > expires_at {
269 return false;
270 }
271 }
272
273 true
274 }
275
276 pub fn is_ip_allowed(&self, ip: &str) -> bool {
278 if self.allowed_ips.is_empty() {
279 return true;
280 }
281
282 self.allowed_ips.iter().any(|allowed| {
283 if allowed.contains('/') {
285 Self::ip_in_cidr(ip, allowed)
286 } else {
287 allowed == ip
288 }
289 })
290 }
291
292 fn ip_in_cidr(ip: &str, cidr: &str) -> bool {
293 let parts: Vec<&str> = cidr.split('/').collect();
295 if parts.len() != 2 {
296 return false;
297 }
298
299 let network = parts[0];
300 let prefix_len: u32 = parts[1].parse().unwrap_or(32);
301
302 let ip_parts: Vec<u8> = ip.split('.').filter_map(|p| p.parse().ok()).collect();
304 let net_parts: Vec<u8> = network.split('.').filter_map(|p| p.parse().ok()).collect();
305
306 if ip_parts.len() != 4 || net_parts.len() != 4 {
307 return false;
308 }
309
310 let ip_num = u32::from_be_bytes([ip_parts[0], ip_parts[1], ip_parts[2], ip_parts[3]]);
311 let net_num = u32::from_be_bytes([net_parts[0], net_parts[1], net_parts[2], net_parts[3]]);
312
313 let mask = if prefix_len == 0 {
314 0
315 } else {
316 !0u32 << (32 - prefix_len)
317 };
318
319 (ip_num & mask) == (net_num & mask)
320 }
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct ServiceAccount {
330 pub name: String,
332
333 pub description: Option<String>,
335
336 pub enabled: bool,
338
339 pub created_at: SystemTime,
341
342 pub roles: Vec<String>,
344
345 pub metadata: HashMap<String, String>,
347
348 pub certificate_subject: Option<String>,
350
351 pub oidc_client_id: Option<String>,
353}
354
355impl ServiceAccount {
356 pub fn new(name: impl Into<String>) -> Self {
357 Self {
358 name: name.into(),
359 description: None,
360 enabled: true,
361 created_at: SystemTime::now(),
362 roles: vec![],
363 metadata: HashMap::new(),
364 certificate_subject: None,
365 oidc_client_id: None,
366 }
367 }
368
369 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
370 self.description = Some(desc.into());
371 self
372 }
373
374 pub fn with_roles(mut self, roles: Vec<String>) -> Self {
375 self.roles = roles;
376 self
377 }
378
379 pub fn with_certificate_subject(mut self, subject: impl Into<String>) -> Self {
380 self.certificate_subject = Some(subject.into());
381 self
382 }
383
384 pub fn with_oidc_client_id(mut self, client_id: impl Into<String>) -> Self {
385 self.oidc_client_id = Some(client_id.into());
386 self
387 }
388}
389
390#[derive(Debug, Clone)]
396pub struct ServiceSession {
397 pub id: String,
399
400 pub service_account: String,
402
403 pub auth_method: AuthMethod,
405
406 expires_at: Instant,
408
409 pub created_timestamp: SystemTime,
411
412 pub permissions: Vec<String>,
414
415 pub client_ip: String,
417
418 pub api_key_id: Option<String>,
420}
421
422impl ServiceSession {
423 pub fn is_expired(&self) -> bool {
424 Instant::now() > self.expires_at
425 }
426
427 pub fn time_until_expiration(&self) -> Duration {
429 self.expires_at.saturating_duration_since(Instant::now())
430 }
431
432 pub fn expires_in_secs(&self) -> u64 {
434 self.time_until_expiration().as_secs()
435 }
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
440pub enum AuthMethod {
441 ApiKey,
442 MutualTls,
443 OidcClientCredentials,
444 SaslScram,
445}
446
447struct AuthRateLimiter {
452 attempts: HashMap<String, Vec<Instant>>,
453 max_attempts: usize,
454 window: Duration,
455}
456
457impl AuthRateLimiter {
458 fn new(max_attempts: usize, window: Duration) -> Self {
459 Self {
460 attempts: HashMap::new(),
461 max_attempts,
462 window,
463 }
464 }
465
466 fn check_and_record(&mut self, key: &str) -> bool {
467 let now = Instant::now();
468 let cutoff = now - self.window;
469
470 let attempts = self.attempts.entry(key.to_string()).or_default();
471
472 attempts.retain(|&t| t > cutoff);
474
475 if attempts.len() >= self.max_attempts {
477 return false;
478 }
479
480 attempts.push(now);
482 true
483 }
484
485 fn clear(&mut self, key: &str) {
486 self.attempts.remove(key);
487 }
488}
489
490pub struct ServiceAuthManager {
496 api_keys: RwLock<HashMap<String, ApiKey>>,
498
499 service_accounts: RwLock<HashMap<String, ServiceAccount>>,
501
502 sessions: RwLock<HashMap<String, ServiceSession>>,
504
505 rate_limiter: RwLock<AuthRateLimiter>,
507
508 session_duration: Duration,
510}
511
512impl ServiceAuthManager {
513 pub fn new() -> Self {
514 Self {
515 api_keys: RwLock::new(HashMap::new()),
516 service_accounts: RwLock::new(HashMap::new()),
517 sessions: RwLock::new(HashMap::new()),
518 rate_limiter: RwLock::new(AuthRateLimiter::new(10, Duration::from_secs(60))),
519 session_duration: Duration::from_secs(3600), }
521 }
522
523 pub fn with_session_duration(mut self, duration: Duration) -> Self {
524 self.session_duration = duration;
525 self
526 }
527
528 pub fn create_service_account(&self, account: ServiceAccount) -> ServiceAuthResult<()> {
534 let mut accounts = self.service_accounts.write();
535
536 if accounts.contains_key(&account.name) {
537 return Err(ServiceAuthError::Internal(format!(
538 "Service account '{}' already exists",
539 account.name
540 )));
541 }
542
543 info!("Created service account: {}", account.name);
544 accounts.insert(account.name.clone(), account);
545 Ok(())
546 }
547
548 pub fn get_service_account(&self, name: &str) -> Option<ServiceAccount> {
550 self.service_accounts.read().get(name).cloned()
551 }
552
553 pub fn disable_service_account(&self, name: &str) -> ServiceAuthResult<()> {
555 let mut accounts = self.service_accounts.write();
556
557 let account = accounts
558 .get_mut(name)
559 .ok_or_else(|| ServiceAuthError::ServiceAccountNotFound(name.to_string()))?;
560
561 account.enabled = false;
562
563 let mut sessions = self.sessions.write();
565 sessions.retain(|_, s| s.service_account != name);
566
567 info!("Disabled service account: {}", name);
568 Ok(())
569 }
570
571 pub fn create_api_key(
577 &self,
578 service_account: &str,
579 description: Option<&str>,
580 expires_in: Option<Duration>,
581 permissions: Vec<String>,
582 ) -> ServiceAuthResult<String> {
583 {
585 let accounts = self.service_accounts.read();
586 if !accounts.contains_key(service_account) {
587 return Err(ServiceAuthError::ServiceAccountNotFound(
588 service_account.to_string(),
589 ));
590 }
591 }
592
593 let (api_key, full_key) =
594 ApiKey::generate(service_account, description, expires_in, permissions)?;
595
596 let key_id = api_key.key_id.clone();
597 self.api_keys.write().insert(key_id.clone(), api_key);
598
599 info!(
600 "Created API key '{}' for service account '{}'",
601 key_id, service_account
602 );
603
604 Ok(full_key)
605 }
606
607 pub fn revoke_api_key(&self, key_id: &str) -> ServiceAuthResult<()> {
609 let mut keys = self.api_keys.write();
610
611 let key = keys
612 .get_mut(key_id)
613 .ok_or_else(|| ServiceAuthError::KeyNotFound(key_id.to_string()))?;
614
615 key.revoked = true;
616
617 let mut sessions = self.sessions.write();
619 sessions.retain(|_, s| s.api_key_id.as_deref() != Some(key_id));
620
621 info!("Revoked API key: {}", key_id);
622 Ok(())
623 }
624
625 pub fn list_api_keys(&self, service_account: &str) -> Vec<ApiKey> {
627 self.api_keys
628 .read()
629 .values()
630 .filter(|k| k.service_account == service_account)
631 .cloned()
632 .collect()
633 }
634
635 pub fn authenticate_api_key(
641 &self,
642 key_string: &str,
643 client_ip: &str,
644 ) -> ServiceAuthResult<ServiceSession> {
645 {
647 let mut limiter = self.rate_limiter.write();
648 if !limiter.check_and_record(client_ip) {
649 warn!("Rate limited auth attempt from {}", client_ip);
650 return Err(ServiceAuthError::RateLimited);
651 }
652 }
653
654 let (key_id, secret) = ApiKey::parse_key(key_string)?;
656
657 let mut keys = self.api_keys.write();
659 let api_key = keys
660 .get_mut(&key_id)
661 .ok_or_else(|| ServiceAuthError::KeyNotFound(key_id.clone()))?;
662
663 if !api_key.verify_secret(&secret) {
665 warn!("Invalid API key secret for key_id={}", key_id);
666 return Err(ServiceAuthError::InvalidCredentials);
667 }
668
669 if !api_key.is_valid() {
671 if api_key.revoked {
672 return Err(ServiceAuthError::KeyRevoked);
673 } else {
674 return Err(ServiceAuthError::KeyExpired);
675 }
676 }
677
678 if !api_key.is_ip_allowed(client_ip) {
680 warn!("API key {} used from non-allowed IP {}", key_id, client_ip);
681 return Err(ServiceAuthError::PermissionDenied(
682 "IP not in allowlist".to_string(),
683 ));
684 }
685
686 {
688 let accounts = self.service_accounts.read();
689 let account = accounts.get(&api_key.service_account).ok_or_else(|| {
690 ServiceAuthError::ServiceAccountNotFound(api_key.service_account.clone())
691 })?;
692
693 if !account.enabled {
694 return Err(ServiceAuthError::ServiceAccountDisabled(
695 api_key.service_account.clone(),
696 ));
697 }
698 }
699
700 api_key.last_used_at = Some(SystemTime::now());
702
703 let session = self.create_session(
705 &api_key.service_account,
706 AuthMethod::ApiKey,
707 client_ip,
708 api_key.permissions.clone(),
709 Some(key_id.clone()),
710 );
711
712 self.rate_limiter.write().clear(client_ip);
714
715 info!(
716 "Authenticated service '{}' via API key '{}' from {}",
717 api_key.service_account, key_id, client_ip
718 );
719
720 Ok(session)
721 }
722
723 pub fn authenticate_certificate(
725 &self,
726 cert_subject: &str,
727 client_ip: &str,
728 ) -> ServiceAuthResult<ServiceSession> {
729 let accounts = self.service_accounts.read();
731 let account = accounts
732 .values()
733 .find(|a| a.certificate_subject.as_deref() == Some(cert_subject))
734 .ok_or_else(|| {
735 ServiceAuthError::CertificateError(format!(
736 "No service account for certificate: {}",
737 cert_subject
738 ))
739 })?;
740
741 if !account.enabled {
742 return Err(ServiceAuthError::ServiceAccountDisabled(
743 account.name.clone(),
744 ));
745 }
746
747 let permissions = account.roles.clone();
749 let session = self.create_session(
750 &account.name,
751 AuthMethod::MutualTls,
752 client_ip,
753 permissions,
754 None,
755 );
756
757 info!(
758 "Authenticated service '{}' via mTLS certificate from {}",
759 account.name, client_ip
760 );
761
762 Ok(session)
763 }
764
765 fn create_session(
767 &self,
768 service_account: &str,
769 auth_method: AuthMethod,
770 client_ip: &str,
771 permissions: Vec<String>,
772 api_key_id: Option<String>,
773 ) -> ServiceSession {
774 let rng = SystemRandom::new();
775 let mut session_id_bytes = [0u8; 16];
776 rng.fill(&mut session_id_bytes).expect("RNG failed");
777 let session_id = hex::encode(session_id_bytes);
778
779 let now = Instant::now();
780 let session = ServiceSession {
781 id: session_id.clone(),
782 service_account: service_account.to_string(),
783 auth_method,
784 expires_at: now + self.session_duration,
785 created_timestamp: SystemTime::now(),
786 permissions,
787 client_ip: client_ip.to_string(),
788 api_key_id,
789 };
790
791 self.sessions.write().insert(session_id, session.clone());
792 session
793 }
794
795 pub fn validate_session(&self, session_id: &str) -> Option<ServiceSession> {
797 let sessions = self.sessions.read();
798 let session = sessions.get(session_id)?;
799
800 if session.is_expired() {
801 return None;
802 }
803
804 let accounts = self.service_accounts.read();
806 let account = accounts.get(&session.service_account)?;
807 if !account.enabled {
808 return None;
809 }
810
811 Some(session.clone())
812 }
813
814 pub fn invalidate_session(&self, session_id: &str) {
816 self.sessions.write().remove(session_id);
817 }
818
819 pub fn cleanup_expired_sessions(&self) {
821 let mut sessions = self.sessions.write();
822 let before = sessions.len();
823 sessions.retain(|_, s| !s.is_expired());
824 let removed = before - sessions.len();
825 if removed > 0 {
826 debug!("Cleaned up {} expired service sessions", removed);
827 }
828 }
829}
830
831impl Default for ServiceAuthManager {
832 fn default() -> Self {
833 Self::new()
834 }
835}
836
837#[derive(Debug, Clone, Serialize, Deserialize)]
843pub enum ServiceAuthRequest {
844 ApiKey { key: String },
846
847 MutualTls { certificate_subject: String },
849
850 OidcClientCredentials {
852 client_id: String,
853 client_secret: String,
854 },
855}
856
857#[derive(Debug, Clone, Serialize, Deserialize)]
859pub enum ServiceAuthResponse {
860 Success {
862 session_id: String,
863 expires_in_secs: u64,
864 permissions: Vec<String>,
865 },
866
867 Failure { error: String },
869}
870
871#[derive(Debug, Clone, Serialize, Deserialize)]
877pub struct ServiceAuthConfig {
878 #[serde(default = "default_true")]
880 pub api_key_enabled: bool,
881
882 #[serde(default)]
884 pub mtls_enabled: bool,
885
886 #[serde(default)]
888 pub oidc_enabled: bool,
889
890 #[serde(default = "default_session_duration")]
892 pub session_duration_secs: u64,
893
894 #[serde(default = "default_max_attempts")]
896 pub max_auth_attempts: usize,
897
898 #[serde(default)]
900 pub service_accounts: Vec<ServiceAccountConfig>,
901}
902
903fn default_true() -> bool {
904 true
905}
906
907fn default_session_duration() -> u64 {
908 3600
909}
910
911fn default_max_attempts() -> usize {
912 10
913}
914
915#[derive(Debug, Clone, Serialize, Deserialize)]
916pub struct ServiceAccountConfig {
917 pub name: String,
918 pub description: Option<String>,
919 pub roles: Vec<String>,
920 pub certificate_subject: Option<String>,
921 pub oidc_client_id: Option<String>,
922}
923
924impl Default for ServiceAuthConfig {
925 fn default() -> Self {
926 Self {
927 api_key_enabled: true,
928 mtls_enabled: false,
929 oidc_enabled: false,
930 session_duration_secs: 3600,
931 max_auth_attempts: 10,
932 service_accounts: vec![],
933 }
934 }
935}
936
937#[cfg(test)]
942mod tests {
943 use super::*;
944
945 #[test]
946 fn test_api_key_generation() {
947 let (api_key, full_key) =
948 ApiKey::generate("test-service", Some("Test key"), None, vec![]).unwrap();
949
950 assert!(!api_key.revoked);
951 assert!(api_key.is_valid());
952 assert!(full_key.starts_with("rvn.v1."));
953
954 let (key_id, secret) = ApiKey::parse_key(&full_key).unwrap();
956 assert_eq!(key_id, api_key.key_id);
957 assert!(api_key.verify_secret(&secret));
958 }
959
960 #[test]
961 fn test_api_key_expiration() {
962 let (mut api_key, _) = ApiKey::generate(
963 "test-service",
964 None,
965 Some(Duration::from_secs(0)), vec![],
967 )
968 .unwrap();
969
970 std::thread::sleep(Duration::from_millis(10));
972 assert!(!api_key.is_valid());
973
974 api_key.revoked = true;
976 assert!(!api_key.is_valid());
977 }
978
979 #[test]
980 fn test_ip_allowlist() {
981 let mut api_key = ApiKey::generate("test-service", None, None, vec![])
982 .unwrap()
983 .0;
984
985 assert!(api_key.is_ip_allowed("192.168.1.1"));
987
988 api_key.allowed_ips = vec!["192.168.1.0/24".to_string()];
990 assert!(api_key.is_ip_allowed("192.168.1.100"));
991 assert!(!api_key.is_ip_allowed("10.0.0.1"));
992 }
993
994 #[test]
995 fn test_service_auth_manager() {
996 let manager = ServiceAuthManager::new();
997
998 let account = ServiceAccount::new("connector-postgres")
1000 .with_description("PostgreSQL CDC connector")
1001 .with_roles(vec!["connector".to_string()]);
1002
1003 manager.create_service_account(account).unwrap();
1004
1005 let full_key = manager
1007 .create_api_key(
1008 "connector-postgres",
1009 Some("Production key"),
1010 None,
1011 vec!["topic:read".to_string(), "topic:write".to_string()],
1012 )
1013 .unwrap();
1014
1015 let session = manager
1017 .authenticate_api_key(&full_key, "127.0.0.1")
1018 .unwrap();
1019
1020 assert_eq!(session.service_account, "connector-postgres");
1021 assert_eq!(session.auth_method, AuthMethod::ApiKey);
1022 assert!(!session.is_expired());
1023
1024 let validated = manager.validate_session(&session.id).unwrap();
1026 assert_eq!(validated.id, session.id);
1027 }
1028
1029 #[test]
1030 fn test_invalid_api_key() {
1031 let manager = ServiceAuthManager::new();
1032
1033 manager
1035 .create_service_account(ServiceAccount::new("test"))
1036 .unwrap();
1037
1038 let result = manager.authenticate_api_key(
1040 "rvn.v1.invalid1.secretsecretsecretsecretsecretsecr",
1041 "127.0.0.1",
1042 );
1043 assert!(matches!(result, Err(ServiceAuthError::KeyNotFound(_))));
1044 }
1045
1046 #[test]
1047 fn test_rate_limiting() {
1048 let manager = ServiceAuthManager::new();
1049
1050 for _ in 0..15 {
1052 let _ = manager.authenticate_api_key(
1053 "rvn.v1.invalid1.secretsecretsecretsecretsecretsecr",
1054 "1.2.3.4",
1055 );
1056 }
1057
1058 let result = manager.authenticate_api_key(
1060 "rvn.v1.invalid1.secretsecretsecretsecretsecretsecr",
1061 "1.2.3.4",
1062 );
1063 assert!(matches!(result, Err(ServiceAuthError::RateLimited)));
1064 }
1065
1066 #[test]
1067 fn test_service_account_disable() {
1068 let manager = ServiceAuthManager::new();
1069
1070 manager
1072 .create_service_account(ServiceAccount::new("test-service"))
1073 .unwrap();
1074
1075 let key = manager
1076 .create_api_key("test-service", None, None, vec![])
1077 .unwrap();
1078
1079 let session = manager.authenticate_api_key(&key, "127.0.0.1").unwrap();
1080
1081 manager.disable_service_account("test-service").unwrap();
1083
1084 assert!(manager.validate_session(&session.id).is_none());
1086
1087 let result = manager.authenticate_api_key(&key, "127.0.0.1");
1089 assert!(matches!(
1090 result,
1091 Err(ServiceAuthError::ServiceAccountDisabled(_))
1092 ));
1093 }
1094
1095 #[test]
1096 fn test_certificate_auth() {
1097 let manager = ServiceAuthManager::new();
1098
1099 let account = ServiceAccount::new("connector-orders")
1101 .with_certificate_subject("CN=connector-orders,O=Rivven")
1102 .with_roles(vec!["connector".to_string()]);
1103
1104 manager.create_service_account(account).unwrap();
1105
1106 let session = manager
1108 .authenticate_certificate("CN=connector-orders,O=Rivven", "127.0.0.1")
1109 .unwrap();
1110
1111 assert_eq!(session.service_account, "connector-orders");
1112 assert_eq!(session.auth_method, AuthMethod::MutualTls);
1113 }
1114
1115 #[test]
1116 fn test_api_key_debug_redacts_secret_hash() {
1117 let (api_key, _) = ApiKey::generate(
1118 "test-service",
1119 Some("Test key"),
1120 None,
1121 vec!["read".to_string()],
1122 )
1123 .unwrap();
1124
1125 let debug_output = format!("{:?}", api_key);
1126
1127 assert!(
1129 debug_output.contains("[REDACTED]"),
1130 "Debug output should contain [REDACTED]: {}",
1131 debug_output
1132 );
1133
1134 assert!(
1136 !debug_output.contains(&api_key.secret_hash),
1137 "Debug output should not contain the secret hash"
1138 );
1139
1140 assert!(
1142 debug_output.contains("key_id"),
1143 "Debug output should show key_id field"
1144 );
1145 assert!(
1146 debug_output.contains("test-service"),
1147 "Debug output should show service_account"
1148 );
1149 }
1150}