Skip to main content

macp_auth/
security.rs

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    /// Creates a test-friendly SecurityLayer that maps any bearer token
70    /// `"tok-<sender>"` to an identity with `sender = <token-value>`.
71    /// For tests, use `Authorization: Bearer agent://name` to authenticate as `agent://name`.
72    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    /// Dev-mode authenticate: accepts any bearer token as the sender identity.
90    /// This is used ONLY in tests (dev_mode), not in production (from_env).
91    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        // Build auth resolver chain
141        let mut resolvers: Vec<Box<dyn crate::auth::AuthResolver>> = Vec::new();
142
143        // JWT resolver (if configured)
144        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        // Static bearer resolver (always present if tokens are configured)
179        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        // Production path: use the auth resolver chain.
246        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        // Explicit identity map (layer_with_tokens in tests)
255        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        // Dev-mode: any bearer token → identity (for tests only)
267        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        // Prune stale senders whose events are all outside the window.
304        // Limit pruning to at most 100 entries per call to bound latency.
305        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    /// Build a SecurityLayer with bearer token identities loaded from a JSON string.
360    /// This avoids touching environment variables (safe for parallel tests).
361    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    /// Build a SecurityLayer with no tokens that does not require auth.
380    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    // ---------------------------------------------------------------
398    // 1. dev_mode() creates a SecurityLayer that doesn't require auth
399    // ---------------------------------------------------------------
400
401    #[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    // ---------------------------------------------------------------
426    // 2. from_env() with no env vars creates an insecure layer
427    // ---------------------------------------------------------------
428
429    #[test]
430    fn from_env_defaults_without_env_vars() {
431        // Verify default configuration via direct construction.
432        let layer = insecure_layer();
433        assert_eq!(layer.max_payload_bytes, 1_048_576);
434    }
435
436    // ---------------------------------------------------------------
437    // 3. Bearer token auth: loading tokens and authenticating
438    // ---------------------------------------------------------------
439
440    #[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()); // empty vec -> None
453        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(); // no auth header at all
488        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        // Authorization header should take priority
544        assert_eq!(id.sender, "agent://bearer-user");
545    }
546
547    // ---------------------------------------------------------------
548    // 4. Dev header extraction: x-macp-agent-id
549    // ---------------------------------------------------------------
550
551    #[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        // allow_dev_sender_header=false, no tokens
578        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    // ---------------------------------------------------------------
631    // 5. authorize_mode() with allowed modes and without
632    // ---------------------------------------------------------------
633
634    #[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        // Regular messages (not session start) should succeed
704        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        // Cannot start sessions (checked first)
725        let err = layer
726            .authorize_mode(&id, "macp.mode.decision.v1", true)
727            .unwrap_err();
728        assert!(matches!(err, MacpError::Forbidden));
729
730        // Cannot use unlisted mode
731        let err = layer
732            .authorize_mode(&id, "macp.mode.task.v1", false)
733            .unwrap_err();
734        assert!(matches!(err, MacpError::Forbidden));
735
736        // Can send non-start message on allowed mode
737        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    // ---------------------------------------------------------------
769    // 6. enforce_rate_limit() with session_start and message categories
770    // ---------------------------------------------------------------
771
772    #[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        // First 3 should succeed
791        for _ in 0..3 {
792            assert!(layer.enforce_rate_limit(sender, true).await.is_ok());
793        }
794        // 4th should be rate limited
795        let err = layer.enforce_rate_limit(sender, true).await.unwrap_err();
796        assert!(matches!(err, MacpError::RateLimited));
797
798        // Regular messages should still be fine (separate bucket)
799        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        // Session starts should still be fine (separate bucket)
826        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        // Sender A exhausts limit
847        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        // Sender B should still be able to start sessions
851        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), // very short window
864            },
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        // Wait for the window to expire
875        tokio::time::sleep(Duration::from_millis(5)).await;
876
877        // Should succeed again after window expiry
878        assert!(layer.enforce_rate_limit(sender, true).await.is_ok());
879    }
880
881    // ---------------------------------------------------------------
882    // 7. Anonymous fallback behavior
883    // ---------------------------------------------------------------
884
885    #[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        // dev_mode: allow_dev_sender_header=true
906        // With no headers at all, returns Unauthenticated (no anonymous fallback)
907        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    // ---------------------------------------------------------------
914    // 8. Token file loading via MACP_AUTH_TOKENS_FILE
915    // ---------------------------------------------------------------
916
917    #[test]
918    fn token_file_loading_via_parse_identities() {
919        // Test the parse_identities path that from_env uses after reading the file.
920        // We write a temp file and then read + parse it the same way from_env would.
921        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); // default_true
938
939        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()); // empty vec -> None
943    }
944
945    #[test]
946    fn token_file_end_to_end_via_layer() {
947        // Build a layer as if loaded from a token file, then authenticate with it.
948        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}