Skip to main content

vcon_server/
access.rs

1//! Turns the bearer token on a request into a caller, or refuses it.
2
3use aws_lc_rs::constant_time;
4use aws_lc_rs::digest::{SHA256, digest};
5use base64::Engine;
6use base64::engine::general_purpose::URL_SAFE_NO_PAD;
7
8use crate::ids::{KeyId, TenantId};
9use crate::tenants::Tenants;
10
11/// The fixed start of every token, so secret scanners can spot a leaked
12/// one.
13const PREFIX: &str = "vcs_";
14
15/// A secret is 256 random bits.
16const SECRET_LEN: usize = 32;
17
18/// What a key may do.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Scope {
21    Write,
22    Read,
23    Delete,
24    Legal,
25    /// Reads the tenant's own audit log.
26    Audit,
27    Admin,
28}
29
30impl Scope {
31    /// Every scope, in the order a key's scopes are written.
32    const ALL: [Scope; 6] = [
33        Scope::Write,
34        Scope::Read,
35        Scope::Delete,
36        Scope::Legal,
37        Scope::Audit,
38        Scope::Admin,
39    ];
40
41    /// The scope with this exact name.
42    pub fn named(name: &str) -> Option<Scope> {
43        Scope::ALL.into_iter().find(|s| s.name() == name)
44    }
45
46    fn bit(self) -> u8 {
47        1 << self as u8
48    }
49
50    fn name(self) -> &'static str {
51        match self {
52            Scope::Write => "write",
53            Scope::Read => "read",
54            Scope::Delete => "delete",
55            Scope::Legal => "legal",
56            Scope::Audit => "audit",
57            Scope::Admin => "admin",
58        }
59    }
60}
61
62/// A set of scopes.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub struct Scopes(u8);
65
66impl Scopes {
67    pub fn of(scopes: &[Scope]) -> Scopes {
68        Scopes(scopes.iter().fold(0, |bits, scope| bits | scope.bit()))
69    }
70
71    pub fn contains(self, scope: Scope) -> bool {
72        self.0 & scope.bit() != 0
73    }
74
75    pub fn is_empty(self) -> bool {
76        self.0 == 0
77    }
78
79    /// The scopes a list names, or None when one is unknown or named twice,
80    /// so a list means one thing. Which scopes a key may hold is Tenants'
81    /// rule.
82    pub fn named(names: &[String]) -> Option<Scopes> {
83        let mut held = Vec::new();
84        for name in names {
85            let scope = Scope::named(name)?;
86            if held.contains(&scope) {
87                return None;
88            }
89            held.push(scope);
90        }
91        Some(Scopes::of(&held))
92    }
93
94    /// Reads scopes written by `Display`: names in the fixed order, comma
95    /// separated, each once.
96    pub fn parse(text: &str) -> Option<Scopes> {
97        let scopes: Vec<Scope> = text.split(',').map(Scope::named).collect::<Option<_>>()?;
98        let read = Scopes::of(&scopes);
99        (read.to_string() == text).then_some(read)
100    }
101}
102
103impl Scopes {
104    /// The names of the scopes held, in the fixed order of `Scope::ALL`,
105    /// as the API and the command line list them.
106    pub fn names(self) -> Vec<&'static str> {
107        Scope::ALL
108            .into_iter()
109            .filter(|s| self.contains(*s))
110            .map(Scope::name)
111            .collect()
112    }
113}
114
115impl std::fmt::Display for Scopes {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.write_str(&self.names().join(","))
118    }
119}
120
121/// A new token for a new key: `vcs_<key id>_<secret>`, with a random id
122/// and a random 256-bit secret.
123pub(crate) fn new_token() -> (KeyId, String) {
124    let id = KeyId::generate();
125    let mut secret = [0; SECRET_LEN];
126    // AWS-LC's RAND_bytes aborts the process when it cannot get enough
127    // randomness, so `fill` never returns its error.
128    aws_lc_rs::rand::fill(&mut secret).expect("AWS-LC aborts instead of failing");
129    let token = format!("{PREFIX}{id}_{}", URL_SAFE_NO_PAD.encode(secret));
130    (id, token)
131}
132
133/// The SHA-256 of a whole token, as the key table stores it.
134pub(crate) fn token_hash(token: &str) -> [u8; 32] {
135    digest(&SHA256, token.as_bytes())
136        .as_ref()
137        .try_into()
138        .expect("SHA-256 is 32 bytes")
139}
140
141/// Who a request comes from: a tenant's key, or with no tenant the
142/// operator's.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct Caller {
145    pub key: KeyId,
146    pub tenant: Option<TenantId>,
147    pub scopes: Scopes,
148}
149
150/// Why a request's token was refused.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum AuthError {
153    /// No Authorization header.
154    Missing,
155    /// The key does not hold the scope the request needs.
156    Forbidden(Scope),
157    /// Not `Bearer` and a token as `new_token` writes one.
158    Malformed,
159    /// No key has this id, or the secret is not its own.
160    Unknown,
161    /// The key was revoked.
162    Revoked,
163    /// The key table could not be read, so nothing is let through.
164    Io,
165}
166
167/// The caller a request's Authorization header names. The key is looked up
168/// on every call, with no cache, so a revoked key fails on its next
169/// request.
170///
171/// A failed check is counted in the audit (`Tenants::count_auth_failure`),
172/// except when the key table could not be read.
173pub fn authenticate(keys: &Tenants, authorization: Option<&str>) -> Result<Caller, AuthError> {
174    match check(keys, authorization) {
175        Ok(caller) => Ok(caller),
176        Err((error, known)) => {
177            if error != AuthError::Io {
178                keys.count_auth_failure(known);
179            }
180            Err(error)
181        }
182    }
183}
184
185/// A failed check, with the key it named if that key is known.
186type Failure = (AuthError, Option<(KeyId, Option<TenantId>)>);
187
188fn check(keys: &Tenants, authorization: Option<&str>) -> Result<Caller, Failure> {
189    let unknown = |error| (error, None);
190    let header = authorization.ok_or(unknown(AuthError::Missing))?;
191    let token = bearer(header).ok_or(unknown(AuthError::Malformed))?;
192    let id = parse(token).map_err(unknown)?;
193    let key = keys
194        .stored_key(id)
195        .map_err(|_| unknown(AuthError::Io))?
196        .ok_or(unknown(AuthError::Unknown))?;
197    let known = |error| (error, Some((id, key.tenant)));
198    // The secret is checked before revocation, so a wrong secret learns
199    // nothing about the key.
200    constant_time::verify_slices_are_equal(&token_hash(token), &key.hash)
201        .map_err(|_| known(AuthError::Unknown))?;
202    if key.revoked {
203        return Err(known(AuthError::Revoked));
204    }
205    Ok(Caller {
206        key: id,
207        tenant: key.tenant,
208        scopes: key.scopes,
209    })
210}
211
212/// Refuses a caller whose key does not hold `scope`. Admin belongs only to
213/// an operator key, and the vCon, legal and audit scopes only to a tenant
214/// key, so a key holding a scope of the other kind is still refused it.
215pub fn require(caller: &Caller, scope: Scope) -> Result<(), AuthError> {
216    let of_its_kind = (scope == Scope::Admin) == caller.tenant.is_none();
217    if of_its_kind && caller.scopes.contains(scope) {
218        Ok(())
219    } else {
220        Err(AuthError::Forbidden(scope))
221    }
222}
223
224/// The tenant a vCon or legal request acts on: the caller's own, from its
225/// key, never from the request.
226pub fn tenant_for(caller: &Caller, scope: Scope) -> Result<TenantId, AuthError> {
227    require(caller, scope)?;
228    caller.tenant.ok_or(AuthError::Forbidden(scope))
229}
230
231/// The token in `Bearer <token>`. The scheme's case does not matter, and
232/// one or more spaces may follow it (RFC 7235).
233fn bearer(header: &str) -> Option<&str> {
234    let (scheme, rest) = header.split_once(' ')?;
235    scheme
236        .eq_ignore_ascii_case("Bearer")
237        .then(|| rest.trim_start_matches(' '))
238}
239
240/// Reads a token written as `vcs_<key id>_<secret>`: the key id is 16
241/// lowercase hex digits, and the secret is 256 bits in base64url without
242/// padding. The secret may hold `_`, so the parts are found by position.
243pub fn parse(token: &str) -> Result<KeyId, AuthError> {
244    let malformed = AuthError::Malformed;
245    let rest = token.strip_prefix(PREFIX).ok_or(malformed)?;
246    let (id, rest) = rest.split_at_checked(16).ok_or(malformed)?;
247    let secret = rest.strip_prefix('_').ok_or(malformed)?;
248    let id = KeyId::parse(id).ok_or(malformed)?;
249    // The decoding refuses padding, other alphabets and stray trailing
250    // bits, so each secret has one spelling.
251    let secret = URL_SAFE_NO_PAD.decode(secret).map_err(|_| malformed)?;
252    if secret.len() != SECRET_LEN {
253        return Err(malformed);
254    }
255    Ok(id)
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn scopes_are_named_in_their_fixed_order() {
264        let scopes = Scopes::of(&[Scope::Legal, Scope::Write, Scope::Read]);
265        assert_eq!(scopes.names(), ["write", "read", "legal"]);
266        assert_eq!(Scopes::of(&[Scope::Admin]).names(), ["admin"]);
267    }
268
269    const ID: &str = "0123456789abcdef";
270
271    fn secret(byte: u8) -> String {
272        URL_SAFE_NO_PAD.encode([byte; 32])
273    }
274
275    #[test]
276    fn a_well_formed_token_gives_its_key_id() {
277        let id = KeyId([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]);
278        let secrets = [
279            secret(0x00),
280            secret(0xff),
281            // 0xfb gives '-' and 0xff gives '_' in base64url.
282            URL_SAFE_NO_PAD.encode([0xfb; 32]),
283            URL_SAFE_NO_PAD.encode([[0xff; 16], [0x00; 16]].concat()),
284        ];
285        for secret in secrets {
286            assert_eq!(secret.len(), 43);
287            let token = format!("vcs_{ID}_{secret}");
288            assert_eq!(parse(&token), Ok(id), "{token}");
289        }
290        assert!(
291            secret(0xff).contains('_'),
292            "a secret may hold the separator"
293        );
294    }
295
296    #[test]
297    fn a_malformed_token_is_refused() {
298        let good = secret(0x42);
299        let malformed = [
300            String::new(),
301            "vcs_".to_owned(),
302            format!("{ID}_{good}"),
303            format!("vck_{ID}_{good}"),
304            format!("VCS_{ID}_{good}"),
305            format!("vcs_{}_{good}", &ID[..15]),
306            format!("vcs_{ID}0_{good}"),
307            format!("vcs_{}_{good}", ID.to_uppercase()),
308            format!("vcs_0123456789abcdeg_{good}"),
309            format!("vcs_{ID}{good}"),
310            format!("vcs_{ID}-{good}"),
311            format!("vcs_{ID}_{}", &good[..42]),
312            format!("vcs_{ID}_{good}A"),
313            format!("vcs_{ID}_{good}="),
314            format!("vcs_{ID}_{}", good.replace('Q', "+")),
315            format!("vcs_{ID}_{}+", &good[..42]),
316            format!("vcs_{ID}_{}B", &good[..42]),
317            format!(" vcs_{ID}_{good}"),
318            format!("vcs_{ID}_{good} "),
319            format!("vcs_{ID}__{good}"),
320            format!("vcs_\u{0661}123456789abcdef_{good}"),
321        ];
322        for token in malformed {
323            assert_eq!(parse(&token), Err(AuthError::Malformed), "{token:?}");
324        }
325    }
326
327    #[test]
328    fn scopes_are_a_set_written_in_a_fixed_order() {
329        let none = Scopes::of(&[]);
330        assert!(none.is_empty());
331        let scopes = Scopes::of(&[Scope::Delete, Scope::Write, Scope::Delete]);
332        assert!(!scopes.is_empty());
333        for scope in Scope::ALL {
334            assert_eq!(
335                scopes.contains(scope),
336                matches!(scope, Scope::Write | Scope::Delete),
337                "{scope:?}"
338            );
339        }
340        assert_eq!(scopes.to_string(), "write,delete");
341        assert_eq!(
342            Scopes::of(&Scope::ALL).to_string(),
343            "write,read,delete,legal,audit,admin"
344        );
345        for scope in Scope::ALL {
346            let one = Scopes::of(&[scope]);
347            assert_eq!(Scopes::parse(&one.to_string()), Some(one), "{scope:?}");
348        }
349        let all = Scopes::of(&Scope::ALL);
350        assert_eq!(Scopes::parse(&all.to_string()), Some(all));
351    }
352
353    #[test]
354    fn only_the_written_spelling_of_scopes_is_read() {
355        for text in [
356            "",
357            "delete,write",
358            "write,write",
359            "write,",
360            ",write",
361            "write, read",
362            "Write",
363            "owner",
364            "write,read,",
365        ] {
366            assert_eq!(Scopes::parse(text), None, "{text:?}");
367        }
368    }
369
370    /// audit is a tenant key's scope: it reads the tenant's own log.
371    #[test]
372    fn audit_is_required_of_a_tenant_key_only() {
373        let tenant = Caller {
374            key: KeyId::generate(),
375            tenant: Some(TenantId::generate()),
376            scopes: Scopes::of(&[Scope::Audit]),
377        };
378        assert_eq!(Scope::named("audit"), Some(Scope::Audit));
379        assert_eq!(require(&tenant, Scope::Audit), Ok(()));
380        assert_eq!(
381            tenant_for(&tenant, Scope::Audit),
382            Ok(tenant.tenant.expect("one"))
383        );
384        assert_eq!(
385            require(&tenant, Scope::Read),
386            Err(AuthError::Forbidden(Scope::Read))
387        );
388        let operator = Caller {
389            tenant: None,
390            scopes: Scopes::of(&[Scope::Admin, Scope::Audit]),
391            ..tenant
392        };
393        assert_eq!(
394            require(&operator, Scope::Audit),
395            Err(AuthError::Forbidden(Scope::Audit))
396        );
397    }
398
399    #[test]
400    fn a_scope_is_found_by_its_exact_name() {
401        for scope in Scope::ALL {
402            assert_eq!(Scope::named(scope.name()), Some(scope), "{scope:?}");
403        }
404        for name in ["", "Read", "READ", " read", "read,write", "superuser"] {
405            assert_eq!(Scope::named(name), None, "{name:?}");
406        }
407    }
408
409    #[test]
410    fn a_new_token_is_well_formed_and_new_each_time() {
411        let (id, token) = new_token();
412        assert_eq!(parse(&token), Ok(id));
413        let (other_id, other) = new_token();
414        assert_ne!(other_id, id);
415        assert_ne!(other[21..], token[21..], "a new secret");
416    }
417
418    /// RFC 6234's SHA-256 test vector for "abc".
419    #[test]
420    fn a_token_is_stored_as_its_sha_256() {
421        assert_eq!(
422            token_hash("abc")
423                .iter()
424                .map(|b| format!("{b:02x}"))
425                .collect::<String>(),
426            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
427        );
428    }
429
430    mod authenticate {
431        use super::*;
432        use crate::audit::Actor;
433        use crate::tenants::{Fixture, NewKey, fixture};
434
435        fn vcon_scopes() -> Scopes {
436            Scopes::of(&[Scope::Write, Scope::Read])
437        }
438
439        fn key(f: &Fixture) -> (TenantId, NewKey) {
440            let t = f.tenants.create(Actor::Cli, "acme", None).expect("made");
441            let key = f
442                .tenants
443                .create_key(Actor::Cli, Some(t), vcon_scopes(), "x")
444                .expect("made");
445            (t, key)
446        }
447
448        fn auth(f: &Fixture, header: &str) -> Result<Caller, AuthError> {
449            authenticate(&f.tenants, Some(header))
450        }
451
452        #[test]
453        fn a_valid_token_names_its_caller() {
454            let f = fixture();
455            let (t, key) = key(&f);
456            let caller = Caller {
457                key: key.id,
458                tenant: Some(t),
459                scopes: vcon_scopes(),
460            };
461            for header in [
462                format!("Bearer {}", key.token),
463                format!("bearer {}", key.token),
464                format!("BEARER   {}", key.token),
465            ] {
466                assert_eq!(auth(&f, &header), Ok(caller), "{header}");
467            }
468            let operator = f
469                .tenants
470                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
471                .expect("made");
472            assert_eq!(
473                auth(&f, &format!("Bearer {}", operator.token)),
474                Ok(Caller {
475                    key: operator.id,
476                    tenant: None,
477                    scopes: Scopes::of(&[Scope::Admin]),
478                })
479            );
480        }
481
482        #[test]
483        fn a_missing_or_malformed_header_is_refused() {
484            let f = fixture();
485            let (_, key) = key(&f);
486            assert_eq!(authenticate(&f.tenants, None), Err(AuthError::Missing));
487            for header in [
488                String::new(),
489                "Bearer".to_owned(),
490                "Bearer ".to_owned(),
491                format!("Bearer{}", key.token),
492                format!("Basic {}", key.token),
493                format!("Bearers {}", key.token),
494                format!("Bearer {} ", key.token),
495                format!("Bearer\t{}", key.token),
496                "Bearer vcs_0123456789abcdef_short".to_owned(),
497            ] {
498                assert_eq!(auth(&f, &header), Err(AuthError::Malformed), "{header:?}");
499            }
500        }
501
502        #[test]
503        fn an_unknown_key_or_a_wrong_secret_is_unknown() {
504            let f = fixture();
505            let (_, key) = key(&f);
506            let (_, stranger) = new_token();
507            assert_eq!(
508                auth(&f, &format!("Bearer {stranger}")),
509                Err(AuthError::Unknown)
510            );
511            let (_, other) = new_token();
512            let forged = format!("{}{}", &key.token[..21], &other[21..]);
513            assert_eq!(
514                auth(&f, &format!("Bearer {forged}")),
515                Err(AuthError::Unknown)
516            );
517            f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
518            assert_eq!(
519                auth(&f, &format!("Bearer {forged}")),
520                Err(AuthError::Unknown),
521                "a wrong secret learns nothing about revocation"
522            );
523        }
524
525        #[test]
526        fn a_revoked_key_is_refused_on_its_next_call() {
527            let f = fixture();
528            let (_, key) = key(&f);
529            let header = format!("Bearer {}", key.token);
530            assert!(auth(&f, &header).is_ok());
531            f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
532            assert_eq!(auth(&f, &header), Err(AuthError::Revoked));
533        }
534
535        fn admin_failures(f: &Fixture) -> Vec<serde_json::Value> {
536            f.admin_records("auth_failed")
537        }
538
539        fn tenant_failures(f: &Fixture, t: TenantId) -> Vec<serde_json::Value> {
540            crate::audit::testing::exported(&f.tenant_catalog(t), None)
541                .into_iter()
542                .filter(|r| r["action"] == "auth_failed")
543                .collect()
544        }
545
546        fn forged(key: &NewKey) -> String {
547            let (_, other) = new_token();
548            format!("Bearer {}{}", &key.token[..21], &other[21..])
549        }
550
551        #[test]
552        fn a_burst_of_bad_tokens_leaves_one_counted_record() {
553            let f = fixture();
554            let (t, _) = key(&f);
555            for _ in 0..20 {
556                assert!(auth(&f, "Bearer not-a-token").is_err());
557            }
558            for _ in 0..5 {
559                assert!(authenticate(&f.tenants, None).is_err());
560                let (_, made_up) = new_token();
561                assert!(auth(&f, &format!("Bearer {made_up}")).is_err());
562            }
563            let records = admin_failures(&f);
564            assert_eq!(records.len(), 1, "{records:?}");
565            assert_eq!(records[0]["actor"], "");
566            assert_eq!(records[0]["status"], 401);
567            assert_eq!(records[0]["count"], 30);
568            assert_eq!(records[0]["at"], "2026-10-23T12:00:00Z");
569            assert_eq!(tenant_failures(&f, t), Vec::<serde_json::Value>::new());
570
571            f.clock
572                .set(time::macros::datetime!(2026-10-23 12:01:30 UTC));
573            assert!(auth(&f, "Bearer not-a-token").is_err());
574            let records = admin_failures(&f);
575            assert_eq!(records.len(), 2, "a new minute, a new record");
576            assert_eq!(records[1]["count"], 1);
577        }
578
579        #[test]
580        fn a_known_key_that_fails_is_counted_in_its_tenants_log() {
581            let f = fixture();
582            let (t, key) = key(&f);
583            for _ in 0..3 {
584                assert_eq!(auth(&f, &forged(&key)), Err(AuthError::Unknown));
585            }
586            f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
587            for _ in 0..2 {
588                assert_eq!(
589                    auth(&f, &format!("Bearer {}", key.token)),
590                    Err(AuthError::Revoked)
591                );
592            }
593            let records = tenant_failures(&f, t);
594            assert_eq!(records.len(), 1, "{records:?}");
595            assert_eq!(records[0]["actor"], key.id.to_string());
596            assert_eq!(records[0]["status"], 401);
597            assert_eq!(records[0]["count"], 5);
598            assert_eq!(admin_failures(&f), Vec::<serde_json::Value>::new());
599        }
600
601        #[test]
602        fn an_operator_key_that_fails_is_counted_in_the_admin_log() {
603            let f = fixture();
604            let operator = f
605                .tenants
606                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
607                .expect("made");
608            assert!(auth(&f, &forged(&operator)).is_err());
609            let records = admin_failures(&f);
610            assert_eq!(records.len(), 1);
611            assert_eq!(records[0]["actor"], operator.id.to_string());
612        }
613
614        #[test]
615        fn a_known_key_whose_tenant_log_is_gone_is_counted_in_the_admin_log() {
616            let f = fixture();
617            let (t, key) = key(&f);
618            f.tenants
619                .remove(Actor::Cli, t, crate::tenants::Removal::Force)
620                .expect("removed");
621            assert_eq!(
622                auth(&f, &format!("Bearer {}", key.token)),
623                Err(AuthError::Revoked)
624            );
625            let records = admin_failures(&f);
626            assert_eq!(records.len(), 1);
627            assert_eq!(records[0]["actor"], key.id.to_string());
628        }
629
630        #[test]
631        fn a_failure_that_cannot_be_counted_is_refused_all_the_same() {
632            let f = fixture();
633            f.conn()
634                .execute_batch(
635                    "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
636                     BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
637                )
638                .expect("trigger");
639            assert_eq!(auth(&f, "Bearer not-a-token"), Err(AuthError::Malformed));
640        }
641
642        #[test]
643        fn a_successful_check_counts_nothing() {
644            let f = fixture();
645            let (t, key) = key(&f);
646            assert!(auth(&f, &format!("Bearer {}", key.token)).is_ok());
647            assert_eq!(admin_failures(&f), Vec::<serde_json::Value>::new());
648            assert_eq!(tenant_failures(&f, t), Vec::<serde_json::Value>::new());
649        }
650
651        #[test]
652        fn a_key_table_that_cannot_be_read_lets_nothing_through() {
653            let f = fixture();
654            let (_, key) = key(&f);
655            let header = format!("Bearer {}", key.token);
656            f.conn()
657                .execute(
658                    "UPDATE api_key SET scopes = 'owner' WHERE id = ?1",
659                    [key.id.bytes()],
660                )
661                .expect("damage");
662            assert_eq!(auth(&f, &header), Err(AuthError::Io), "damaged scopes");
663            f.conn()
664                .execute_batch("ALTER TABLE api_key RENAME TO gone")
665                .expect("lose the table");
666            assert_eq!(auth(&f, &header), Err(AuthError::Io), "no table");
667            assert_eq!(
668                admin_failures(&f),
669                Vec::<serde_json::Value>::new(),
670                "an unreadable table is not a failed check"
671            );
672        }
673    }
674
675    mod scopes {
676        use super::*;
677
678        const TENANT: TenantId = TenantId([1; 16]);
679
680        fn caller(tenant: Option<TenantId>, scopes: &[Scope]) -> Caller {
681            Caller {
682                key: KeyId([2; 8]),
683                tenant,
684                scopes: Scopes::of(scopes),
685            }
686        }
687
688        /// Which scopes each key may use: (key, scopes it allows).
689        #[test]
690        fn each_key_may_use_only_the_scopes_it_holds_of_its_kind() {
691            let vcon = [Scope::Write, Scope::Read, Scope::Delete, Scope::Legal];
692            let matrix = [
693                (caller(Some(TENANT), &[Scope::Write]), vec![Scope::Write]),
694                (
695                    caller(Some(TENANT), &[Scope::Read, Scope::Delete]),
696                    vec![Scope::Read, Scope::Delete],
697                ),
698                (caller(Some(TENANT), &[Scope::Legal]), vec![Scope::Legal]),
699                (caller(Some(TENANT), &vcon), vcon.to_vec()),
700                (caller(None, &[Scope::Admin]), vec![Scope::Admin]),
701                // A key of one kind holding a scope of the other, as only a
702                // damaged table could, is refused it.
703                (
704                    caller(Some(TENANT), &[Scope::Admin, Scope::Read]),
705                    vec![Scope::Read],
706                ),
707                (
708                    caller(None, &[Scope::Admin, Scope::Write]),
709                    vec![Scope::Admin],
710                ),
711            ];
712            for (who, allowed) in matrix {
713                for scope in Scope::ALL {
714                    let expected = if allowed.contains(&scope) {
715                        Ok(())
716                    } else {
717                        Err(AuthError::Forbidden(scope))
718                    };
719                    assert_eq!(require(&who, scope), expected, "{who:?} {scope:?}");
720                }
721            }
722        }
723
724        #[test]
725        fn a_request_acts_on_the_tenant_of_its_key() {
726            let tenant_key = caller(Some(TENANT), &[Scope::Write]);
727            assert_eq!(tenant_for(&tenant_key, Scope::Write), Ok(TENANT));
728            assert_eq!(
729                tenant_for(&tenant_key, Scope::Read),
730                Err(AuthError::Forbidden(Scope::Read))
731            );
732        }
733
734        #[test]
735        fn an_operator_key_acts_on_no_tenant() {
736            let operator = caller(None, &[Scope::Admin, Scope::Write]);
737            for scope in Scope::ALL {
738                assert_eq!(
739                    tenant_for(&operator, scope),
740                    Err(AuthError::Forbidden(scope)),
741                    "{scope:?}"
742                );
743            }
744        }
745    }
746}