1use argon2::{
61 password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
62 Argon2,
63};
64use parking_lot::RwLock;
65use ring::rand::{SecureRandom, SystemRandom};
66use serde::{Deserialize, Serialize};
67use sha2::{Digest, Sha256};
68use std::collections::HashMap;
69use std::time::{Duration, Instant, SystemTime};
70use thiserror::Error;
71use tracing::{debug, info, warn};
72
73#[derive(Error, Debug)]
78pub enum ServiceAuthError {
79 #[error("Invalid API key format")]
80 InvalidKeyFormat,
81
82 #[error("API key not found: {0}")]
83 KeyNotFound(String),
84
85 #[error("API key expired")]
86 KeyExpired,
87
88 #[error("API key revoked")]
89 KeyRevoked,
90
91 #[error("Invalid credentials")]
92 InvalidCredentials,
93
94 #[error("Certificate error: {0}")]
95 CertificateError(String),
96
97 #[error("Service account not found: {0}")]
98 ServiceAccountNotFound(String),
99
100 #[error("Service account disabled: {0}")]
101 ServiceAccountDisabled(String),
102
103 #[error("Permission denied: {0}")]
104 PermissionDenied(String),
105
106 #[error("Rate limited: too many authentication attempts")]
107 RateLimited,
108
109 #[error("Internal error: {0}")]
110 Internal(String),
111}
112
113pub type ServiceAuthResult<T> = Result<T, ServiceAuthError>;
114
115#[derive(Clone, Serialize, Deserialize)]
126pub struct ApiKey {
127 pub key_id: String,
129
130 pub secret_hash: String,
132
133 pub service_account: String,
135
136 pub description: Option<String>,
138
139 pub created_at: SystemTime,
141
142 pub expires_at: Option<SystemTime>,
144
145 pub last_used_at: Option<SystemTime>,
147
148 pub revoked: bool,
150
151 pub allowed_ips: Vec<String>,
153
154 pub permissions: Vec<String>,
156}
157
158impl std::fmt::Debug for ApiKey {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 f.debug_struct("ApiKey")
161 .field("key_id", &self.key_id)
162 .field("secret_hash", &"[REDACTED]")
163 .field("service_account", &self.service_account)
164 .field("description", &self.description)
165 .field("created_at", &self.created_at)
166 .field("expires_at", &self.expires_at)
167 .field("last_used_at", &self.last_used_at)
168 .field("revoked", &self.revoked)
169 .field("allowed_ips", &self.allowed_ips)
170 .field("permissions", &self.permissions)
171 .finish()
172 }
173}
174
175impl ApiKey {
176 pub fn generate(
178 service_account: &str,
179 description: Option<&str>,
180 expires_in: Option<Duration>,
181 permissions: Vec<String>,
182 ) -> ServiceAuthResult<(Self, String)> {
183 let rng = SystemRandom::new();
184
185 let mut key_id_bytes = [0u8; 8];
187 rng.fill(&mut key_id_bytes)
188 .map_err(|_| ServiceAuthError::Internal("RNG failed".into()))?;
189 let key_id = hex::encode(key_id_bytes);
190
191 let mut secret_bytes = [0u8; 32];
193 rng.fill(&mut secret_bytes)
194 .map_err(|_| ServiceAuthError::Internal("RNG failed".into()))?;
195 let secret = base64::Engine::encode(
196 &base64::engine::general_purpose::STANDARD_NO_PAD,
197 secret_bytes,
198 );
199
200 let secret_hash = Self::hash_secret(&secret);
202
203 let full_key = format!("rvn.v1.{}.{}", key_id, secret);
205
206 let now = SystemTime::now();
207 let expires_at = expires_in.map(|d| now + d);
208
209 let api_key = Self {
210 key_id,
211 secret_hash,
212 service_account: service_account.to_string(),
213 description: description.map(|s| s.to_string()),
214 created_at: now,
215 expires_at,
216 last_used_at: None,
217 revoked: false,
218 allowed_ips: vec![],
219 permissions,
220 };
221
222 Ok((api_key, full_key))
223 }
224
225 fn hash_secret(secret: &str) -> String {
227 let rng = SystemRandom::new();
229 let mut salt_bytes = [0u8; 16];
230 rng.fill(&mut salt_bytes)
231 .expect("SystemRandom fill should not fail");
232 let salt = SaltString::encode_b64(&salt_bytes).expect("salt encoding should not fail");
233 let argon2 = Argon2::default();
234 argon2
235 .hash_password(secret.as_bytes(), &salt)
236 .expect("Argon2 hashing should not fail")
237 .to_string()
238 }
239
240 fn hash_secret_sha256(secret: &str) -> String {
242 let mut hasher = Sha256::new();
243 hasher.update(secret.as_bytes());
244 hex::encode(hasher.finalize())
245 }
246
247 pub fn parse_key(key: &str) -> ServiceAuthResult<(String, String)> {
249 let parts: Vec<&str> = key.split('.').collect();
250 if parts.len() != 4 || parts[0] != "rvn" || parts[1] != "v1" {
251 return Err(ServiceAuthError::InvalidKeyFormat);
252 }
253
254 let key_id = parts[2].to_string();
255 let secret = parts[3].to_string();
256
257 Ok((key_id, secret))
258 }
259
260 pub fn verify_secret(&self, secret: &str) -> bool {
265 if self.secret_hash.starts_with("$argon2") {
266 match PasswordHash::new(&self.secret_hash) {
268 Ok(parsed_hash) => Argon2::default()
269 .verify_password(secret.as_bytes(), &parsed_hash)
270 .is_ok(),
271 Err(_) => false,
272 }
273 } else {
274 let provided_hash = Self::hash_secret_sha256(secret);
276 if provided_hash.len() != self.secret_hash.len() {
277 return false;
278 }
279 let mut result = 0u8;
280 for (a, b) in provided_hash
281 .as_bytes()
282 .iter()
283 .zip(self.secret_hash.as_bytes())
284 {
285 result |= a ^ b;
286 }
287 result == 0
288 }
289 }
290
291 pub fn is_valid(&self) -> bool {
293 if self.revoked {
294 return false;
295 }
296
297 if let Some(expires_at) = self.expires_at {
298 if SystemTime::now() > expires_at {
299 return false;
300 }
301 }
302
303 true
304 }
305
306 pub fn is_ip_allowed(&self, ip: &str) -> bool {
308 if self.allowed_ips.is_empty() {
309 return true;
310 }
311
312 self.allowed_ips.iter().any(|allowed| {
313 if allowed.contains('/') {
315 Self::ip_in_cidr(ip, allowed)
316 } else {
317 allowed == ip
318 }
319 })
320 }
321
322 fn ip_in_cidr(ip: &str, cidr: &str) -> bool {
323 use std::net::IpAddr;
324
325 let parts: Vec<&str> = cidr.split('/').collect();
326 if parts.len() != 2 {
327 return false;
328 }
329
330 let prefix_len: u32 = match parts[1].parse() {
331 Ok(v) => v,
332 Err(_) => return false,
333 };
334
335 let ip_addr: IpAddr = match ip.parse() {
336 Ok(v) => v,
337 Err(_) => return false,
338 };
339 let net_addr: IpAddr = match parts[0].parse() {
340 Ok(v) => v,
341 Err(_) => return false,
342 };
343
344 match (ip_addr, net_addr) {
345 (IpAddr::V4(ip4), IpAddr::V4(net4)) => {
346 if prefix_len > 32 {
347 return false;
348 }
349 let mask = if prefix_len == 0 {
350 0u32
351 } else {
352 !0u32 << (32 - prefix_len)
353 };
354 let ip_num = u32::from(ip4);
355 let net_num = u32::from(net4);
356 (ip_num & mask) == (net_num & mask)
357 }
358 (IpAddr::V6(ip6), IpAddr::V6(net6)) => {
359 if prefix_len > 128 {
360 return false;
361 }
362 let ip_bits = u128::from(ip6);
363 let net_bits = u128::from(net6);
364 let mask = if prefix_len == 0 {
365 0u128
366 } else {
367 !0u128 << (128 - prefix_len)
368 };
369 (ip_bits & mask) == (net_bits & mask)
370 }
371 _ => false, }
373 }
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct ServiceAccount {
383 pub name: String,
385
386 pub description: Option<String>,
388
389 pub enabled: bool,
391
392 pub created_at: SystemTime,
394
395 pub roles: Vec<String>,
397
398 pub metadata: HashMap<String, String>,
400
401 pub certificate_subject: Option<String>,
403
404 pub oidc_client_id: Option<String>,
406}
407
408impl ServiceAccount {
409 pub fn new(name: impl Into<String>) -> Self {
410 Self {
411 name: name.into(),
412 description: None,
413 enabled: true,
414 created_at: SystemTime::now(),
415 roles: vec![],
416 metadata: HashMap::new(),
417 certificate_subject: None,
418 oidc_client_id: None,
419 }
420 }
421
422 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
423 self.description = Some(desc.into());
424 self
425 }
426
427 pub fn with_roles(mut self, roles: Vec<String>) -> Self {
428 self.roles = roles;
429 self
430 }
431
432 pub fn with_certificate_subject(mut self, subject: impl Into<String>) -> Self {
433 self.certificate_subject = Some(subject.into());
434 self
435 }
436
437 pub fn with_oidc_client_id(mut self, client_id: impl Into<String>) -> Self {
438 self.oidc_client_id = Some(client_id.into());
439 self
440 }
441}
442
443#[derive(Debug, Clone)]
449pub struct ServiceSession {
450 pub id: String,
452
453 pub service_account: String,
455
456 pub auth_method: AuthMethod,
458
459 expires_at: Instant,
461
462 pub created_timestamp: SystemTime,
464
465 pub permissions: Vec<String>,
467
468 pub client_ip: String,
470
471 pub api_key_id: Option<String>,
473}
474
475impl ServiceSession {
476 pub fn is_expired(&self) -> bool {
477 Instant::now() > self.expires_at
478 }
479
480 pub fn time_until_expiration(&self) -> Duration {
482 self.expires_at.saturating_duration_since(Instant::now())
483 }
484
485 pub fn expires_in_secs(&self) -> u64 {
487 self.time_until_expiration().as_secs()
488 }
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
493pub enum AuthMethod {
494 ApiKey,
495 MutualTls,
496 OidcClientCredentials,
497 SaslScram,
498}
499
500struct AuthRateLimiter {
505 attempts: HashMap<String, Vec<Instant>>,
506 max_attempts: usize,
507 window: Duration,
508}
509
510impl AuthRateLimiter {
511 fn new(max_attempts: usize, window: Duration) -> Self {
512 Self {
513 attempts: HashMap::new(),
514 max_attempts,
515 window,
516 }
517 }
518
519 fn check_and_record(&mut self, key: &str) -> bool {
520 let now = Instant::now();
521 let cutoff = now - self.window;
522
523 let attempts = self.attempts.entry(key.to_string()).or_default();
524
525 attempts.retain(|&t| t > cutoff);
527
528 if attempts.len() >= self.max_attempts {
530 return false;
531 }
532
533 attempts.push(now);
535 true
536 }
537
538 fn clear(&mut self, key: &str) {
539 self.attempts.remove(key);
540 }
541}
542
543pub struct ServiceAuthManager {
549 api_keys: RwLock<HashMap<String, ApiKey>>,
551
552 service_accounts: RwLock<HashMap<String, ServiceAccount>>,
554
555 sessions: RwLock<HashMap<String, ServiceSession>>,
557
558 rate_limiter: RwLock<AuthRateLimiter>,
560
561 session_duration: Duration,
563}
564
565impl ServiceAuthManager {
566 pub fn new() -> Self {
567 Self {
568 api_keys: RwLock::new(HashMap::new()),
569 service_accounts: RwLock::new(HashMap::new()),
570 sessions: RwLock::new(HashMap::new()),
571 rate_limiter: RwLock::new(AuthRateLimiter::new(10, Duration::from_secs(60))),
572 session_duration: Duration::from_secs(3600), }
574 }
575
576 pub fn with_session_duration(mut self, duration: Duration) -> Self {
577 self.session_duration = duration;
578 self
579 }
580
581 pub fn create_service_account(&self, account: ServiceAccount) -> ServiceAuthResult<()> {
587 let mut accounts = self.service_accounts.write();
588
589 if accounts.contains_key(&account.name) {
590 return Err(ServiceAuthError::Internal(format!(
591 "Service account '{}' already exists",
592 account.name
593 )));
594 }
595
596 info!("Created service account: {}", account.name);
597 accounts.insert(account.name.clone(), account);
598 Ok(())
599 }
600
601 pub fn get_service_account(&self, name: &str) -> Option<ServiceAccount> {
603 self.service_accounts.read().get(name).cloned()
604 }
605
606 pub fn disable_service_account(&self, name: &str) -> ServiceAuthResult<()> {
608 let mut accounts = self.service_accounts.write();
609
610 let account = accounts
611 .get_mut(name)
612 .ok_or_else(|| ServiceAuthError::ServiceAccountNotFound(name.to_string()))?;
613
614 account.enabled = false;
615
616 let mut sessions = self.sessions.write();
618 sessions.retain(|_, s| s.service_account != name);
619
620 info!("Disabled service account: {}", name);
621 Ok(())
622 }
623
624 pub fn create_api_key(
630 &self,
631 service_account: &str,
632 description: Option<&str>,
633 expires_in: Option<Duration>,
634 permissions: Vec<String>,
635 ) -> ServiceAuthResult<String> {
636 {
638 let accounts = self.service_accounts.read();
639 if !accounts.contains_key(service_account) {
640 return Err(ServiceAuthError::ServiceAccountNotFound(
641 service_account.to_string(),
642 ));
643 }
644 }
645
646 let (api_key, full_key) =
647 ApiKey::generate(service_account, description, expires_in, permissions)?;
648
649 let key_id = api_key.key_id.clone();
650 self.api_keys.write().insert(key_id.clone(), api_key);
651
652 info!(
653 "Created API key '{}' for service account '{}'",
654 key_id, service_account
655 );
656
657 Ok(full_key)
658 }
659
660 pub fn revoke_api_key(&self, key_id: &str) -> ServiceAuthResult<()> {
662 let mut keys = self.api_keys.write();
663
664 let key = keys
665 .get_mut(key_id)
666 .ok_or_else(|| ServiceAuthError::KeyNotFound(key_id.to_string()))?;
667
668 key.revoked = true;
669
670 let mut sessions = self.sessions.write();
672 sessions.retain(|_, s| s.api_key_id.as_deref() != Some(key_id));
673
674 info!("Revoked API key: {}", key_id);
675 Ok(())
676 }
677
678 pub fn list_api_keys(&self, service_account: &str) -> Vec<ApiKey> {
680 self.api_keys
681 .read()
682 .values()
683 .filter(|k| k.service_account == service_account)
684 .cloned()
685 .collect()
686 }
687
688 pub fn authenticate_api_key(
694 &self,
695 key_string: &str,
696 client_ip: &str,
697 ) -> ServiceAuthResult<ServiceSession> {
698 {
700 let mut limiter = self.rate_limiter.write();
701 if !limiter.check_and_record(client_ip) {
702 warn!("Rate limited auth attempt from {}", client_ip);
703 return Err(ServiceAuthError::RateLimited);
704 }
705 }
706
707 let (key_id, secret) = ApiKey::parse_key(key_string)?;
709
710 let keys = self.api_keys.read();
712 let api_key = keys
713 .get(&key_id)
714 .ok_or_else(|| ServiceAuthError::KeyNotFound(key_id.clone()))?;
715
716 if !api_key.verify_secret(&secret) {
718 warn!("Invalid API key secret for key_id={}", key_id);
719 return Err(ServiceAuthError::InvalidCredentials);
720 }
721
722 if !api_key.is_valid() {
724 if api_key.revoked {
725 return Err(ServiceAuthError::KeyRevoked);
726 } else {
727 return Err(ServiceAuthError::KeyExpired);
728 }
729 }
730
731 if !api_key.is_ip_allowed(client_ip) {
733 warn!("API key {} used from non-allowed IP {}", key_id, client_ip);
734 return Err(ServiceAuthError::PermissionDenied(
735 "IP not in allowlist".to_string(),
736 ));
737 }
738
739 {
741 let accounts = self.service_accounts.read();
742 let account = accounts.get(&api_key.service_account).ok_or_else(|| {
743 ServiceAuthError::ServiceAccountNotFound(api_key.service_account.clone())
744 })?;
745
746 if !account.enabled {
747 return Err(ServiceAuthError::ServiceAccountDisabled(
748 api_key.service_account.clone(),
749 ));
750 }
751 }
752
753 let service_account = api_key.service_account.clone();
754 let permissions = api_key.permissions.clone();
755 drop(keys); if let Some(mut keys) = self.api_keys.try_write() {
759 if let Some(api_key) = keys.get_mut(&key_id) {
760 api_key.last_used_at = Some(SystemTime::now());
761 }
762 }
763
764 let session = self.create_session(
766 &service_account,
767 AuthMethod::ApiKey,
768 client_ip,
769 permissions,
770 Some(key_id.clone()),
771 );
772
773 self.rate_limiter.write().clear(client_ip);
775
776 info!(
777 "Authenticated service '{}' via API key '{}' from {}",
778 service_account, key_id, client_ip
779 );
780
781 Ok(session)
782 }
783
784 pub fn authenticate_certificate(
786 &self,
787 cert_subject: &str,
788 client_ip: &str,
789 ) -> ServiceAuthResult<ServiceSession> {
790 let accounts = self.service_accounts.read();
792 let account = accounts
793 .values()
794 .find(|a| a.certificate_subject.as_deref() == Some(cert_subject))
795 .ok_or_else(|| {
796 ServiceAuthError::CertificateError(format!(
797 "No service account for certificate: {}",
798 cert_subject
799 ))
800 })?;
801
802 if !account.enabled {
803 return Err(ServiceAuthError::ServiceAccountDisabled(
804 account.name.clone(),
805 ));
806 }
807
808 let permissions = account.roles.clone();
810 let session = self.create_session(
811 &account.name,
812 AuthMethod::MutualTls,
813 client_ip,
814 permissions,
815 None,
816 );
817
818 info!(
819 "Authenticated service '{}' via mTLS certificate from {}",
820 account.name, client_ip
821 );
822
823 Ok(session)
824 }
825
826 fn create_session(
828 &self,
829 service_account: &str,
830 auth_method: AuthMethod,
831 client_ip: &str,
832 permissions: Vec<String>,
833 api_key_id: Option<String>,
834 ) -> ServiceSession {
835 let rng = SystemRandom::new();
836 let mut session_id_bytes = [0u8; 16];
837 rng.fill(&mut session_id_bytes).expect("RNG failed");
838 let session_id = hex::encode(session_id_bytes);
839
840 let now = Instant::now();
841 let session = ServiceSession {
842 id: session_id.clone(),
843 service_account: service_account.to_string(),
844 auth_method,
845 expires_at: now + self.session_duration,
846 created_timestamp: SystemTime::now(),
847 permissions,
848 client_ip: client_ip.to_string(),
849 api_key_id,
850 };
851
852 self.sessions.write().insert(session_id, session.clone());
853 session
854 }
855
856 pub fn validate_session(&self, session_id: &str) -> Option<ServiceSession> {
858 let sessions = self.sessions.read();
859 let session = sessions.get(session_id)?;
860
861 if session.is_expired() {
862 return None;
863 }
864
865 let accounts = self.service_accounts.read();
867 let account = accounts.get(&session.service_account)?;
868 if !account.enabled {
869 return None;
870 }
871
872 Some(session.clone())
873 }
874
875 pub fn invalidate_session(&self, session_id: &str) {
877 self.sessions.write().remove(session_id);
878 }
879
880 pub fn cleanup_expired_sessions(&self) {
882 let mut sessions = self.sessions.write();
883 let before = sessions.len();
884 sessions.retain(|_, s| !s.is_expired());
885 let removed = before - sessions.len();
886 if removed > 0 {
887 debug!("Cleaned up {} expired service sessions", removed);
888 }
889 }
890}
891
892impl Default for ServiceAuthManager {
893 fn default() -> Self {
894 Self::new()
895 }
896}
897
898#[derive(Debug, Clone, Serialize, Deserialize)]
904pub enum ServiceAuthRequest {
905 ApiKey { key: String },
907
908 MutualTls { certificate_subject: String },
910
911 OidcClientCredentials {
913 client_id: String,
914 client_secret: String,
915 },
916}
917
918#[derive(Debug, Clone, Serialize, Deserialize)]
920pub enum ServiceAuthResponse {
921 Success {
923 session_id: String,
924 expires_in_secs: u64,
925 permissions: Vec<String>,
926 },
927
928 Failure { error: String },
930}
931
932#[derive(Debug, Clone, Serialize, Deserialize)]
938pub struct ServiceAuthConfig {
939 #[serde(default = "default_true")]
941 pub api_key_enabled: bool,
942
943 #[serde(default)]
945 pub mtls_enabled: bool,
946
947 #[serde(default)]
949 pub oidc_enabled: bool,
950
951 #[serde(default = "default_session_duration")]
953 pub session_duration_secs: u64,
954
955 #[serde(default = "default_max_attempts")]
957 pub max_auth_attempts: usize,
958
959 #[serde(default)]
961 pub service_accounts: Vec<ServiceAccountConfig>,
962}
963
964fn default_true() -> bool {
965 true
966}
967
968fn default_session_duration() -> u64 {
969 3600
970}
971
972fn default_max_attempts() -> usize {
973 10
974}
975
976#[derive(Debug, Clone, Serialize, Deserialize)]
977pub struct ServiceAccountConfig {
978 pub name: String,
979 pub description: Option<String>,
980 pub roles: Vec<String>,
981 pub certificate_subject: Option<String>,
982 pub oidc_client_id: Option<String>,
983}
984
985impl Default for ServiceAuthConfig {
986 fn default() -> Self {
987 Self {
988 api_key_enabled: true,
989 mtls_enabled: false,
990 oidc_enabled: false,
991 session_duration_secs: 3600,
992 max_auth_attempts: 10,
993 service_accounts: vec![],
994 }
995 }
996}
997
998#[cfg(test)]
1003mod tests {
1004 use super::*;
1005
1006 #[test]
1007 fn test_api_key_generation() {
1008 let (api_key, full_key) =
1009 ApiKey::generate("test-service", Some("Test key"), None, vec![]).unwrap();
1010
1011 assert!(!api_key.revoked);
1012 assert!(api_key.is_valid());
1013 assert!(full_key.starts_with("rvn.v1."));
1014
1015 let (key_id, secret) = ApiKey::parse_key(&full_key).unwrap();
1017 assert_eq!(key_id, api_key.key_id);
1018 assert!(api_key.verify_secret(&secret));
1019 }
1020
1021 #[test]
1022 fn test_api_key_expiration() {
1023 let (mut api_key, _) = ApiKey::generate(
1024 "test-service",
1025 None,
1026 Some(Duration::from_secs(0)), vec![],
1028 )
1029 .unwrap();
1030
1031 std::thread::sleep(Duration::from_millis(10));
1033 assert!(!api_key.is_valid());
1034
1035 api_key.revoked = true;
1037 assert!(!api_key.is_valid());
1038 }
1039
1040 #[test]
1041 fn test_ip_allowlist() {
1042 let mut api_key = ApiKey::generate("test-service", None, None, vec![])
1043 .unwrap()
1044 .0;
1045
1046 assert!(api_key.is_ip_allowed("192.168.1.1"));
1048
1049 api_key.allowed_ips = vec!["192.168.1.0/24".to_string()];
1051 assert!(api_key.is_ip_allowed("192.168.1.100"));
1052 assert!(!api_key.is_ip_allowed("10.0.0.1"));
1053 }
1054
1055 #[test]
1056 fn test_service_auth_manager() {
1057 let manager = ServiceAuthManager::new();
1058
1059 let account = ServiceAccount::new("connector-postgres")
1061 .with_description("PostgreSQL CDC connector")
1062 .with_roles(vec!["connector".to_string()]);
1063
1064 manager.create_service_account(account).unwrap();
1065
1066 let full_key = manager
1068 .create_api_key(
1069 "connector-postgres",
1070 Some("Production key"),
1071 None,
1072 vec!["topic:read".to_string(), "topic:write".to_string()],
1073 )
1074 .unwrap();
1075
1076 let session = manager
1078 .authenticate_api_key(&full_key, "127.0.0.1")
1079 .unwrap();
1080
1081 assert_eq!(session.service_account, "connector-postgres");
1082 assert_eq!(session.auth_method, AuthMethod::ApiKey);
1083 assert!(!session.is_expired());
1084
1085 let validated = manager.validate_session(&session.id).unwrap();
1087 assert_eq!(validated.id, session.id);
1088 }
1089
1090 #[test]
1091 fn test_invalid_api_key() {
1092 let manager = ServiceAuthManager::new();
1093
1094 manager
1096 .create_service_account(ServiceAccount::new("test"))
1097 .unwrap();
1098
1099 let result = manager.authenticate_api_key(
1101 "rvn.v1.invalid1.secretsecretsecretsecretsecretsecr",
1102 "127.0.0.1",
1103 );
1104 assert!(matches!(result, Err(ServiceAuthError::KeyNotFound(_))));
1105 }
1106
1107 #[test]
1108 fn test_rate_limiting() {
1109 let manager = ServiceAuthManager::new();
1110
1111 for _ in 0..15 {
1113 let _ = manager.authenticate_api_key(
1114 "rvn.v1.invalid1.secretsecretsecretsecretsecretsecr",
1115 "1.2.3.4",
1116 );
1117 }
1118
1119 let result = manager.authenticate_api_key(
1121 "rvn.v1.invalid1.secretsecretsecretsecretsecretsecr",
1122 "1.2.3.4",
1123 );
1124 assert!(matches!(result, Err(ServiceAuthError::RateLimited)));
1125 }
1126
1127 #[test]
1128 fn test_service_account_disable() {
1129 let manager = ServiceAuthManager::new();
1130
1131 manager
1133 .create_service_account(ServiceAccount::new("test-service"))
1134 .unwrap();
1135
1136 let key = manager
1137 .create_api_key("test-service", None, None, vec![])
1138 .unwrap();
1139
1140 let session = manager.authenticate_api_key(&key, "127.0.0.1").unwrap();
1141
1142 manager.disable_service_account("test-service").unwrap();
1144
1145 assert!(manager.validate_session(&session.id).is_none());
1147
1148 let result = manager.authenticate_api_key(&key, "127.0.0.1");
1150 assert!(matches!(
1151 result,
1152 Err(ServiceAuthError::ServiceAccountDisabled(_))
1153 ));
1154 }
1155
1156 #[test]
1157 fn test_certificate_auth() {
1158 let manager = ServiceAuthManager::new();
1159
1160 let account = ServiceAccount::new("connector-orders")
1162 .with_certificate_subject("CN=connector-orders,O=Rivven")
1163 .with_roles(vec!["connector".to_string()]);
1164
1165 manager.create_service_account(account).unwrap();
1166
1167 let session = manager
1169 .authenticate_certificate("CN=connector-orders,O=Rivven", "127.0.0.1")
1170 .unwrap();
1171
1172 assert_eq!(session.service_account, "connector-orders");
1173 assert_eq!(session.auth_method, AuthMethod::MutualTls);
1174 }
1175
1176 #[test]
1177 fn test_api_key_debug_redacts_secret_hash() {
1178 let (api_key, _) = ApiKey::generate(
1179 "test-service",
1180 Some("Test key"),
1181 None,
1182 vec!["read".to_string()],
1183 )
1184 .unwrap();
1185
1186 let debug_output = format!("{:?}", api_key);
1187
1188 assert!(
1190 debug_output.contains("[REDACTED]"),
1191 "Debug output should contain [REDACTED]: {}",
1192 debug_output
1193 );
1194
1195 assert!(
1197 !debug_output.contains(&api_key.secret_hash),
1198 "Debug output should not contain the secret hash"
1199 );
1200
1201 assert!(
1203 debug_output.contains("key_id"),
1204 "Debug output should show key_id field"
1205 );
1206 assert!(
1207 debug_output.contains("test-service"),
1208 "Debug output should show service_account"
1209 );
1210 }
1211}