1use std::fmt;
4
5use aws_lc_rs::constant_time;
6use aws_lc_rs::digest::{SHA256, digest};
7use base64::Engine;
8use base64::engine::general_purpose::URL_SAFE_NO_PAD;
9
10use crate::ids::{KeyId, TenantId};
11use crate::tenants::Tenants;
12
13const PREFIX: &str = "vcs_";
16
17const SECRET_LEN: usize = 32;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Scope {
23 Write,
25 Read,
27 Delete,
29 Legal,
31 Audit,
33 Admin,
35}
36
37impl Scope {
38 const ALL: [Scope; 6] = [
40 Scope::Write,
41 Scope::Read,
42 Scope::Delete,
43 Scope::Legal,
44 Scope::Audit,
45 Scope::Admin,
46 ];
47
48 #[must_use]
50 pub fn named(name: &str) -> Option<Scope> {
51 Scope::ALL.into_iter().find(|s| s.name() == name)
52 }
53
54 fn bit(self) -> u8 {
55 1 << self as u8
56 }
57
58 fn name(self) -> &'static str {
59 match self {
60 Scope::Write => "write",
61 Scope::Read => "read",
62 Scope::Delete => "delete",
63 Scope::Legal => "legal",
64 Scope::Audit => "audit",
65 Scope::Admin => "admin",
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub struct Scopes(u8);
73
74impl Scopes {
75 #[must_use]
77 pub fn of(scopes: &[Scope]) -> Scopes {
78 Scopes(scopes.iter().fold(0, |bits, scope| bits | scope.bit()))
79 }
80
81 #[must_use]
83 pub fn contains(self, scope: Scope) -> bool {
84 self.0 & scope.bit() != 0
85 }
86
87 #[must_use]
89 pub fn is_empty(self) -> bool {
90 self.0 == 0
91 }
92
93 #[must_use]
97 pub fn named(names: &[String]) -> Option<Scopes> {
98 let mut held = Vec::new();
99 for name in names {
100 let scope = Scope::named(name)?;
101 if held.contains(&scope) {
102 return None;
103 }
104 held.push(scope);
105 }
106 Some(Scopes::of(&held))
107 }
108
109 pub fn parse(text: &str) -> Option<Scopes> {
112 let scopes: Vec<Scope> = text.split(',').map(Scope::named).collect::<Option<_>>()?;
113 let read = Scopes::of(&scopes);
114 (read.to_string() == text).then_some(read)
115 }
116}
117
118impl Scopes {
119 pub fn names(self) -> Vec<&'static str> {
122 Scope::ALL
123 .into_iter()
124 .filter(|s| self.contains(*s))
125 .map(Scope::name)
126 .collect()
127 }
128}
129
130impl std::fmt::Display for Scopes {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 f.write_str(&self.names().join(","))
133 }
134}
135
136pub(crate) fn new_token() -> (KeyId, String) {
139 let id = KeyId::generate();
140 let mut secret = [0; SECRET_LEN];
141 aws_lc_rs::rand::fill(&mut secret).expect("AWS-LC aborts instead of failing");
144 let token = format!("{PREFIX}{id}_{}", URL_SAFE_NO_PAD.encode(secret));
145 (id, token)
146}
147
148pub(crate) fn token_hash(token: &str) -> [u8; 32] {
150 digest(&SHA256, token.as_bytes())
151 .as_ref()
152 .try_into()
153 .expect("SHA-256 is 32 bytes")
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub struct Caller {
160 pub key: KeyId,
162 pub tenant: Option<TenantId>,
164 pub scopes: Scopes,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum AuthError {
171 Missing,
173 Forbidden(Scope),
175 Malformed,
177 Unknown,
179 Revoked,
181 Io,
183}
184
185impl fmt::Display for AuthError {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 AuthError::Missing => f.write_str("no Authorization header"),
189 AuthError::Forbidden(scope) => {
190 write!(f, "the key does not hold the {} scope", scope.name())
191 }
192 AuthError::Malformed => {
193 f.write_str("the Authorization header is not Bearer and a token")
194 }
195 AuthError::Unknown => f.write_str("no key has this id and secret"),
196 AuthError::Revoked => f.write_str("the key was revoked"),
197 AuthError::Io => f.write_str("the key table could not be read"),
198 }
199 }
200}
201
202impl std::error::Error for AuthError {}
203
204pub fn authenticate(keys: &Tenants, authorization: Option<&str>) -> Result<Caller, AuthError> {
218 match check(keys, authorization) {
219 Ok(caller) => Ok(caller),
220 Err((error, known)) => {
221 if error != AuthError::Io {
222 keys.count_auth_failure(known);
223 }
224 Err(error)
225 }
226 }
227}
228
229type Failure = (AuthError, Option<(KeyId, Option<TenantId>)>);
231
232fn check(keys: &Tenants, authorization: Option<&str>) -> Result<Caller, Failure> {
233 let unknown = |error| (error, None);
234 let header = authorization.ok_or(unknown(AuthError::Missing))?;
235 let token = bearer(header).ok_or(unknown(AuthError::Malformed))?;
236 let id = parse(token).map_err(unknown)?;
237 let key = keys
238 .stored_key(id)
239 .map_err(|_| unknown(AuthError::Io))?
240 .ok_or(unknown(AuthError::Unknown))?;
241 let known = |error| (error, Some((id, key.tenant)));
242 constant_time::verify_slices_are_equal(&token_hash(token), &key.hash)
245 .map_err(|_| known(AuthError::Unknown))?;
246 if key.revoked {
247 return Err(known(AuthError::Revoked));
248 }
249 Ok(Caller {
250 key: id,
251 tenant: key.tenant,
252 scopes: key.scopes,
253 })
254}
255
256pub fn require(caller: &Caller, scope: Scope) -> Result<(), AuthError> {
264 let of_its_kind = (scope == Scope::Admin) == caller.tenant.is_none();
265 if of_its_kind && caller.scopes.contains(scope) {
266 Ok(())
267 } else {
268 Err(AuthError::Forbidden(scope))
269 }
270}
271
272pub fn tenant_for(caller: &Caller, scope: Scope) -> Result<TenantId, AuthError> {
280 require(caller, scope)?;
281 caller.tenant.ok_or(AuthError::Forbidden(scope))
282}
283
284fn bearer(header: &str) -> Option<&str> {
287 let (scheme, rest) = header.split_once(' ')?;
288 scheme
289 .eq_ignore_ascii_case("Bearer")
290 .then(|| rest.trim_start_matches(' '))
291}
292
293pub fn parse(token: &str) -> Result<KeyId, AuthError> {
301 let malformed = AuthError::Malformed;
302 let rest = token.strip_prefix(PREFIX).ok_or(malformed)?;
303 let (id, rest) = rest.split_at_checked(16).ok_or(malformed)?;
304 let secret = rest.strip_prefix('_').ok_or(malformed)?;
305 let id = KeyId::parse(id).ok_or(malformed)?;
306 let secret = URL_SAFE_NO_PAD.decode(secret).map_err(|_| malformed)?;
309 if secret.len() != SECRET_LEN {
310 return Err(malformed);
311 }
312 Ok(id)
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 const ID: &str = "0123456789abcdef";
320
321 fn secret(byte: u8) -> String {
322 URL_SAFE_NO_PAD.encode([byte; 32])
323 }
324
325 #[test]
326 fn a_well_formed_token_gives_its_key_id() {
327 let id = KeyId([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]);
328 let secrets = [
329 secret(0x00),
330 secret(0xff),
331 URL_SAFE_NO_PAD.encode([0xfb; 32]),
333 URL_SAFE_NO_PAD.encode([[0xff; 16], [0x00; 16]].concat()),
334 ];
335 for secret in secrets {
336 assert_eq!(
337 secret.len(),
338 43,
339 "32 bytes are 43 base64url characters without padding"
340 );
341 let token = format!("vcs_{ID}_{secret}");
342 assert_eq!(parse(&token), Ok(id), "{token}");
343 }
344 assert!(
345 secret(0xff).contains('_'),
346 "a secret may hold the separator"
347 );
348 }
349
350 #[test]
351 fn a_malformed_token_is_refused() {
352 let good = secret(0x42);
353 let malformed = [
354 String::new(),
355 "vcs_".to_owned(),
356 format!("{ID}_{good}"),
357 format!("vck_{ID}_{good}"),
358 format!("VCS_{ID}_{good}"),
359 format!("vcs_{}_{good}", &ID[..15]),
360 format!("vcs_{ID}0_{good}"),
361 format!("vcs_{}_{good}", ID.to_uppercase()),
362 format!("vcs_0123456789abcdeg_{good}"),
363 format!("vcs_{ID}{good}"),
364 format!("vcs_{ID}-{good}"),
365 format!("vcs_{ID}_{}", &good[..42]),
366 format!("vcs_{ID}_{good}A"),
367 format!("vcs_{ID}_{good}="),
368 format!("vcs_{ID}_{}", good.replace('Q', "+")),
369 format!("vcs_{ID}_{}+", &good[..42]),
370 format!("vcs_{ID}_{}B", &good[..42]),
371 format!(" vcs_{ID}_{good}"),
372 format!("vcs_{ID}_{good} "),
373 format!("vcs_{ID}__{good}"),
374 format!("vcs_\u{0661}123456789abcdef_{good}"),
375 ];
376 for token in malformed {
377 assert_eq!(parse(&token), Err(AuthError::Malformed), "{token:?}");
378 }
379 }
380
381 #[test]
382 fn scopes_are_a_set_written_in_a_fixed_order() {
383 let none = Scopes::of(&[]);
384 assert!(none.is_empty(), "no scopes given, none held");
385 let scopes = Scopes::of(&[Scope::Delete, Scope::Write, Scope::Delete]);
386 assert!(
387 !scopes.is_empty(),
388 "two scopes given, so the set is not empty"
389 );
390 for scope in Scope::ALL {
391 assert_eq!(
392 scopes.contains(scope),
393 matches!(scope, Scope::Write | Scope::Delete),
394 "{scope:?}"
395 );
396 }
397 assert_eq!(
398 scopes.to_string(),
399 "write,delete",
400 "a repeated scope is held once, and written in the fixed order"
401 );
402 assert_eq!(
403 Scopes::of(&Scope::ALL).to_string(),
404 "write,read,delete,legal,audit,admin",
405 "every scope, in the fixed order"
406 );
407 assert_eq!(
408 Scopes::of(&[Scope::Legal, Scope::Write, Scope::Read]).names(),
409 ["write", "read", "legal"],
410 "names come in Scope::ALL's order, not the order given"
411 );
412 for scope in Scope::ALL {
413 let one = Scopes::of(&[scope]);
414 assert_eq!(Scopes::parse(&one.to_string()), Some(one), "{scope:?}");
415 }
416 let all = Scopes::of(&Scope::ALL);
417 assert_eq!(
418 Scopes::parse(&all.to_string()),
419 Some(all),
420 "parse reads every scope back from its written form"
421 );
422 }
423
424 #[test]
425 fn only_the_written_spelling_of_scopes_is_read() {
426 for text in [
427 "",
428 "delete,write",
429 "write,write",
430 "write,",
431 ",write",
432 "write, read",
433 "Write",
434 "owner",
435 "write,read,",
436 ] {
437 assert_eq!(Scopes::parse(text), None, "{text:?}");
438 }
439 }
440
441 #[test]
443 fn audit_is_required_of_a_tenant_key_only() {
444 let tenant = Caller {
445 key: KeyId::generate(),
446 tenant: Some(TenantId::generate()),
447 scopes: Scopes::of(&[Scope::Audit]),
448 };
449 assert_eq!(
450 Scope::named("audit"),
451 Some(Scope::Audit),
452 "the scope is named audit"
453 );
454 assert_eq!(
455 require(&tenant, Scope::Audit),
456 Ok(()),
457 "a tenant key with the audit scope may read its log"
458 );
459 assert_eq!(
460 tenant_for(&tenant, Scope::Audit),
461 Ok(tenant.tenant.expect("one")),
462 "and the log it reads is its own tenant's"
463 );
464 assert_eq!(
465 require(&tenant, Scope::Read),
466 Err(AuthError::Forbidden(Scope::Read)),
467 "audit grants no other scope"
468 );
469 let operator = Caller {
470 tenant: None,
471 scopes: Scopes::of(&[Scope::Admin, Scope::Audit]),
472 ..tenant
473 };
474 assert_eq!(
475 require(&operator, Scope::Audit),
476 Err(AuthError::Forbidden(Scope::Audit)),
477 "an operator key has no tenant log to read, even with the audit scope"
478 );
479 }
480
481 #[test]
482 fn a_scope_is_found_by_its_exact_name() {
483 for scope in Scope::ALL {
484 assert_eq!(Scope::named(scope.name()), Some(scope), "{scope:?}");
485 }
486 for name in ["", "Read", "READ", " read", "read,write", "superuser"] {
487 assert_eq!(Scope::named(name), None, "{name:?}");
488 }
489 }
490
491 #[test]
492 fn a_new_token_is_well_formed_and_new_each_time() {
493 let (id, token) = new_token();
494 assert_eq!(
495 parse(&token),
496 Ok(id),
497 "a new token parses to the id it was made with"
498 );
499 let (other_id, other) = new_token();
500 assert_ne!(other_id, id, "each new token has a new random key id");
501 assert_ne!(other[21..], token[21..], "a new secret");
502 }
503
504 #[test]
506 fn a_token_is_stored_as_its_sha_256() {
507 assert_eq!(
508 token_hash("abc")
509 .iter()
510 .map(|b| format!("{b:02x}"))
511 .collect::<String>(),
512 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
513 "the hash is plain SHA-256, as the RFC's vector shows"
514 );
515 }
516
517 mod authenticate {
518 use super::*;
519 use crate::audit::Actor;
520 use crate::tenants::{Fixture, NewKey, fixture};
521
522 fn vcon_scopes() -> Scopes {
523 Scopes::of(&[Scope::Write, Scope::Read])
524 }
525
526 fn key(f: &Fixture) -> (TenantId, NewKey) {
527 let t = f.tenants.create(Actor::Cli, "acme", None).expect("made");
528 let key = f
529 .tenants
530 .create_key(Actor::Cli, Some(t), vcon_scopes(), "x")
531 .expect("made");
532 (t, key)
533 }
534
535 fn auth(f: &Fixture, header: &str) -> Result<Caller, AuthError> {
536 authenticate(&f.tenants, Some(header))
537 }
538
539 #[test]
540 fn a_valid_token_names_its_caller() {
541 let f = fixture();
542 let (t, key) = key(&f);
543 let caller = Caller {
544 key: key.id,
545 tenant: Some(t),
546 scopes: vcon_scopes(),
547 };
548 for header in [
549 format!("Bearer {}", key.token),
550 format!("bearer {}", key.token),
551 format!("BEARER {}", key.token),
552 ] {
553 assert_eq!(auth(&f, &header), Ok(caller), "{header}");
554 }
555 let operator = f
556 .tenants
557 .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
558 .expect("made");
559 assert_eq!(
560 auth(&f, &format!("Bearer {}", operator.token)),
561 Ok(Caller {
562 key: operator.id,
563 tenant: None,
564 scopes: Scopes::of(&[Scope::Admin]),
565 }),
566 "an operator key's caller has no tenant"
567 );
568 }
569
570 #[test]
571 fn a_missing_or_malformed_header_is_refused() {
572 let f = fixture();
573 let (_, key) = key(&f);
574 assert_eq!(
575 authenticate(&f.tenants, None),
576 Err(AuthError::Missing),
577 "no Authorization header is told apart from a bad one"
578 );
579 for header in [
580 String::new(),
581 "Bearer".to_owned(),
582 "Bearer ".to_owned(),
583 format!("Bearer{}", key.token),
584 format!("Basic {}", key.token),
585 format!("Bearers {}", key.token),
586 format!("Bearer {} ", key.token),
587 format!("Bearer\t{}", key.token),
588 "Bearer vcs_0123456789abcdef_short".to_owned(),
589 ] {
590 assert_eq!(auth(&f, &header), Err(AuthError::Malformed), "{header:?}");
591 }
592 }
593
594 #[test]
595 fn an_unknown_key_or_a_wrong_secret_is_unknown() {
596 let f = fixture();
597 let (_, key) = key(&f);
598 let (_, stranger) = new_token();
599 assert_eq!(
600 auth(&f, &format!("Bearer {stranger}")),
601 Err(AuthError::Unknown),
602 "a well-formed token whose key id no catalog holds"
603 );
604 let (_, other) = new_token();
605 let forged = format!("{}{}", &key.token[..21], &other[21..]);
606 assert_eq!(
607 auth(&f, &format!("Bearer {forged}")),
608 Err(AuthError::Unknown),
609 "a real key id with another secret is answered as an unknown key"
610 );
611 f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
612 assert_eq!(
613 auth(&f, &format!("Bearer {forged}")),
614 Err(AuthError::Unknown),
615 "a wrong secret learns nothing about revocation"
616 );
617 }
618
619 #[test]
620 fn a_revoked_key_is_refused_on_its_next_call() {
621 let f = fixture();
622 let (_, key) = key(&f);
623 let header = format!("Bearer {}", key.token);
624 assert!(
625 auth(&f, &header).is_ok(),
626 "the key works before it is revoked"
627 );
628 f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
629 assert_eq!(
630 auth(&f, &header),
631 Err(AuthError::Revoked),
632 "the next call after revocation is refused: no cache keeps it alive"
633 );
634 }
635
636 fn admin_failures(f: &Fixture) -> Vec<serde_json::Value> {
637 f.admin_records("auth_failed")
638 }
639
640 fn tenant_failures(f: &Fixture, t: TenantId) -> Vec<serde_json::Value> {
641 crate::audit::testing::exported(&f.tenant_catalog(t), None)
642 .into_iter()
643 .filter(|r| r["action"] == "auth_failed")
644 .collect()
645 }
646
647 fn forged(key: &NewKey) -> String {
648 let (_, other) = new_token();
649 format!("Bearer {}{}", &key.token[..21], &other[21..])
650 }
651
652 #[test]
653 fn a_burst_of_bad_tokens_leaves_one_counted_record() {
654 let f = fixture();
655 let (t, _) = key(&f);
656 for _ in 0..20 {
657 assert!(auth(&f, "Bearer not-a-token").is_err(), "a malformed token");
658 }
659 for _ in 0..5 {
660 assert!(authenticate(&f.tenants, None).is_err(), "no header");
661 let (_, made_up) = new_token();
662 assert!(
663 auth(&f, &format!("Bearer {made_up}")).is_err(),
664 "a key id no catalog holds"
665 );
666 }
667 let records = admin_failures(&f);
668 assert_eq!(records.len(), 1, "{records:?}");
669 assert_eq!(
670 records[0]["actor"], "",
671 "no key is known, so no actor is named"
672 );
673 assert_eq!(
674 records[0]["status"], 401,
675 "each was refused as unauthenticated"
676 );
677 assert_eq!(
678 records[0]["count"], 30,
679 "20 malformed, 5 missing, 5 unknown"
680 );
681 assert_eq!(
682 records[0]["at"], "2026-10-23T12:00:00Z",
683 "the record is stamped with the fixture clock's minute"
684 );
685 assert_eq!(
686 tenant_failures(&f, t),
687 Vec::<serde_json::Value>::new(),
688 "no failure named this tenant's key"
689 );
690
691 f.clock
692 .set(time::macros::datetime!(2026-10-23 12:01:30 UTC));
693 assert!(auth(&f, "Bearer not-a-token").is_err(), "a malformed token");
694 let records = admin_failures(&f);
695 assert_eq!(records.len(), 2, "a new minute, a new record");
696 assert_eq!(
697 records[1]["count"], 1,
698 "the new record counts only the new failure"
699 );
700 }
701
702 #[test]
703 fn a_known_key_that_fails_is_counted_in_its_tenants_log() {
704 let f = fixture();
705 let (t, key) = key(&f);
706 for _ in 0..3 {
707 assert_eq!(
708 auth(&f, &forged(&key)),
709 Err(AuthError::Unknown),
710 "a wrong secret for a real key"
711 );
712 }
713 f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
714 for _ in 0..2 {
715 assert_eq!(
716 auth(&f, &format!("Bearer {}", key.token)),
717 Err(AuthError::Revoked),
718 "the right secret for a revoked key"
719 );
720 }
721 let records = tenant_failures(&f, t);
722 assert_eq!(records.len(), 1, "{records:?}");
723 assert_eq!(
724 records[0]["actor"],
725 key.id.to_string(),
726 "the failures name the key they tried"
727 );
728 assert_eq!(
729 records[0]["status"], 401,
730 "each was refused as unauthenticated"
731 );
732 assert_eq!(
733 records[0]["count"], 5,
734 "3 wrong secrets and 2 calls after revocation"
735 );
736 assert_eq!(
737 admin_failures(&f),
738 Vec::<serde_json::Value>::new(),
739 "a tenant key's failures go to its tenant's log only"
740 );
741 }
742
743 #[test]
744 fn an_operator_key_that_fails_is_counted_in_the_admin_log() {
745 let f = fixture();
746 let operator = f
747 .tenants
748 .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
749 .expect("made");
750 assert!(auth(&f, &forged(&operator)).is_err(), "a wrong secret");
751 let records = admin_failures(&f);
752 assert_eq!(
753 records.len(),
754 1,
755 "an operator key has no tenant log: {records:?}"
756 );
757 assert_eq!(
758 records[0]["actor"],
759 operator.id.to_string(),
760 "the record names the operator key tried"
761 );
762 }
763
764 #[test]
765 fn a_known_key_whose_tenant_log_is_gone_is_counted_in_the_admin_log() {
766 let f = fixture();
767 let (t, key) = key(&f);
768 f.tenants
769 .remove(Actor::Cli, t, crate::tenants::Removal::Force)
770 .expect("removed");
771 assert_eq!(
772 auth(&f, &format!("Bearer {}", key.token)),
773 Err(AuthError::Revoked),
774 "removing a tenant revokes its keys"
775 );
776 let records = admin_failures(&f);
777 assert_eq!(records.len(), 1, "with the tenant's log gone: {records:?}");
778 assert_eq!(
779 records[0]["actor"],
780 key.id.to_string(),
781 "the record still names the key tried"
782 );
783 }
784
785 #[test]
786 fn a_failure_that_cannot_be_counted_is_refused_all_the_same() {
787 let f = fixture();
788 f.conn()
789 .execute_batch(
790 "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
791 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
792 )
793 .expect("trigger");
794 assert_eq!(
795 auth(&f, "Bearer not-a-token"),
796 Err(AuthError::Malformed),
797 "a failed audit write does not turn a refusal into a pass"
798 );
799 }
800
801 #[test]
802 fn a_successful_check_counts_nothing() {
803 let f = fixture();
804 let (t, key) = key(&f);
805 assert!(
806 auth(&f, &format!("Bearer {}", key.token)).is_ok(),
807 "the right token"
808 );
809 assert_eq!(
810 admin_failures(&f),
811 Vec::<serde_json::Value>::new(),
812 "a success is not a failure to count"
813 );
814 assert_eq!(
815 tenant_failures(&f, t),
816 Vec::<serde_json::Value>::new(),
817 "a success is not a failure to count"
818 );
819 }
820
821 #[test]
822 fn a_key_table_that_cannot_be_read_lets_nothing_through() {
823 let f = fixture();
824 let (_, key) = key(&f);
825 let header = format!("Bearer {}", key.token);
826 f.conn()
827 .execute(
828 "UPDATE api_key SET scopes = 'owner' WHERE id = ?1",
829 [key.id.bytes()],
830 )
831 .expect("damage");
832 assert_eq!(auth(&f, &header), Err(AuthError::Io), "damaged scopes");
833 f.conn()
834 .execute_batch("ALTER TABLE api_key RENAME TO gone")
835 .expect("lose the table");
836 assert_eq!(auth(&f, &header), Err(AuthError::Io), "no table");
837 assert_eq!(
838 admin_failures(&f),
839 Vec::<serde_json::Value>::new(),
840 "an unreadable table is not a failed check"
841 );
842 }
843 }
844
845 mod scopes {
846 use super::*;
847
848 const TENANT: TenantId = TenantId([1; 16]);
849
850 fn caller(tenant: Option<TenantId>, scopes: &[Scope]) -> Caller {
851 Caller {
852 key: KeyId([2; 8]),
853 tenant,
854 scopes: Scopes::of(scopes),
855 }
856 }
857
858 #[test]
860 fn each_key_may_use_only_the_scopes_it_holds_of_its_kind() {
861 let vcon = [Scope::Write, Scope::Read, Scope::Delete, Scope::Legal];
862 let matrix = [
863 (caller(Some(TENANT), &[Scope::Write]), vec![Scope::Write]),
864 (
865 caller(Some(TENANT), &[Scope::Read, Scope::Delete]),
866 vec![Scope::Read, Scope::Delete],
867 ),
868 (caller(Some(TENANT), &[Scope::Legal]), vec![Scope::Legal]),
869 (caller(Some(TENANT), &vcon), vcon.to_vec()),
870 (caller(None, &[Scope::Admin]), vec![Scope::Admin]),
871 (
874 caller(Some(TENANT), &[Scope::Admin, Scope::Read]),
875 vec![Scope::Read],
876 ),
877 (
878 caller(None, &[Scope::Admin, Scope::Write]),
879 vec![Scope::Admin],
880 ),
881 ];
882 for (who, allowed) in matrix {
883 for scope in Scope::ALL {
884 let expected = if allowed.contains(&scope) {
885 Ok(())
886 } else {
887 Err(AuthError::Forbidden(scope))
888 };
889 assert_eq!(require(&who, scope), expected, "{who:?} {scope:?}");
890 }
891 }
892 }
893
894 #[test]
895 fn a_request_acts_on_the_tenant_of_its_key() {
896 let tenant_key = caller(Some(TENANT), &[Scope::Write]);
897 assert_eq!(
898 tenant_for(&tenant_key, Scope::Write),
899 Ok(TENANT),
900 "a tenant key acts on its own tenant"
901 );
902 assert_eq!(
903 tenant_for(&tenant_key, Scope::Read),
904 Err(AuthError::Forbidden(Scope::Read)),
905 "only with a scope it holds"
906 );
907 }
908
909 #[test]
910 fn an_operator_key_acts_on_no_tenant() {
911 let operator = caller(None, &[Scope::Admin, Scope::Write]);
912 for scope in Scope::ALL {
913 assert_eq!(
914 tenant_for(&operator, scope),
915 Err(AuthError::Forbidden(scope)),
916 "{scope:?}"
917 );
918 }
919 }
920 }
921}