1#[cfg(feature = "exochain")]
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::Duration;
11
12use async_trait::async_trait;
13use chrono::{DateTime, Utc};
14use dashmap::DashMap;
15use serde::{Deserialize, Serialize};
16use sha2::{Digest, Sha256};
17use tracing::{info, warn};
18
19use crate::error::KernelError;
20use crate::health::HealthStatus;
21use crate::process::Pid;
22use crate::service::{ServiceType, SystemService};
23
24#[cfg(feature = "exochain")]
25use crate::chain::ChainManager;
26#[cfg(feature = "exochain")]
27use crate::gate::GateBackend;
28
29#[non_exhaustive]
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36pub enum CredentialType {
37 ApiKey,
39 BearerToken,
41 Certificate,
43 Custom(String),
45}
46
47impl std::fmt::Display for CredentialType {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::ApiKey => write!(f, "api_key"),
51 Self::BearerToken => write!(f, "bearer_token"),
52 Self::Certificate => write!(f, "certificate"),
53 Self::Custom(s) => write!(f, "custom({s})"),
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
64pub struct StoredCredential {
65 pub name: String,
67 pub credential_type: CredentialType,
69 encrypted_value: Vec<u8>,
71 pub allowed_agents: Vec<String>,
73 pub created_at: DateTime<Utc>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct IssuedToken {
84 pub token_id: String,
86 pub credential_name: String,
88 pub issued_to: Pid,
90 pub issued_at: DateTime<Utc>,
92 pub expires_at: DateTime<Utc>,
94 pub scope: Vec<String>,
96}
97
98impl IssuedToken {
99 pub fn is_expired(&self) -> bool {
101 Utc::now() > self.expires_at
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct CredentialRequest {
112 pub credential_name: String,
114 pub requester_pid: Pid,
116 pub agent_id: String,
118 pub scope: Vec<String>,
120 pub ttl_secs: u64,
122}
123
124#[non_exhaustive]
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub enum CredentialGrant {
128 Granted(IssuedToken),
130 Denied { reason: String },
132}
133
134#[derive(Debug, Clone)]
146pub struct HashedCredential {
147 pub agent_id: String,
149 pub hash: Vec<u8>,
151 pub created_at: DateTime<Utc>,
153 pub scopes: Vec<String>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct AuthToken {
160 pub token_id: String,
162 pub agent_id: String,
164 pub scopes: Vec<String>,
166 pub expires_at: DateTime<Utc>,
168 pub created_at: DateTime<Utc>,
170}
171
172impl AuthToken {
173 pub fn is_expired(&self) -> bool {
175 Utc::now() > self.expires_at
176 }
177}
178
179pub struct AuthService {
190 credentials: DashMap<String, StoredCredential>,
192 hashed_credentials: DashMap<String, HashedCredential>,
194 active_tokens: DashMap<String, IssuedToken>,
196 auth_tokens: DashMap<String, AuthToken>,
198 audit_log: std::sync::RwLock<Vec<AuditEntry>>,
200 encryption_key: [u8; 32],
202 token_counter: AtomicU64,
204 #[cfg(feature = "exochain")]
206 chain_manager: Option<Arc<ChainManager>>,
207 #[cfg(feature = "exochain")]
209 governance_gate: Option<Arc<dyn GateBackend>>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct AuditEntry {
215 pub action: String,
217 pub agent_id: String,
219 pub credential_name: String,
221 pub timestamp: DateTime<Utc>,
223 pub allowed: bool,
225}
226
227impl AuthService {
228 pub fn new(encryption_key: [u8; 32]) -> Self {
230 Self {
231 credentials: DashMap::new(),
232 hashed_credentials: DashMap::new(),
233 active_tokens: DashMap::new(),
234 auth_tokens: DashMap::new(),
235 audit_log: std::sync::RwLock::new(Vec::new()),
236 encryption_key,
237 token_counter: AtomicU64::new(0),
238 #[cfg(feature = "exochain")]
239 chain_manager: None,
240 #[cfg(feature = "exochain")]
241 governance_gate: None,
242 }
243 }
244
245 pub fn new_default() -> Self {
247 Self::new([0u8; 32])
248 }
249
250 #[cfg(feature = "exochain")]
252 pub fn with_chain_manager(mut self, cm: Arc<ChainManager>) -> Self {
253 self.chain_manager = Some(cm);
254 self
255 }
256
257 #[cfg(feature = "exochain")]
259 pub fn with_governance_gate(mut self, gate: Arc<dyn GateBackend>) -> Self {
260 self.governance_gate = Some(gate);
261 self
262 }
263
264 pub fn register_credential(
268 &self,
269 name: &str,
270 credential_type: CredentialType,
271 value: &[u8],
272 allowed_agents: Vec<String>,
273 ) -> Result<(), KernelError> {
274 #[cfg(feature = "exochain")]
276 if let Some(ref gate) = self.governance_gate {
277 use crate::gate::GateDecision;
278 use crate::governance::{GateEffectKind, with_effect_context};
279 let ctx = with_effect_context(
280 serde_json::json!({
281 "credential_name": name,
282 "credential_type": credential_type.to_string(),
283 }),
284 &GateEffectKind::AuthCredentialRegister.effect_vector(),
285 );
286 let decision = gate.check("auth-service", "auth.credential.register", &ctx);
287 if let GateDecision::Deny { reason, .. } = decision {
288 return Err(KernelError::GovernanceDenied(format!(
289 "credential registration denied: {reason}"
290 )));
291 }
292 }
293
294 if self.credentials.contains_key(name) {
295 return Err(KernelError::Service(format!(
296 "credential already registered: {name}"
297 )));
298 }
299
300 let encrypted = self.xor_encrypt(value);
301 #[cfg(feature = "exochain")]
302 let cred_type_for_chain = credential_type.to_string();
303 self.credentials.insert(
304 name.to_string(),
305 StoredCredential {
306 name: name.to_string(),
307 credential_type,
308 encrypted_value: encrypted,
309 allowed_agents,
310 created_at: Utc::now(),
311 },
312 );
313
314 #[cfg(feature = "exochain")]
315 if let Some(ref cm) = self.chain_manager {
316 cm.append(
317 "auth",
318 crate::chain::EVENT_KIND_AUTH_CREDENTIAL_REGISTER,
319 Some(serde_json::json!({
320 "credential_name": name,
321 "credential_type": cred_type_for_chain,
322 })),
323 );
324 }
325
326 info!(name, "credential registered");
327 Ok(())
328 }
329
330 pub fn rotate_credential(&self, name: &str, new_value: &[u8]) -> Result<(), KernelError> {
332 #[cfg(feature = "exochain")]
334 if let Some(ref gate) = self.governance_gate {
335 use crate::gate::GateDecision;
336 use crate::governance::{GateEffectKind, with_effect_context};
337 let ctx = with_effect_context(
338 serde_json::json!({
339 "credential_name": name,
340 }),
341 &GateEffectKind::AuthCredentialRotate.effect_vector(),
342 );
343 let decision = gate.check("auth-service", "auth.credential.rotate", &ctx);
344 if let GateDecision::Deny { reason, .. } = decision {
345 return Err(KernelError::GovernanceDenied(format!(
346 "credential rotation denied: {reason}"
347 )));
348 }
349 }
350
351 let mut cred = self
352 .credentials
353 .get_mut(name)
354 .ok_or_else(|| KernelError::Service(format!("credential not found: {name}")))?;
355 cred.encrypted_value = self.xor_encrypt(new_value);
356
357 #[cfg(feature = "exochain")]
358 if let Some(ref cm) = self.chain_manager {
359 cm.append(
360 "auth",
361 crate::chain::EVENT_KIND_AUTH_CREDENTIAL_ROTATE,
362 Some(serde_json::json!({
363 "credential_name": name,
364 })),
365 );
366 }
367
368 info!(name, "credential rotated");
369 Ok(())
370 }
371
372 pub fn request_token(&self, request: &CredentialRequest) -> Result<IssuedToken, KernelError> {
376 #[cfg(feature = "exochain")]
378 if let Some(ref gate) = self.governance_gate {
379 use crate::gate::GateDecision;
380 use crate::governance::{GateEffectKind, with_effect_context};
381 let ctx = with_effect_context(
382 serde_json::json!({
383 "credential_name": &request.credential_name,
384 "agent_id": &request.agent_id,
385 "ttl_secs": request.ttl_secs,
386 }),
387 &GateEffectKind::AuthTokenIssue.effect_vector(),
388 );
389 let decision = gate.check("auth-service", "auth.token.issue", &ctx);
390 if let GateDecision::Deny { reason, .. } = decision {
391 return Err(KernelError::GovernanceDenied(format!(
392 "token issuance denied: {reason}"
393 )));
394 }
395 }
396
397 let cred = self
398 .credentials
399 .get(&request.credential_name)
400 .ok_or_else(|| {
401 KernelError::Service(format!("credential not found: {}", request.credential_name))
402 })?;
403
404 if !cred.allowed_agents.is_empty() && !cred.allowed_agents.contains(&request.agent_id) {
406 self.audit(
407 "token.denied",
408 &request.agent_id,
409 &request.credential_name,
410 false,
411 );
412 warn!(
413 agent_id = %request.agent_id,
414 credential = %request.credential_name,
415 "token request denied"
416 );
417 return Err(KernelError::CapabilityDenied {
418 pid: request.requester_pid,
419 action: "request_token".into(),
420 reason: format!(
421 "agent '{}' not authorized for credential '{}'",
422 request.agent_id, request.credential_name
423 ),
424 });
425 }
426
427 let ttl = Duration::from_secs(request.ttl_secs.max(1));
428 let token = IssuedToken {
429 token_id: uuid::Uuid::new_v4().to_string(),
430 credential_name: request.credential_name.clone(),
431 issued_to: request.requester_pid,
432 issued_at: Utc::now(),
433 expires_at: Utc::now()
434 + chrono::Duration::from_std(ttl).unwrap_or(chrono::Duration::hours(1)),
435 scope: request.scope.clone(),
436 };
437
438 self.active_tokens
439 .insert(token.token_id.clone(), token.clone());
440 self.audit(
441 "token.issued",
442 &request.agent_id,
443 &request.credential_name,
444 true,
445 );
446
447 #[cfg(feature = "exochain")]
448 if let Some(ref cm) = self.chain_manager {
449 cm.append(
450 "auth",
451 crate::chain::EVENT_KIND_AUTH_TOKEN_ISSUE,
452 Some(serde_json::json!({
453 "token_id": token.token_id,
454 "credential_name": request.credential_name,
455 "agent_id": request.agent_id,
456 "ttl_secs": request.ttl_secs,
457 })),
458 );
459 }
460
461 info!(
462 token_id = %token.token_id,
463 credential = %request.credential_name,
464 agent = %request.agent_id,
465 ttl_secs = request.ttl_secs,
466 "token issued"
467 );
468 Ok(token)
469 }
470
471 pub fn validate_token(&self, token_id: &str) -> Result<IssuedToken, KernelError> {
473 let token = self
474 .active_tokens
475 .get(token_id)
476 .ok_or_else(|| KernelError::Service("token not found".into()))?;
477
478 if token.is_expired() {
479 return Err(KernelError::Service("token expired".into()));
480 }
481
482 Ok(token.clone())
483 }
484
485 pub fn revoke_token(&self, token_id: &str) -> Result<bool, KernelError> {
499 #[cfg(feature = "exochain")]
502 if let Some(ref gate) = self.governance_gate {
503 use crate::gate::GateDecision;
504 use crate::governance::{GateEffectKind, with_effect_context};
505 let ctx = with_effect_context(
506 serde_json::json!({
507 "token_id": token_id,
508 }),
509 &GateEffectKind::AuthTokenRevoke.effect_vector(),
510 );
511 let decision = gate.check("auth-service", "auth.token.revoke", &ctx);
512 if let GateDecision::Deny { reason, .. } = decision {
513 return Err(KernelError::GovernanceDenied(format!(
514 "token revocation denied: {reason}"
515 )));
516 }
517 }
518
519 let removed = self.active_tokens.remove(token_id).is_some();
520
521 #[cfg(feature = "exochain")]
522 if removed && let Some(ref cm) = self.chain_manager {
523 cm.append(
524 "auth",
525 crate::chain::EVENT_KIND_AUTH_TOKEN_REVOKE,
526 Some(serde_json::json!({
527 "token_id": token_id,
528 })),
529 );
530 }
531
532 Ok(removed)
533 }
534
535 pub fn active_token_count(&self) -> usize {
537 self.active_tokens
538 .iter()
539 .filter(|t| !t.value().is_expired())
540 .count()
541 }
542
543 fn sha256_hash(data: &[u8]) -> Vec<u8> {
547 let mut hasher = Sha256::new();
548 hasher.update(data);
549 hasher.finalize().to_vec()
550 }
551
552 pub fn register_hashed_credential(
556 &self,
557 agent_id: &str,
558 raw_credential: &[u8],
559 scopes: Vec<String>,
560 ) -> Result<(), KernelError> {
561 #[cfg(feature = "exochain")]
563 if let Some(ref gate) = self.governance_gate {
564 use crate::gate::GateDecision;
565 use crate::governance::{GateEffectKind, with_effect_context};
566 let ctx = with_effect_context(
567 serde_json::json!({
568 "agent_id": agent_id,
569 }),
570 &GateEffectKind::AuthCredentialRegister.effect_vector(),
571 );
572 let decision = gate.check("auth-service", "auth.credential.register", &ctx);
573 if let GateDecision::Deny { reason, .. } = decision {
574 return Err(KernelError::GovernanceDenied(format!(
575 "hashed credential registration denied: {reason}"
576 )));
577 }
578 }
579
580 if self.hashed_credentials.contains_key(agent_id) {
581 return Err(KernelError::Service(format!(
582 "hashed credential already registered for agent: {agent_id}"
583 )));
584 }
585
586 let hash = Self::sha256_hash(raw_credential);
587 self.hashed_credentials.insert(
588 agent_id.to_string(),
589 HashedCredential {
590 agent_id: agent_id.to_string(),
591 hash,
592 created_at: Utc::now(),
593 scopes,
594 },
595 );
596
597 #[cfg(feature = "exochain")]
598 if let Some(ref cm) = self.chain_manager {
599 cm.append(
600 "auth",
601 crate::chain::EVENT_KIND_AUTH_CREDENTIAL_REGISTER,
602 Some(serde_json::json!({
603 "agent_id": agent_id,
604 "type": "hashed",
605 })),
606 );
607 }
608
609 info!(agent_id, "hashed credential registered");
610 Ok(())
611 }
612
613 pub fn authenticate(
617 &self,
618 agent_id: &str,
619 raw_credential: &[u8],
620 ) -> Result<AuthToken, KernelError> {
621 let cred = self
622 .hashed_credentials
623 .get(agent_id)
624 .ok_or_else(|| KernelError::Service(format!("no credential for agent: {agent_id}")))?;
625
626 let provided_hash = Self::sha256_hash(raw_credential);
627 if cred.hash != provided_hash {
628 self.audit("authenticate.failed", agent_id, agent_id, false);
629
630 #[cfg(feature = "exochain")]
631 if let Some(ref cm) = self.chain_manager {
632 cm.append(
633 "auth",
634 crate::chain::EVENT_KIND_AUTH_ATTEMPT,
635 Some(serde_json::json!({
636 "agent_id": agent_id,
637 "success": false,
638 })),
639 );
640 }
641
642 warn!(agent_id, "authentication failed — hash mismatch");
643 return Err(KernelError::Service("authentication failed".into()));
644 }
645
646 let seq = self.token_counter.fetch_add(1, Ordering::Relaxed);
647 let token = AuthToken {
648 token_id: format!("auth-{}-{seq}", uuid::Uuid::new_v4()),
649 agent_id: agent_id.to_string(),
650 scopes: cred.scopes.clone(),
651 expires_at: Utc::now() + chrono::Duration::hours(1),
652 created_at: Utc::now(),
653 };
654
655 self.auth_tokens
656 .insert(token.token_id.clone(), token.clone());
657 self.audit("authenticate.success", agent_id, agent_id, true);
658
659 #[cfg(feature = "exochain")]
660 if let Some(ref cm) = self.chain_manager {
661 cm.append(
662 "auth",
663 crate::chain::EVENT_KIND_AUTH_ATTEMPT,
664 Some(serde_json::json!({
665 "agent_id": agent_id,
666 "success": true,
667 "token_id": token.token_id,
668 })),
669 );
670 }
671
672 info!(agent_id, token_id = %token.token_id, "agent authenticated");
673 Ok(token)
674 }
675
676 pub fn validate_auth_token(&self, token_id: &str) -> Result<AuthToken, KernelError> {
678 let token = self
679 .auth_tokens
680 .get(token_id)
681 .ok_or_else(|| KernelError::Service("auth token not found".into()))?;
682
683 if token.is_expired() {
684 return Err(KernelError::Service("auth token expired".into()));
685 }
686
687 Ok(token.clone())
688 }
689
690 pub fn revoke_auth_token(&self, token_id: &str) -> bool {
692 self.auth_tokens.remove(token_id).is_some()
693 }
694
695 pub fn check_scope(&self, token_id: &str, required_scope: &str) -> bool {
697 self.auth_tokens
698 .get(token_id)
699 .map(|t| !t.is_expired() && t.scopes.contains(&required_scope.to_string()))
700 .unwrap_or(false)
701 }
702
703 fn audit(&self, action: &str, agent_id: &str, credential_name: &str, allowed: bool) {
706 if let Ok(mut log) = self.audit_log.write() {
707 log.push(AuditEntry {
708 action: action.to_string(),
709 agent_id: agent_id.to_string(),
710 credential_name: credential_name.to_string(),
711 timestamp: Utc::now(),
712 allowed,
713 });
714 }
715 }
716
717 pub fn audit_log(&self) -> Vec<AuditEntry> {
719 self.audit_log.read().map(|l| l.clone()).unwrap_or_default()
720 }
721
722 fn xor_encrypt(&self, data: &[u8]) -> Vec<u8> {
725 data.iter()
726 .enumerate()
727 .map(|(i, b)| b ^ self.encryption_key[i % 32])
728 .collect()
729 }
730}
731
732#[async_trait]
733impl SystemService for AuthService {
734 fn name(&self) -> &str {
735 "auth-service"
736 }
737
738 fn service_type(&self) -> ServiceType {
739 ServiceType::Core
740 }
741
742 async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
743 info!("auth service started");
744 Ok(())
745 }
746
747 async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
748 info!(
749 credentials = self.credentials.len(),
750 active_tokens = self.active_token_count(),
751 "auth service stopped"
752 );
753 Ok(())
754 }
755
756 async fn health_check(&self) -> HealthStatus {
757 HealthStatus::Healthy
758 }
759}
760
761#[cfg(test)]
764mod tests {
765 use super::*;
766
767 fn pid(n: u64) -> Pid {
768 n
769 }
770
771 fn make_request(cred: &str, agent: &str, pid_val: u64) -> CredentialRequest {
772 CredentialRequest {
773 credential_name: cred.to_string(),
774 requester_pid: pid(pid_val),
775 agent_id: agent.to_string(),
776 scope: vec!["read".to_string()],
777 ttl_secs: 3600,
778 }
779 }
780
781 #[test]
782 fn register_and_request_token() {
783 let svc = AuthService::new_default();
784 svc.register_credential(
785 "github-key",
786 CredentialType::ApiKey,
787 b"ghp_secret_value",
788 vec!["deploy-agent".to_string()],
789 )
790 .unwrap();
791
792 let req = make_request("github-key", "deploy-agent", 1);
793 let token = svc.request_token(&req).unwrap();
794 assert!(!token.token_id.is_empty());
795 assert_eq!(token.credential_name, "github-key");
796 }
797
798 #[test]
799 fn unauthorized_agent_denied() {
800 let svc = AuthService::new_default();
801 svc.register_credential(
802 "secret",
803 CredentialType::BearerToken,
804 b"token",
805 vec!["allowed-agent".to_string()],
806 )
807 .unwrap();
808
809 let req = make_request("secret", "other-agent", 2);
810 let result = svc.request_token(&req);
811 assert!(result.is_err());
812 let err = result.unwrap_err().to_string();
813 assert!(
814 err.contains("denied") || err.contains("authorized"),
815 "got: {err}"
816 );
817 }
818
819 #[test]
820 fn empty_allowed_agents_permits_all() {
821 let svc = AuthService::new_default();
822 svc.register_credential(
823 "open-cred",
824 CredentialType::ApiKey,
825 b"value",
826 vec![], )
828 .unwrap();
829
830 let req = make_request("open-cred", "random-agent", 5);
831 let token = svc.request_token(&req).unwrap();
832 assert!(!token.is_expired());
833 }
834
835 #[test]
836 fn validate_token() {
837 let svc = AuthService::new_default();
838 svc.register_credential("cred", CredentialType::ApiKey, b"val", vec![])
839 .unwrap();
840 let req = make_request("cred", "agent", 1);
841 let token = svc.request_token(&req).unwrap();
842 let validated = svc.validate_token(&token.token_id).unwrap();
843 assert_eq!(validated.token_id, token.token_id);
844 }
845
846 #[test]
847 fn validate_nonexistent_token_fails() {
848 let svc = AuthService::new_default();
849 assert!(svc.validate_token("no-such-token").is_err());
850 }
851
852 #[test]
853 fn revoke_token() {
854 let svc = AuthService::new_default();
855 svc.register_credential("cred", CredentialType::ApiKey, b"val", vec![])
856 .unwrap();
857 let req = make_request("cred", "agent", 1);
858 let token = svc.request_token(&req).unwrap();
859 assert!(svc.revoke_token(&token.token_id).unwrap());
860 assert!(svc.validate_token(&token.token_id).is_err());
861 assert!(!svc.revoke_token(&token.token_id).unwrap());
863 }
864
865 #[cfg(feature = "exochain")]
872 #[test]
873 fn revoke_token_denied_by_governance() {
874 use crate::gate::{GateBackend, GateDecision};
875 use std::sync::Arc;
876
877 struct RevokeDenyGate;
878 impl GateBackend for RevokeDenyGate {
879 fn check(&self, _source: &str, action: &str, _ctx: &serde_json::Value) -> GateDecision {
880 if action == "auth.token.revoke" {
881 GateDecision::Deny {
882 reason: "policy: revoke disabled".into(),
883 receipt: None,
884 }
885 } else {
886 GateDecision::Permit { token: None }
887 }
888 }
889 }
890
891 let svc = AuthService::new_default()
892 .with_governance_gate(Arc::new(RevokeDenyGate) as Arc<dyn GateBackend>);
893 svc.register_credential("cred", CredentialType::ApiKey, b"val", vec![])
894 .unwrap();
895 let req = make_request("cred", "agent", 1);
896 let token = svc.request_token(&req).unwrap();
897
898 let err = svc.revoke_token(&token.token_id).unwrap_err();
899 match err {
900 KernelError::GovernanceDenied(msg) => {
901 assert!(msg.contains("token revocation denied"), "got: {msg}");
902 }
903 other => panic!("expected GovernanceDenied, got {other:?}"),
904 }
905
906 assert!(svc.validate_token(&token.token_id).is_ok());
909 }
910
911 #[cfg(feature = "exochain")]
916 #[test]
917 fn rotate_allowed_revoke_denied_policy() {
918 use crate::gate::{GateBackend, GateDecision};
919 use std::sync::Arc;
920
921 struct RotateAllowRevokeDeny;
922 impl GateBackend for RotateAllowRevokeDeny {
923 fn check(&self, _source: &str, action: &str, _ctx: &serde_json::Value) -> GateDecision {
924 if action == "auth.token.revoke" {
925 GateDecision::Deny {
926 reason: "policy: agent may rotate but not revoke".into(),
927 receipt: None,
928 }
929 } else {
930 GateDecision::Permit { token: None }
932 }
933 }
934 }
935
936 let svc = AuthService::new_default()
937 .with_governance_gate(Arc::new(RotateAllowRevokeDeny) as Arc<dyn GateBackend>);
938 svc.register_credential("cred", CredentialType::ApiKey, b"old", vec![])
939 .unwrap();
940 let req = make_request("cred", "agent", 1);
941 let token = svc.request_token(&req).unwrap();
942
943 svc.rotate_credential("cred", b"new").unwrap();
945
946 let err = svc.revoke_token(&token.token_id).unwrap_err();
948 match err {
949 KernelError::GovernanceDenied(msg) => {
950 assert!(msg.contains("token revocation denied"), "got: {msg}");
951 }
952 other => panic!("expected GovernanceDenied, got {other:?}"),
953 }
954 assert!(svc.validate_token(&token.token_id).is_ok());
955 }
956
957 #[test]
958 fn credential_rotation_preserves_tokens() {
959 let svc = AuthService::new_default();
960 svc.register_credential("rotate-cred", CredentialType::ApiKey, b"old_val", vec![])
961 .unwrap();
962 let req = make_request("rotate-cred", "agent", 1);
963 let token = svc.request_token(&req).unwrap();
964
965 svc.rotate_credential("rotate-cred", b"new_val").unwrap();
967
968 let validated = svc.validate_token(&token.token_id).unwrap();
970 assert_eq!(validated.credential_name, "rotate-cred");
971 }
972
973 #[test]
974 fn raw_credential_never_exposed() {
975 let key = [0xAB; 32];
976 let svc = AuthService::new(key);
977 svc.register_credential("secret", CredentialType::ApiKey, b"raw_secret", vec![])
978 .unwrap();
979
980 let cred = svc.credentials.get("secret").unwrap();
982 assert_ne!(cred.encrypted_value, b"raw_secret");
983 }
984
985 #[test]
986 fn audit_log_records_events() {
987 let svc = AuthService::new_default();
988 svc.register_credential(
989 "audited",
990 CredentialType::ApiKey,
991 b"val",
992 vec!["agent-a".to_string()],
993 )
994 .unwrap();
995
996 let req = make_request("audited", "agent-a", 1);
998 svc.request_token(&req).unwrap();
999
1000 let bad_req = make_request("audited", "agent-b", 2);
1002 let _ = svc.request_token(&bad_req);
1003
1004 let log = svc.audit_log();
1005 assert_eq!(log.len(), 2);
1006 assert!(log[0].allowed);
1007 assert!(!log[1].allowed);
1008 }
1009
1010 #[test]
1011 fn duplicate_credential_registration_fails() {
1012 let svc = AuthService::new_default();
1013 svc.register_credential("dup", CredentialType::ApiKey, b"val", vec![])
1014 .unwrap();
1015 let result = svc.register_credential("dup", CredentialType::ApiKey, b"val2", vec![]);
1016 assert!(result.is_err());
1017 }
1018
1019 #[test]
1020 fn request_nonexistent_credential_fails() {
1021 let svc = AuthService::new_default();
1022 let req = make_request("missing", "agent", 1);
1023 assert!(svc.request_token(&req).is_err());
1024 }
1025
1026 #[tokio::test]
1027 async fn system_service_impl() {
1028 let svc = AuthService::new_default();
1029 assert_eq!(svc.name(), "auth-service");
1030 assert_eq!(svc.service_type(), ServiceType::Core);
1031 svc.start().await.unwrap();
1032 assert_eq!(svc.health_check().await, HealthStatus::Healthy);
1033 svc.stop().await.unwrap();
1034 }
1035
1036 #[test]
1039 fn register_and_authenticate_success() {
1040 let svc = AuthService::new_default();
1041 svc.register_hashed_credential("agent-1", b"my_password", vec!["read".into()])
1042 .unwrap();
1043 let token = svc.authenticate("agent-1", b"my_password").unwrap();
1044 assert_eq!(token.agent_id, "agent-1");
1045 assert!(!token.token_id.is_empty());
1046 assert_eq!(token.scopes, vec!["read".to_string()]);
1047 }
1048
1049 #[test]
1050 fn authenticate_wrong_credential_fails() {
1051 let svc = AuthService::new_default();
1052 svc.register_hashed_credential("agent-2", b"correct", vec![])
1053 .unwrap();
1054 let result = svc.authenticate("agent-2", b"wrong");
1055 assert!(result.is_err());
1056 let err = result.unwrap_err().to_string();
1057 assert!(err.contains("failed"), "got: {err}");
1058 }
1059
1060 #[test]
1061 fn authenticate_issues_token_with_scopes() {
1062 let svc = AuthService::new_default();
1063 svc.register_hashed_credential(
1064 "scoped-agent",
1065 b"secret",
1066 vec!["read".into(), "write".into()],
1067 )
1068 .unwrap();
1069 let token = svc.authenticate("scoped-agent", b"secret").unwrap();
1070 assert_eq!(token.scopes, vec!["read".to_string(), "write".to_string()]);
1071 }
1072
1073 #[test]
1074 fn validate_auth_token_succeeds() {
1075 let svc = AuthService::new_default();
1076 svc.register_hashed_credential("v-agent", b"pass", vec![])
1077 .unwrap();
1078 let token = svc.authenticate("v-agent", b"pass").unwrap();
1079 let validated = svc.validate_auth_token(&token.token_id).unwrap();
1080 assert_eq!(validated.agent_id, "v-agent");
1081 }
1082
1083 #[test]
1084 fn validate_expired_auth_token_fails() {
1085 let svc = AuthService::new_default();
1086 svc.register_hashed_credential("exp-agent", b"pass", vec![])
1087 .unwrap();
1088 let token = svc.authenticate("exp-agent", b"pass").unwrap();
1089
1090 let expired = AuthToken {
1092 token_id: "expired-tok".to_string(),
1093 agent_id: "exp-agent".to_string(),
1094 scopes: vec![],
1095 expires_at: Utc::now() - chrono::Duration::hours(1),
1096 created_at: Utc::now() - chrono::Duration::hours(2),
1097 };
1098 svc.auth_tokens.insert("expired-tok".into(), expired);
1099
1100 assert!(svc.validate_auth_token(&token.token_id).is_ok());
1102 let result = svc.validate_auth_token("expired-tok");
1104 assert!(result.is_err());
1105 assert!(result.unwrap_err().to_string().contains("expired"));
1106 }
1107
1108 #[test]
1109 fn revoke_auth_token_works() {
1110 let svc = AuthService::new_default();
1111 svc.register_hashed_credential("rev-agent", b"pass", vec![])
1112 .unwrap();
1113 let token = svc.authenticate("rev-agent", b"pass").unwrap();
1114 assert!(svc.revoke_auth_token(&token.token_id));
1115 assert!(svc.validate_auth_token(&token.token_id).is_err());
1116 assert!(!svc.revoke_auth_token(&token.token_id));
1118 }
1119
1120 #[test]
1121 fn check_scope_with_matching_scope() {
1122 let svc = AuthService::new_default();
1123 svc.register_hashed_credential("sc-agent", b"cred", vec!["admin".into(), "read".into()])
1124 .unwrap();
1125 let token = svc.authenticate("sc-agent", b"cred").unwrap();
1126 assert!(svc.check_scope(&token.token_id, "admin"));
1127 assert!(svc.check_scope(&token.token_id, "read"));
1128 }
1129
1130 #[test]
1131 fn check_scope_with_missing_scope_fails() {
1132 let svc = AuthService::new_default();
1133 svc.register_hashed_credential("sc-agent2", b"cred", vec!["read".into()])
1134 .unwrap();
1135 let token = svc.authenticate("sc-agent2", b"cred").unwrap();
1136 assert!(!svc.check_scope(&token.token_id, "write"));
1137 assert!(!svc.check_scope(&token.token_id, "admin"));
1138 }
1139
1140 #[test]
1141 fn raw_credentials_never_stored_in_hash() {
1142 let svc = AuthService::new_default();
1143 let raw = b"super_secret_password";
1144 svc.register_hashed_credential("hash-check", raw, vec![])
1145 .unwrap();
1146
1147 let cred = svc.hashed_credentials.get("hash-check").unwrap();
1148 assert_ne!(cred.hash.as_slice(), raw.as_slice());
1150 assert_eq!(cred.hash.len(), 32);
1152 }
1153
1154 #[test]
1155 fn check_scope_on_nonexistent_token_returns_false() {
1156 let svc = AuthService::new_default();
1157 assert!(!svc.check_scope("no-such-token", "read"));
1158 }
1159}