1use macp_core::error::MacpError;
2use std::collections::{HashMap, HashSet, VecDeque};
3use std::fs;
4use std::path::PathBuf;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7use tokio::sync::Mutex;
8use tonic::metadata::MetadataMap;
9
10#[derive(Clone, Debug)]
11pub struct AuthIdentity {
12 pub sender: String,
13 pub allowed_modes: Option<HashSet<String>>,
14 pub can_start_sessions: bool,
15 pub max_open_sessions: Option<usize>,
16 pub can_manage_mode_registry: bool,
17 pub is_observer: bool,
18}
19
20#[derive(Clone, Debug, serde::Deserialize)]
21struct RawIdentity {
22 token: String,
23 sender: String,
24 #[serde(default)]
25 allowed_modes: Vec<String>,
26 #[serde(default = "default_true")]
27 can_start_sessions: bool,
28 max_open_sessions: Option<usize>,
29 #[serde(default)]
30 can_manage_mode_registry: bool,
31 #[serde(default)]
32 is_observer: bool,
33}
34
35#[derive(Clone, Debug, serde::Deserialize)]
36#[serde(untagged)]
37enum RawConfig {
38 List(Vec<RawIdentity>),
39 Wrapped { tokens: Vec<RawIdentity> },
40}
41
42fn default_true() -> bool {
43 true
44}
45
46#[derive(Clone, Debug)]
47pub struct RateLimitConfig {
48 pub limit: usize,
49 pub window: Duration,
50}
51
52#[derive(Default)]
53struct RateBucket {
54 start_events: Mutex<HashMap<String, VecDeque<Instant>>>,
55 message_events: Mutex<HashMap<String, VecDeque<Instant>>>,
56}
57
58#[derive(Clone)]
59pub struct SecurityLayer {
60 identities: Arc<HashMap<String, AuthIdentity>>,
61 rate_bucket: Arc<RateBucket>,
62 auth_chain: Option<Arc<crate::auth::AuthResolverChain>>,
63 pub max_payload_bytes: usize,
64 session_start_rate: RateLimitConfig,
65 message_rate: RateLimitConfig,
66}
67
68impl SecurityLayer {
69 pub fn dev_mode() -> Self {
73 Self {
74 identities: Arc::new(HashMap::new()),
75 rate_bucket: Arc::new(RateBucket::default()),
76 auth_chain: None,
77 max_payload_bytes: 1_048_576,
78 session_start_rate: RateLimitConfig {
79 limit: usize::MAX,
80 window: Duration::from_secs(60),
81 },
82 message_rate: RateLimitConfig {
83 limit: usize::MAX,
84 window: Duration::from_secs(60),
85 },
86 }
87 }
88
89 fn dev_authenticate(&self, metadata: &MetadataMap) -> Result<AuthIdentity, MacpError> {
92 if let Some(token) = Self::bearer_token(metadata) {
93 return Ok(AuthIdentity {
94 sender: token,
95 allowed_modes: None,
96 can_start_sessions: true,
97 max_open_sessions: None,
98 can_manage_mode_registry: true,
99 is_observer: false,
100 });
101 }
102 Err(MacpError::Unauthenticated)
103 }
104
105 pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
106 let max_payload_bytes = std::env::var("MACP_MAX_PAYLOAD_BYTES")
107 .ok()
108 .and_then(|v| v.parse::<usize>().ok())
109 .unwrap_or(1_048_576);
110
111 let session_start_rate = RateLimitConfig {
112 limit: std::env::var("MACP_SESSION_START_LIMIT_PER_MINUTE")
113 .ok()
114 .and_then(|v| v.parse::<usize>().ok())
115 .unwrap_or(60),
116 window: Duration::from_secs(60),
117 };
118 let message_rate = RateLimitConfig {
119 limit: std::env::var("MACP_MESSAGE_LIMIT_PER_MINUTE")
120 .ok()
121 .and_then(|v| v.parse::<usize>().ok())
122 .unwrap_or(600),
123 window: Duration::from_secs(60),
124 };
125
126 let raw = if let Ok(json) = std::env::var("MACP_AUTH_TOKENS_JSON") {
127 Some(json)
128 } else if let Ok(path) = std::env::var("MACP_AUTH_TOKENS_FILE") {
129 Some(fs::read_to_string(PathBuf::from(path))?)
130 } else {
131 None
132 };
133
134 let identities = raw
135 .as_ref()
136 .map(|json| Self::parse_identities(json))
137 .transpose()?
138 .unwrap_or_default();
139
140 let mut resolvers: Vec<Box<dyn crate::auth::AuthResolver>> = Vec::new();
142
143 if let Ok(issuer) = std::env::var("MACP_AUTH_ISSUER") {
145 let audience =
146 std::env::var("MACP_AUTH_AUDIENCE").unwrap_or_else(|_| "macp-runtime".into());
147 let cache_ttl = std::env::var("MACP_AUTH_JWKS_TTL_SECS")
148 .ok()
149 .and_then(|v| v.parse().ok())
150 .unwrap_or(300u64);
151 let config = crate::auth::resolvers::jwt_bearer::JwtConfig {
152 issuer,
153 audience,
154 algorithms: vec![
155 jsonwebtoken::Algorithm::RS256,
156 jsonwebtoken::Algorithm::ES256,
157 jsonwebtoken::Algorithm::HS256,
158 ],
159 };
160 if let Ok(jwks_json) = std::env::var("MACP_AUTH_JWKS_JSON") {
161 match crate::auth::resolvers::JwtBearerResolver::from_inline_json(
162 config, &jwks_json,
163 ) {
164 Ok(resolver) => resolvers.push(Box::new(resolver)),
165 Err(e) => {
166 tracing::error!("failed to create JWT resolver from inline JWKS: {e}")
167 }
168 }
169 } else if let Ok(jwks_url) = std::env::var("MACP_AUTH_JWKS_URL") {
170 resolvers.push(Box::new(
171 crate::auth::resolvers::JwtBearerResolver::from_url(
172 config, jwks_url, cache_ttl,
173 ),
174 ));
175 }
176 }
177
178 if !identities.is_empty() {
180 resolvers.push(Box::new(crate::auth::resolvers::StaticBearerResolver::new(
181 identities.clone(),
182 )));
183 }
184
185 let auth_chain = if resolvers.is_empty() {
186 None
187 } else {
188 Some(Arc::new(crate::auth::AuthResolverChain::new(resolvers)))
189 };
190
191 Ok(Self {
192 identities: Arc::new(identities),
193 rate_bucket: Arc::new(RateBucket::default()),
194 auth_chain,
195 max_payload_bytes,
196 session_start_rate,
197 message_rate,
198 })
199 }
200
201 fn parse_identities(
202 json: &str,
203 ) -> Result<HashMap<String, AuthIdentity>, Box<dyn std::error::Error>> {
204 let parsed: RawConfig = serde_json::from_str(json)?;
205 let items = match parsed {
206 RawConfig::List(items) => items,
207 RawConfig::Wrapped { tokens } => tokens,
208 };
209 let mut identities = HashMap::new();
210 for item in items {
211 identities.insert(
212 item.token,
213 AuthIdentity {
214 sender: item.sender,
215 allowed_modes: if item.allowed_modes.is_empty() {
216 None
217 } else {
218 Some(item.allowed_modes.into_iter().collect())
219 },
220 can_start_sessions: item.can_start_sessions,
221 max_open_sessions: item.max_open_sessions,
222 can_manage_mode_registry: item.can_manage_mode_registry,
223 is_observer: item.is_observer,
224 },
225 );
226 }
227 Ok(identities)
228 }
229
230 fn bearer_token(metadata: &MetadataMap) -> Option<String> {
231 metadata
232 .get("authorization")
233 .and_then(|value| value.to_str().ok())
234 .and_then(|value| value.strip_prefix("Bearer "))
235 .map(str::to_string)
236 .or_else(|| {
237 metadata
238 .get("x-macp-token")
239 .and_then(|value| value.to_str().ok())
240 .map(str::to_string)
241 })
242 }
243
244 pub fn authenticate_metadata(&self, metadata: &MetadataMap) -> Result<AuthIdentity, MacpError> {
245 if let Some(chain) = &self.auth_chain {
247 let chain = Arc::clone(chain);
248 let metadata_clone = metadata.clone();
249 return tokio::task::block_in_place(|| {
250 tokio::runtime::Handle::current().block_on(chain.authenticate(&metadata_clone))
251 });
252 }
253
254 if !self.identities.is_empty() {
256 if let Some(token) = Self::bearer_token(metadata) {
257 return self
258 .identities
259 .get(&token)
260 .cloned()
261 .ok_or(MacpError::Unauthenticated);
262 }
263 return Err(MacpError::Unauthenticated);
264 }
265
266 self.dev_authenticate(metadata)
268 }
269
270 pub fn authorize_mode(
271 &self,
272 identity: &AuthIdentity,
273 mode: &str,
274 is_session_start: bool,
275 ) -> Result<(), MacpError> {
276 if is_session_start && !identity.can_start_sessions {
277 return Err(MacpError::Forbidden);
278 }
279 if let Some(allowed_modes) = &identity.allowed_modes {
280 if !allowed_modes.contains(mode) {
281 return Err(MacpError::Forbidden);
282 }
283 }
284 Ok(())
285 }
286
287 pub fn authorize_mode_registry(&self, identity: &AuthIdentity) -> Result<(), MacpError> {
288 if identity.can_manage_mode_registry {
289 Ok(())
290 } else {
291 Err(MacpError::Forbidden)
292 }
293 }
294
295 async fn check_bucket(
296 bucket: &Mutex<HashMap<String, VecDeque<Instant>>>,
297 sender: &str,
298 config: &RateLimitConfig,
299 ) -> Result<(), MacpError> {
300 let now = Instant::now();
301 let mut guard = bucket.lock().await;
302
303 let stale_keys: Vec<String> = guard
306 .iter()
307 .filter(|(_, deque)| {
308 deque
309 .back()
310 .map(|last| now.duration_since(*last) > config.window)
311 .unwrap_or(true)
312 })
313 .map(|(k, _)| k.clone())
314 .collect();
315 for key in stale_keys {
316 guard.remove(&key);
317 }
318
319 let deque = guard.entry(sender.to_string()).or_default();
320 while deque
321 .front()
322 .map(|instant| now.duration_since(*instant) > config.window)
323 .unwrap_or(false)
324 {
325 deque.pop_front();
326 }
327 if deque.len() >= config.limit {
328 return Err(MacpError::RateLimited);
329 }
330 deque.push_back(now);
331 Ok(())
332 }
333
334 pub async fn enforce_rate_limit(
335 &self,
336 sender: &str,
337 is_session_start: bool,
338 ) -> Result<(), MacpError> {
339 if is_session_start {
340 Self::check_bucket(
341 &self.rate_bucket.start_events,
342 sender,
343 &self.session_start_rate,
344 )
345 .await
346 } else {
347 Self::check_bucket(&self.rate_bucket.message_events, sender, &self.message_rate).await
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use std::io::Write;
356 use tempfile::NamedTempFile;
357 use tonic::metadata::MetadataMap;
358
359 fn layer_with_tokens(json: &str) -> SecurityLayer {
362 let identities = SecurityLayer::parse_identities(json).expect("valid JSON");
363 SecurityLayer {
364 identities: Arc::new(identities),
365 rate_bucket: Arc::new(RateBucket::default()),
366 auth_chain: None,
367 max_payload_bytes: 1_048_576,
368 session_start_rate: RateLimitConfig {
369 limit: usize::MAX,
370 window: Duration::from_secs(60),
371 },
372 message_rate: RateLimitConfig {
373 limit: usize::MAX,
374 window: Duration::from_secs(60),
375 },
376 }
377 }
378
379 fn insecure_layer() -> SecurityLayer {
381 SecurityLayer {
382 identities: Arc::new(HashMap::new()),
383 rate_bucket: Arc::new(RateBucket::default()),
384 auth_chain: None,
385 max_payload_bytes: 1_048_576,
386 session_start_rate: RateLimitConfig {
387 limit: usize::MAX,
388 window: Duration::from_secs(60),
389 },
390 message_rate: RateLimitConfig {
391 limit: usize::MAX,
392 window: Duration::from_secs(60),
393 },
394 }
395 }
396
397 #[test]
402 fn dev_mode_requires_dev_header() {
403 let layer = SecurityLayer::dev_mode();
404 let meta = MetadataMap::new();
405 let err = layer.authenticate_metadata(&meta).unwrap_err();
406 assert!(matches!(err, MacpError::Unauthenticated));
407 }
408
409 #[test]
410 fn dev_mode_rejects_dev_sender_header() {
411 let layer = SecurityLayer::dev_mode();
412 let mut meta = MetadataMap::new();
413 meta.insert("x-macp-agent-id", "agent://dev-bot".parse().unwrap());
414 let err = layer.authenticate_metadata(&meta).unwrap_err();
415 assert!(matches!(err, MacpError::Unauthenticated));
416 }
417
418 #[test]
419 fn dev_mode_has_unlimited_rate_limits() {
420 let layer = SecurityLayer::dev_mode();
421 assert_eq!(layer.session_start_rate.limit, usize::MAX);
422 assert_eq!(layer.message_rate.limit, usize::MAX);
423 }
424
425 #[test]
430 fn from_env_defaults_without_env_vars() {
431 let layer = insecure_layer();
433 assert_eq!(layer.max_payload_bytes, 1_048_576);
434 }
435
436 #[test]
441 fn bearer_token_authentication_via_authorization_header() {
442 let json = r#"[{"token":"tok-abc","sender":"agent://alice","allowed_modes":[],"can_start_sessions":true}]"#;
443 let layer = layer_with_tokens(json);
444
445 let mut meta = MetadataMap::new();
446 meta.insert("authorization", "Bearer tok-abc".parse().unwrap());
447
448 let id = layer
449 .authenticate_metadata(&meta)
450 .expect("should authenticate");
451 assert_eq!(id.sender, "agent://alice");
452 assert!(id.allowed_modes.is_none()); assert!(id.can_start_sessions);
454 }
455
456 #[test]
457 fn bearer_token_authentication_via_x_macp_token_header() {
458 let json = r#"[{"token":"tok-xyz","sender":"agent://bob"}]"#;
459 let layer = layer_with_tokens(json);
460
461 let mut meta = MetadataMap::new();
462 meta.insert("x-macp-token", "tok-xyz".parse().unwrap());
463
464 let id = layer
465 .authenticate_metadata(&meta)
466 .expect("should authenticate");
467 assert_eq!(id.sender, "agent://bob");
468 }
469
470 #[test]
471 fn invalid_bearer_token_returns_unauthenticated() {
472 let json = r#"[{"token":"tok-real","sender":"agent://alice"}]"#;
473 let layer = layer_with_tokens(json);
474
475 let mut meta = MetadataMap::new();
476 meta.insert("authorization", "Bearer tok-fake".parse().unwrap());
477
478 let err = layer.authenticate_metadata(&meta).unwrap_err();
479 assert!(matches!(err, MacpError::Unauthenticated));
480 }
481
482 #[test]
483 fn no_token_when_auth_required_returns_unauthenticated() {
484 let json = r#"[{"token":"tok-only","sender":"agent://sole"}]"#;
485 let layer = layer_with_tokens(json);
486
487 let meta = MetadataMap::new(); let err = layer.authenticate_metadata(&meta).unwrap_err();
489 assert!(matches!(err, MacpError::Unauthenticated));
490 }
491
492 #[test]
493 fn parse_identities_wrapped_format() {
494 let json = r#"{"tokens":[{"token":"t1","sender":"agent://wrapped"}]}"#;
495 let layer = layer_with_tokens(json);
496
497 let mut meta = MetadataMap::new();
498 meta.insert("authorization", "Bearer t1".parse().unwrap());
499 let id = layer
500 .authenticate_metadata(&meta)
501 .expect("should authenticate");
502 assert_eq!(id.sender, "agent://wrapped");
503 }
504
505 #[test]
506 fn parse_identities_with_allowed_modes() {
507 let json = r#"[{"token":"t-modes","sender":"agent://limited","allowed_modes":["macp.mode.decision.v1","macp.mode.task.v1"],"can_start_sessions":false,"max_open_sessions":5}]"#;
508 let layer = layer_with_tokens(json);
509
510 let mut meta = MetadataMap::new();
511 meta.insert("authorization", "Bearer t-modes".parse().unwrap());
512 let id = layer
513 .authenticate_metadata(&meta)
514 .expect("should authenticate");
515
516 assert_eq!(id.sender, "agent://limited");
517 assert!(!id.can_start_sessions);
518 assert_eq!(id.max_open_sessions, Some(5));
519 let modes = id
520 .allowed_modes
521 .as_ref()
522 .expect("should have allowed_modes");
523 assert!(modes.contains("macp.mode.decision.v1"));
524 assert!(modes.contains("macp.mode.task.v1"));
525 assert!(!modes.contains("macp.mode.proposal.v1"));
526 }
527
528 #[test]
529 fn authorization_header_takes_priority_over_x_macp_token() {
530 let json = r#"[
531 {"token":"bearer-tok","sender":"agent://bearer-user"},
532 {"token":"header-tok","sender":"agent://header-user"}
533 ]"#;
534 let layer = layer_with_tokens(json);
535
536 let mut meta = MetadataMap::new();
537 meta.insert("authorization", "Bearer bearer-tok".parse().unwrap());
538 meta.insert("x-macp-token", "header-tok".parse().unwrap());
539
540 let id = layer
541 .authenticate_metadata(&meta)
542 .expect("should authenticate");
543 assert_eq!(id.sender, "agent://bearer-user");
545 }
546
547 #[test]
552 fn dev_sender_header_rejected_without_chain() {
553 let layer = SecurityLayer {
554 identities: Arc::new(HashMap::new()),
555 rate_bucket: Arc::new(RateBucket::default()),
556 auth_chain: None,
557 max_payload_bytes: 1_048_576,
558 session_start_rate: RateLimitConfig {
559 limit: usize::MAX,
560 window: Duration::from_secs(60),
561 },
562 message_rate: RateLimitConfig {
563 limit: usize::MAX,
564 window: Duration::from_secs(60),
565 },
566 };
567
568 let mut meta = MetadataMap::new();
569 meta.insert("x-macp-agent-id", "agent://dev-agent".parse().unwrap());
570
571 let err = layer.authenticate_metadata(&meta).unwrap_err();
572 assert!(matches!(err, MacpError::Unauthenticated));
573 }
574
575 #[test]
576 fn dev_sender_header_ignored_when_not_allowed() {
577 let layer = SecurityLayer {
579 identities: Arc::new(HashMap::new()),
580 rate_bucket: Arc::new(RateBucket::default()),
581 auth_chain: None,
582 max_payload_bytes: 1_048_576,
583 session_start_rate: RateLimitConfig {
584 limit: usize::MAX,
585 window: Duration::from_secs(60),
586 },
587 message_rate: RateLimitConfig {
588 limit: usize::MAX,
589 window: Duration::from_secs(60),
590 },
591 };
592
593 let mut meta = MetadataMap::new();
594 meta.insert("x-macp-agent-id", "agent://sneaky".parse().unwrap());
595
596 let err = layer.authenticate_metadata(&meta).unwrap_err();
597 assert!(matches!(err, MacpError::Unauthenticated));
598 }
599
600 #[test]
601 fn bearer_token_takes_priority_over_dev_header() {
602 let json = r#"[{"token":"real-tok","sender":"agent://real"}]"#;
603 let identities = SecurityLayer::parse_identities(json).unwrap();
604
605 let layer = SecurityLayer {
606 identities: Arc::new(identities),
607 rate_bucket: Arc::new(RateBucket::default()),
608 auth_chain: None,
609 max_payload_bytes: 1_048_576,
610 session_start_rate: RateLimitConfig {
611 limit: usize::MAX,
612 window: Duration::from_secs(60),
613 },
614 message_rate: RateLimitConfig {
615 limit: usize::MAX,
616 window: Duration::from_secs(60),
617 },
618 };
619
620 let mut meta = MetadataMap::new();
621 meta.insert("authorization", "Bearer real-tok".parse().unwrap());
622 meta.insert("x-macp-agent-id", "agent://dev-override".parse().unwrap());
623
624 let id = layer
625 .authenticate_metadata(&meta)
626 .expect("should authenticate via bearer");
627 assert_eq!(id.sender, "agent://real");
628 }
629
630 #[test]
635 fn authorize_mode_allows_any_mode_when_no_restriction() {
636 let layer = SecurityLayer::dev_mode();
637 let id = AuthIdentity {
638 sender: "agent://any".into(),
639 allowed_modes: None,
640 can_start_sessions: true,
641 max_open_sessions: None,
642 can_manage_mode_registry: false,
643 is_observer: false,
644 };
645 assert!(layer
646 .authorize_mode(&id, "macp.mode.decision.v1", false)
647 .is_ok());
648 assert!(layer.authorize_mode(&id, "macp.mode.task.v1", true).is_ok());
649 assert!(layer.authorize_mode(&id, "arbitrary.mode", false).is_ok());
650 }
651
652 #[test]
653 fn authorize_mode_rejects_unlisted_mode() {
654 let layer = SecurityLayer::dev_mode();
655 let mut allowed = HashSet::new();
656 allowed.insert("macp.mode.decision.v1".to_string());
657
658 let id = AuthIdentity {
659 sender: "agent://restricted".into(),
660 allowed_modes: Some(allowed),
661 can_start_sessions: true,
662 max_open_sessions: None,
663 can_manage_mode_registry: false,
664 is_observer: false,
665 };
666 assert!(layer
667 .authorize_mode(&id, "macp.mode.decision.v1", false)
668 .is_ok());
669 let err = layer
670 .authorize_mode(&id, "macp.mode.task.v1", false)
671 .unwrap_err();
672 assert!(matches!(err, MacpError::Forbidden));
673 }
674
675 #[test]
676 fn authorize_mode_rejects_session_start_when_not_allowed() {
677 let layer = SecurityLayer::dev_mode();
678 let id = AuthIdentity {
679 sender: "agent://no-start".into(),
680 allowed_modes: None,
681 can_start_sessions: false,
682 max_open_sessions: None,
683 can_manage_mode_registry: false,
684 is_observer: false,
685 };
686 let err = layer
687 .authorize_mode(&id, "macp.mode.decision.v1", true)
688 .unwrap_err();
689 assert!(matches!(err, MacpError::Forbidden));
690 }
691
692 #[test]
693 fn authorize_mode_allows_non_session_start_even_when_start_forbidden() {
694 let layer = SecurityLayer::dev_mode();
695 let id = AuthIdentity {
696 sender: "agent://no-start".into(),
697 allowed_modes: None,
698 can_start_sessions: false,
699 max_open_sessions: None,
700 can_manage_mode_registry: false,
701 is_observer: false,
702 };
703 assert!(layer
705 .authorize_mode(&id, "macp.mode.decision.v1", false)
706 .is_ok());
707 }
708
709 #[test]
710 fn authorize_mode_checks_both_can_start_and_allowed_modes() {
711 let layer = SecurityLayer::dev_mode();
712 let mut allowed = HashSet::new();
713 allowed.insert("macp.mode.decision.v1".to_string());
714
715 let id = AuthIdentity {
716 sender: "agent://double-check".into(),
717 allowed_modes: Some(allowed),
718 can_start_sessions: false,
719 max_open_sessions: None,
720 can_manage_mode_registry: false,
721 is_observer: false,
722 };
723
724 let err = layer
726 .authorize_mode(&id, "macp.mode.decision.v1", true)
727 .unwrap_err();
728 assert!(matches!(err, MacpError::Forbidden));
729
730 let err = layer
732 .authorize_mode(&id, "macp.mode.task.v1", false)
733 .unwrap_err();
734 assert!(matches!(err, MacpError::Forbidden));
735
736 assert!(layer
738 .authorize_mode(&id, "macp.mode.decision.v1", false)
739 .is_ok());
740 }
741
742 #[test]
743 fn authorize_mode_registry_requires_explicit_privilege() {
744 let layer = SecurityLayer::dev_mode();
745 let id = AuthIdentity {
746 sender: "agent://no-admin".into(),
747 allowed_modes: None,
748 can_start_sessions: true,
749 max_open_sessions: None,
750 is_observer: false,
751 can_manage_mode_registry: false,
752 };
753 let err = layer.authorize_mode_registry(&id).unwrap_err();
754 assert!(matches!(err, MacpError::Forbidden));
755 }
756
757 #[test]
758 fn bearer_token_can_manage_mode_registry() {
759 let json =
760 r#"[{"token":"admin-tok","sender":"agent://admin","can_manage_mode_registry":true}]"#;
761 let layer = layer_with_tokens(json);
762 let mut meta = MetadataMap::new();
763 meta.insert("authorization", "Bearer admin-tok".parse().unwrap());
764 let id = layer.authenticate_metadata(&meta).unwrap();
765 assert!(layer.authorize_mode_registry(&id).is_ok());
766 }
767
768 #[tokio::test]
773 async fn rate_limit_session_start_enforced() {
774 let layer = SecurityLayer {
775 identities: Arc::new(HashMap::new()),
776 rate_bucket: Arc::new(RateBucket::default()),
777 auth_chain: None,
778 max_payload_bytes: 1_048_576,
779 session_start_rate: RateLimitConfig {
780 limit: 3,
781 window: Duration::from_secs(60),
782 },
783 message_rate: RateLimitConfig {
784 limit: usize::MAX,
785 window: Duration::from_secs(60),
786 },
787 };
788
789 let sender = "agent://rate-test";
790 for _ in 0..3 {
792 assert!(layer.enforce_rate_limit(sender, true).await.is_ok());
793 }
794 let err = layer.enforce_rate_limit(sender, true).await.unwrap_err();
796 assert!(matches!(err, MacpError::RateLimited));
797
798 assert!(layer.enforce_rate_limit(sender, false).await.is_ok());
800 }
801
802 #[tokio::test]
803 async fn rate_limit_message_enforced() {
804 let layer = SecurityLayer {
805 identities: Arc::new(HashMap::new()),
806 rate_bucket: Arc::new(RateBucket::default()),
807 auth_chain: None,
808 max_payload_bytes: 1_048_576,
809 session_start_rate: RateLimitConfig {
810 limit: usize::MAX,
811 window: Duration::from_secs(60),
812 },
813 message_rate: RateLimitConfig {
814 limit: 2,
815 window: Duration::from_secs(60),
816 },
817 };
818
819 let sender = "agent://msg-test";
820 assert!(layer.enforce_rate_limit(sender, false).await.is_ok());
821 assert!(layer.enforce_rate_limit(sender, false).await.is_ok());
822 let err = layer.enforce_rate_limit(sender, false).await.unwrap_err();
823 assert!(matches!(err, MacpError::RateLimited));
824
825 assert!(layer.enforce_rate_limit(sender, true).await.is_ok());
827 }
828
829 #[tokio::test]
830 async fn rate_limit_per_sender_isolation() {
831 let layer = SecurityLayer {
832 identities: Arc::new(HashMap::new()),
833 rate_bucket: Arc::new(RateBucket::default()),
834 auth_chain: None,
835 max_payload_bytes: 1_048_576,
836 session_start_rate: RateLimitConfig {
837 limit: 1,
838 window: Duration::from_secs(60),
839 },
840 message_rate: RateLimitConfig {
841 limit: usize::MAX,
842 window: Duration::from_secs(60),
843 },
844 };
845
846 assert!(layer.enforce_rate_limit("agent://a", true).await.is_ok());
848 assert!(layer.enforce_rate_limit("agent://a", true).await.is_err());
849
850 assert!(layer.enforce_rate_limit("agent://b", true).await.is_ok());
852 }
853
854 #[tokio::test]
855 async fn rate_limit_window_expiry() {
856 let layer = SecurityLayer {
857 identities: Arc::new(HashMap::new()),
858 rate_bucket: Arc::new(RateBucket::default()),
859 auth_chain: None,
860 max_payload_bytes: 1_048_576,
861 session_start_rate: RateLimitConfig {
862 limit: 1,
863 window: Duration::from_millis(1), },
865 message_rate: RateLimitConfig {
866 limit: usize::MAX,
867 window: Duration::from_secs(60),
868 },
869 };
870
871 let sender = "agent://expiry-test";
872 assert!(layer.enforce_rate_limit(sender, true).await.is_ok());
873
874 tokio::time::sleep(Duration::from_millis(5)).await;
876
877 assert!(layer.enforce_rate_limit(sender, true).await.is_ok());
879 }
880
881 #[test]
886 fn no_anonymous_fallback_even_when_auth_not_required() {
887 let layer = insecure_layer();
888 let meta = MetadataMap::new();
889 let err = layer.authenticate_metadata(&meta).unwrap_err();
890 assert!(matches!(err, MacpError::Unauthenticated));
891 }
892
893 #[test]
894 fn no_anonymous_fallback_when_auth_required() {
895 let json = r#"[{"token":"t","sender":"agent://real"}]"#;
896 let layer = layer_with_tokens(json);
897
898 let meta = MetadataMap::new();
899 let err = layer.authenticate_metadata(&meta).unwrap_err();
900 assert!(matches!(err, MacpError::Unauthenticated));
901 }
902
903 #[test]
904 fn dev_mode_no_fallback_with_empty_metadata() {
905 let layer = SecurityLayer::dev_mode();
908 let meta = MetadataMap::new();
909 let err = layer.authenticate_metadata(&meta).unwrap_err();
910 assert!(matches!(err, MacpError::Unauthenticated));
911 }
912
913 #[test]
918 fn token_file_loading_via_parse_identities() {
919 let json = r#"[
922 {"token":"file-tok-1","sender":"agent://file-alice","allowed_modes":["macp.mode.decision.v1"]},
923 {"token":"file-tok-2","sender":"agent://file-bob","can_start_sessions":false}
924 ]"#;
925 let mut tmp = NamedTempFile::new().expect("create temp file");
926 write!(tmp, "{}", json).expect("write temp file");
927
928 let contents = fs::read_to_string(tmp.path()).expect("read temp file");
929 let identities = SecurityLayer::parse_identities(&contents).expect("parse identities");
930
931 assert_eq!(identities.len(), 2);
932
933 let alice = identities.get("file-tok-1").expect("alice entry");
934 assert_eq!(alice.sender, "agent://file-alice");
935 let alice_modes = alice.allowed_modes.as_ref().expect("should have modes");
936 assert!(alice_modes.contains("macp.mode.decision.v1"));
937 assert!(alice.can_start_sessions); let bob = identities.get("file-tok-2").expect("bob entry");
940 assert_eq!(bob.sender, "agent://file-bob");
941 assert!(!bob.can_start_sessions);
942 assert!(bob.allowed_modes.is_none()); }
944
945 #[test]
946 fn token_file_end_to_end_via_layer() {
947 let json = r#"[{"token":"e2e-tok","sender":"agent://e2e-agent"}]"#;
949 let mut tmp = NamedTempFile::new().expect("create temp file");
950 write!(tmp, "{}", json).expect("write temp file");
951
952 let contents = fs::read_to_string(tmp.path()).expect("read temp file");
953 let layer = layer_with_tokens(&contents);
954
955 let mut meta = MetadataMap::new();
956 meta.insert("authorization", "Bearer e2e-tok".parse().unwrap());
957 let id = layer
958 .authenticate_metadata(&meta)
959 .expect("should authenticate");
960 assert_eq!(id.sender, "agent://e2e-agent");
961 }
962
963 #[test]
964 fn parse_identities_invalid_json_returns_error() {
965 let result = SecurityLayer::parse_identities("not valid json");
966 assert!(result.is_err());
967 }
968
969 #[test]
970 fn parse_identities_empty_list() {
971 let identities = SecurityLayer::parse_identities("[]").expect("valid empty list");
972 assert!(identities.is_empty());
973 }
974
975 #[test]
976 fn parse_identities_wrapped_empty() {
977 let identities =
978 SecurityLayer::parse_identities(r#"{"tokens":[]}"#).expect("valid wrapped empty");
979 assert!(identities.is_empty());
980 }
981}