1use std::fmt;
109use std::sync::Arc;
110
111pub const REQUIRED_SENTINEL: &str = "required";
114
115#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct RequiredHeader {
120 pub name: String,
122 pub expected: String,
124}
125
126#[derive(Debug, Clone, Default, PartialEq, Eq)]
132pub struct HeaderAuthRules {
133 pub required_headers: Vec<RequiredHeader>,
135 pub auth_required: Option<String>,
138 pub api_key_required: bool,
140 pub authorization_header_required: Option<String>,
143 pub api_key_header: Option<String>,
145}
146
147impl HeaderAuthRules {
148 #[must_use]
151 pub const fn has_rules(&self) -> bool {
152 !self.required_headers.is_empty()
153 || self.auth_required.is_some()
154 || self.api_key_required
155 || self.authorization_header_required.is_some()
156 }
157}
158
159pub type AuthVerifier = Arc<dyn Fn(&str) -> bool + Send + Sync>;
166
167#[derive(Clone, Default)]
170pub struct RouteVerifiers {
171 pub auth: Option<AuthVerifier>,
173 pub api_key: Option<AuthVerifier>,
175}
176
177impl fmt::Debug for RouteVerifiers {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 f.debug_struct("RouteVerifiers")
180 .field("auth", &self.auth.is_some())
181 .field("api_key", &self.api_key.is_some())
182 .finish()
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum GuardViolation {
190 MissingHeader,
192 MismatchedHeader,
194 AuthorizationHeader,
197 RequireAuth,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct GuardBlock {
207 pub violation: GuardViolation,
209 pub status: u16,
212 pub body: String,
214}
215
216impl fmt::Display for GuardBlock {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 write!(f, "{} {}", self.status, self.body)
219 }
220}
221
222#[must_use]
230pub fn extract_credential<'a>(auth_header: &'a str, auth_type: &str) -> Option<&'a str> {
231 match auth_type {
232 "bearer" => auth_header.strip_prefix("Bearer "),
233 "basic" => auth_header.strip_prefix("Basic "),
234 _ if auth_header.is_empty() => None,
235 _ => Some(auth_header),
236 }
237}
238
239#[must_use]
247pub fn decide<L>(
248 rules: &HeaderAuthRules,
249 verifiers: &RouteVerifiers,
250 lookup: L,
251) -> Option<GuardBlock>
252where
253 L: Fn(&str) -> Option<String>,
254{
255 required_headers_block(rules, &lookup)
256 .or_else(|| authentication_block(rules, verifiers, &lookup))
257}
258
259fn required_headers_block<L>(rules: &HeaderAuthRules, lookup: &L) -> Option<GuardBlock>
261where
262 L: Fn(&str) -> Option<String>,
263{
264 for rule in &rules.required_headers {
265 let actual = lookup(&rule.name).filter(|value| !value.is_empty());
266 let Some(actual) = actual else {
267 return Some(GuardBlock {
268 violation: GuardViolation::MissingHeader,
269 status: 400,
270 body: format!("Missing required header: {}", rule.name),
271 });
272 };
273 if rule.expected != REQUIRED_SENTINEL && actual != rule.expected {
274 return Some(GuardBlock {
275 violation: GuardViolation::MismatchedHeader,
276 status: 400,
277 body: format!("Header '{}' does not match the required value", rule.name),
278 });
279 }
280 }
281 None
282}
283
284fn authentication_block<L>(
286 rules: &HeaderAuthRules,
287 verifiers: &RouteVerifiers,
288 lookup: &L,
289) -> Option<GuardBlock>
290where
291 L: Fn(&str) -> Option<String>,
292{
293 if let Some(scheme) = rules.authorization_header_required.as_deref() {
295 let auth_header = lookup("authorization").unwrap_or_default();
296 return extract_credential(&auth_header, scheme).map_or_else(
297 || {
298 Some(GuardBlock {
299 violation: GuardViolation::AuthorizationHeader,
300 status: 401,
301 body: "Authentication required".to_owned(),
302 })
303 },
304 |_| None,
305 );
306 }
307
308 if let Some(scheme) = rules.auth_required.as_deref() {
309 let auth_header = lookup("authorization").unwrap_or_default();
310 let Some(credential) = extract_credential(&auth_header, scheme) else {
311 return Some(auth_failure(GuardViolation::RequireAuth));
312 };
313 return resolve_verifier(verifiers.auth.as_ref(), credential);
314 }
315
316 if rules.api_key_required {
317 let header = rules.api_key_header.as_deref().unwrap_or("");
318 let credential = lookup(header).filter(|value| !value.is_empty());
319 let Some(credential) = credential else {
320 return Some(auth_failure(GuardViolation::RequireAuth));
321 };
322 return resolve_verifier(verifiers.api_key.as_ref(), &credential);
323 }
324
325 None
326}
327
328fn resolve_verifier(verifier: Option<&AuthVerifier>, credential: &str) -> Option<GuardBlock> {
331 let Some(verifier) = verifier else {
332 return Some(auth_failure(GuardViolation::RequireAuth));
333 };
334 if verifier(credential) {
335 None
336 } else {
337 Some(auth_failure(GuardViolation::RequireAuth))
338 }
339}
340
341fn auth_failure(violation: GuardViolation) -> GuardBlock {
345 GuardBlock {
346 violation,
347 status: 401,
348 body: "Authentication required".to_owned(),
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 fn lookup_of<'h>(headers: &'h [(&'h str, &'h str)]) -> impl Fn(&str) -> Option<String> + 'h {
359 move |name: &str| {
360 headers
361 .iter()
362 .find(|(key, _)| key.eq_ignore_ascii_case(name))
363 .map(|(_, value)| (*value).to_owned())
364 }
365 }
366
367 fn bearer_rules(scheme: &str) -> HeaderAuthRules {
368 HeaderAuthRules {
369 auth_required: Some(scheme.to_owned()),
370 ..HeaderAuthRules::default()
371 }
372 }
373
374 #[test]
375 fn empty_rules_pass_everything() {
376 let rules = HeaderAuthRules::default();
377 assert!(!rules.has_rules());
378 assert!(decide(&rules, &RouteVerifiers::default(), |_| None).is_none());
379 }
380
381 #[test]
382 fn has_rules_tracks_each_field() {
383 let rules = HeaderAuthRules {
384 api_key_required: true,
385 ..HeaderAuthRules::default()
386 };
387 assert!(rules.has_rules());
388 let rules = HeaderAuthRules {
389 authorization_header_required: Some("bearer".to_owned()),
390 ..HeaderAuthRules::default()
391 };
392 assert!(rules.has_rules());
393 }
394
395 #[test]
396 fn required_header_missing_or_empty_is_a_dynamic_400() {
397 let rules = HeaderAuthRules {
398 required_headers: vec![RequiredHeader {
399 name: "X-Request-ID".to_owned(),
400 expected: REQUIRED_SENTINEL.to_owned(),
401 }],
402 ..HeaderAuthRules::default()
403 };
404 let block = decide(&rules, &RouteVerifiers::default(), |_| None).expect("missing");
405 assert_eq!(block.violation, GuardViolation::MissingHeader);
406 assert_eq!(block.status, 400);
407 assert_eq!(block.body, "Missing required header: X-Request-ID");
408 assert_eq!(
409 block.to_string(),
410 "400 Missing required header: X-Request-ID"
411 );
412
413 let block = decide(
415 &rules,
416 &RouteVerifiers::default(),
417 lookup_of(&[("X-Request-ID", "")]),
418 )
419 .expect("empty");
420 assert_eq!(block.violation, GuardViolation::MissingHeader);
421 }
422
423 #[test]
424 fn required_header_lookup_is_case_insensitive_and_values_exact() {
425 let rules = HeaderAuthRules {
426 required_headers: vec![RequiredHeader {
427 name: "X-Role".to_owned(),
428 expected: "admin".to_owned(),
429 }],
430 ..HeaderAuthRules::default()
431 };
432 assert!(
433 decide(
434 &rules,
435 &RouteVerifiers::default(),
436 lookup_of(&[("x-role", "admin")])
437 )
438 .is_none()
439 );
440 let block = decide(
442 &rules,
443 &RouteVerifiers::default(),
444 lookup_of(&[("X-ROLE", "Admin")]),
445 )
446 .expect("mismatch");
447 assert_eq!(block.violation, GuardViolation::MismatchedHeader);
448 assert_eq!(block.status, 400);
449 assert_eq!(
450 block.body,
451 "Header 'X-Role' does not match the required value"
452 );
453 }
454
455 #[test]
456 fn required_headers_report_the_first_failing_rule_in_order() {
457 let rules = HeaderAuthRules {
458 required_headers: vec![
459 RequiredHeader {
460 name: "A".to_owned(),
461 expected: REQUIRED_SENTINEL.to_owned(),
462 },
463 RequiredHeader {
464 name: "B".to_owned(),
465 expected: "yes".to_owned(),
466 },
467 RequiredHeader {
468 name: "C".to_owned(),
469 expected: REQUIRED_SENTINEL.to_owned(),
470 },
471 ],
472 ..HeaderAuthRules::default()
473 };
474 let lookup = lookup_of(&[("A", "1"), ("B", "no"), ("C", "")]);
475 let block = decide(&rules, &RouteVerifiers::default(), lookup).expect("B fails");
476 assert_eq!(block.body, "Header 'B' does not match the required value");
477
478 let lookup = lookup_of(&[("A", "1"), ("B", "yes")]);
479 let block = decide(&rules, &RouteVerifiers::default(), lookup).expect("C fails");
480 assert_eq!(block.body, "Missing required header: C");
481 }
482
483 #[test]
484 fn bearer_scheme_demands_the_exact_prefix() {
485 let verifiers = RouteVerifiers::default();
486 assert!(
489 decide(
490 &bearer_rules("bearer"),
491 &verifiers,
492 lookup_of(&[("authorization", "Bearer tok")])
493 )
494 .is_some()
495 );
496
497 let rules = bearer_rules("bearer");
498 let verifiers = RouteVerifiers {
499 auth: Some(Arc::new(|_| true)),
500 api_key: None,
501 };
502 assert!(
503 decide(
504 &rules,
505 &verifiers,
506 lookup_of(&[("authorization", "Bearer tok")])
507 )
508 .is_none()
509 );
510 for junk in ["bearer tok", "Bearer", "Basic tok", "tok"] {
513 assert!(
514 decide(&rules, &verifiers, lookup_of(&[("authorization", junk)])).is_some(),
515 "'{junk}' must fail the bearer presence check"
516 );
517 }
518 assert!(
521 decide(
522 &rules,
523 &verifiers,
524 lookup_of(&[("authorization", "Bearer ")])
525 )
526 .is_none()
527 );
528 assert!(decide(&rules, &verifiers, |_| None).is_some());
530 }
531
532 #[test]
533 fn basic_scheme_demands_the_exact_prefix() {
534 let rules = bearer_rules("basic");
535 let verifiers = RouteVerifiers {
536 auth: Some(Arc::new(|_| true)),
537 api_key: None,
538 };
539 assert!(
540 decide(
541 &rules,
542 &verifiers,
543 lookup_of(&[("authorization", "Basic dXNlcjpwdw==")])
544 )
545 .is_none()
546 );
547 assert!(
548 decide(
549 &rules,
550 &verifiers,
551 lookup_of(&[("authorization", "Bearer dXNlcjpwdw==")])
552 )
553 .is_some()
554 );
555 }
556
557 #[test]
558 fn other_schemes_carry_the_whole_header_value() {
559 let rules = bearer_rules("apikey");
560 let verifiers = RouteVerifiers {
561 auth: Some(Arc::new(|credential| credential == "raw-key")),
562 api_key: None,
563 };
564 assert!(
565 decide(
566 &rules,
567 &verifiers,
568 lookup_of(&[("authorization", "raw-key")])
569 )
570 .is_none()
571 );
572 assert!(
574 decide(
575 &rules,
576 &verifiers,
577 lookup_of(&[("authorization", "Bearer raw-key")])
578 )
579 .is_some()
580 );
581 assert!(decide(&rules, &verifiers, lookup_of(&[("authorization", "")])).is_some());
583 }
584
585 #[test]
586 fn verifier_outcomes_are_the_fixed_401_shape() {
587 let rules = bearer_rules("bearer");
588 let accept = RouteVerifiers {
589 auth: Some(Arc::new(|_| true)),
590 api_key: None,
591 };
592 let reject = RouteVerifiers {
593 auth: Some(Arc::new(|_| false)),
594 api_key: None,
595 };
596 assert!(decide(&rules, &accept, lookup_of(&[("authorization", "Bearer t")])).is_none());
597 let block =
598 decide(&rules, &reject, lookup_of(&[("authorization", "Bearer t")])).expect("rejected");
599 assert_eq!(block.violation, GuardViolation::RequireAuth);
600 assert_eq!(block.status, 401);
601 assert_eq!(block.body, "Authentication required");
602
603 let block = decide(
606 &rules,
607 &RouteVerifiers::default(),
608 lookup_of(&[("authorization", "Bearer t")]),
609 )
610 .expect("no verifier");
611 assert_eq!(block.status, 401);
612 assert_eq!(block.body, "Authentication required");
613 }
614
615 #[test]
616 fn presence_scheme_short_circuits_the_other_auth_paths() {
617 let rules = HeaderAuthRules {
618 auth_required: Some("bearer".to_owned()),
619 api_key_required: true,
620 api_key_header: Some("X-Key".to_owned()),
621 authorization_header_required: Some("basic".to_owned()),
622 ..HeaderAuthRules::default()
623 };
624 let verifiers = RouteVerifiers::default();
627 assert!(
628 decide(
629 &rules,
630 &verifiers,
631 lookup_of(&[("authorization", "Basic ok")])
632 )
633 .is_none()
634 );
635 assert!(
637 decide(
638 &rules,
639 &verifiers,
640 lookup_of(&[("authorization", "Basic ")])
641 )
642 .is_none()
643 );
644 let block = decide(
646 &rules,
647 &verifiers,
648 lookup_of(&[("authorization", "bearer x")]),
649 )
650 .expect("presence");
651 assert_eq!(block.violation, GuardViolation::AuthorizationHeader);
652 }
653
654 #[test]
655 fn api_key_path_reads_its_header_and_verifier() {
656 let rules = HeaderAuthRules {
657 api_key_required: true,
658 api_key_header: Some("X-Key".to_owned()),
659 ..HeaderAuthRules::default()
660 };
661 let verifiers = RouteVerifiers {
662 auth: None,
663 api_key: Some(Arc::new(|credential| credential == "k-1")),
664 };
665 assert!(decide(&rules, &verifiers, lookup_of(&[("X-Key", "k-1")])).is_none());
666 for headers in [
668 vec![],
669 vec![("X-Key", "")],
670 vec![("X-Key", "wrong")],
671 vec![("X-Other", "k-1")],
672 ] {
673 let block = decide(&rules, &verifiers, lookup_of(&headers)).expect("api key failure");
674 assert_eq!(block.violation, GuardViolation::RequireAuth);
675 assert_eq!(block.status, 401);
676 }
677 let rules = HeaderAuthRules {
679 api_key_required: true,
680 ..HeaderAuthRules::default()
681 };
682 let verifiers = RouteVerifiers {
683 api_key: Some(Arc::new(|_| true)),
684 auth: None,
685 };
686 assert!(decide(&rules, &verifiers, lookup_of(&[("X-Key", "k-1")])).is_some());
687 }
688
689 #[test]
690 fn required_headers_run_before_authentication() {
691 let rules = HeaderAuthRules {
692 required_headers: vec![RequiredHeader {
693 name: "X-Request-ID".to_owned(),
694 expected: REQUIRED_SENTINEL.to_owned(),
695 }],
696 auth_required: Some("bearer".to_owned()),
697 ..HeaderAuthRules::default()
698 };
699 let verifiers = RouteVerifiers::default();
700 let block = decide(&rules, &verifiers, |_| None).expect("header first");
702 assert_eq!(block.status, 400);
703 assert_eq!(block.body, "Missing required header: X-Request-ID");
704 }
705
706 #[test]
707 fn auth_required_beats_api_key_when_both_set() {
708 let rules = HeaderAuthRules {
709 auth_required: Some("bearer".to_owned()),
710 api_key_required: true,
711 api_key_header: Some("X-Key".to_owned()),
712 ..HeaderAuthRules::default()
713 };
714 let verifiers = RouteVerifiers {
715 auth: Some(Arc::new(|_| true)),
716 api_key: Some(Arc::new(|_| false)),
717 };
718 assert!(
722 decide(
723 &rules,
724 &verifiers,
725 lookup_of(&[("authorization", "Bearer t")])
726 )
727 .is_none()
728 );
729 }
730
731 #[test]
732 fn route_verifiers_debug_renders_the_verifier_presence() {
733 let verifiers = RouteVerifiers {
734 auth: None,
735 api_key: None,
736 };
737 assert!(format!("{verifiers:?}").contains("RouteVerifiers"));
738 }
739}