Skip to main content

vcon_server/tenants/
keys.rs

1//! The access keys in server.sqlite: made, listed and revoked here, and
2//! read by `access` on every request.
3
4use rusqlite::{OptionalExtension, Transaction, params};
5use time::OffsetDateTime;
6use time::format_description::well_known::Rfc3339;
7
8use super::{Slot, TenantError, Tenants, immediate, io};
9use crate::access::{self, Scope, Scopes};
10use crate::audit::{Action, Actor, Entry};
11use crate::clock::{from_nanos, nanos};
12use crate::ids::{KeyId, TenantId};
13
14/// A key as `list_keys` shows it: never its token or hash.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct KeyInfo {
17    pub id: KeyId,
18    /// None for an operator key.
19    pub tenant: Option<TenantId>,
20    pub scopes: Scopes,
21    pub label: String,
22    pub created_at: OffsetDateTime,
23    pub revoked_at: Option<OffsetDateTime>,
24}
25
26impl KeyInfo {
27    /// The key as the admin API and the command line show it, never its
28    /// token: id, tenant (null for an operator key), scopes, label and
29    /// times in RFC 3339. None when a time cannot be written so.
30    pub fn to_json(&self) -> Option<serde_json::Value> {
31        let time = |at: OffsetDateTime| at.format(&Rfc3339).ok();
32        let revoked_at = match self.revoked_at {
33            Some(at) => Some(time(at)?),
34            None => None,
35        };
36        Some(serde_json::json!({
37            "id": self.id.to_string(),
38            "tenant": self.tenant.map(|t| t.to_string()),
39            "scopes": self.scopes.names(),
40            "label": self.label,
41            "created_at": time(self.created_at)?,
42            "revoked_at": revoked_at,
43        }))
44    }
45}
46
47/// A key just made. Its token is shown this once and never stored.
48#[derive(Debug)]
49pub struct NewKey {
50    pub id: KeyId,
51    pub token: String,
52}
53
54/// A key as `access` checks it.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub(crate) struct StoredKey {
57    pub(crate) hash: Vec<u8>,
58    pub(crate) tenant: Option<TenantId>,
59    pub(crate) scopes: Scopes,
60    pub(crate) revoked: bool,
61}
62
63impl Tenants {
64    /// Counts a failed check of a token. A known key's failures are
65    /// counted under its id in its tenant's log, or the admin log for an
66    /// operator key or a tenant whose log cannot be opened. Any other
67    /// failure is counted with no actor in the admin log, so tokens made up
68    /// one after another share a record. A failure to count leaves the
69    /// request refused all the same.
70    pub(crate) fn count_auth_failure(&self, known: Option<(KeyId, Option<TenantId>)>) {
71        let entry = |actor| Entry {
72            actor,
73            action: Action::AuthFailed,
74            target: None,
75            version: None,
76            status: 401,
77        };
78        if let Some((id, Some(tenant))) = known
79            && self
80                .store(tenant)
81                .is_ok_and(|store| store.count(&entry(Actor::Key(id))).is_ok())
82        {
83            return;
84        }
85        let actor = known.map_or(Actor::Unparsed, |(id, _)| Actor::Key(id));
86        let _ = self.count_admin(&entry(actor));
87    }
88
89    /// The key with this id, read afresh on every call, so a revocation
90    /// counts from the next request. Nothing is recorded here; `access`
91    /// records the outcome.
92    pub(crate) fn stored_key(&self, id: KeyId) -> Result<Option<StoredKey>, TenantError> {
93        let row = self
94            .conn()
95            .query_row(
96                "SELECT token_hash, tenant, scopes, revoked_at IS NOT NULL
97                 FROM api_key WHERE id = ?1",
98                [id.bytes()],
99                |r| {
100                    Ok((
101                        r.get::<_, Vec<u8>>(0)?,
102                        r.get::<_, Option<[u8; 16]>>(1)?,
103                        r.get::<_, String>(2)?,
104                        r.get::<_, bool>(3)?,
105                    ))
106                },
107            )
108            .optional()
109            .map_err(io)?;
110        row.map(|(hash, tenant, scopes, revoked)| {
111            Ok(StoredKey {
112                hash,
113                tenant: tenant.map(TenantId),
114                // Scopes that do not read back are damage, not none.
115                scopes: Scopes::parse(&scopes).ok_or(TenantError::Io)?,
116                revoked,
117            })
118        })
119        .transpose()
120    }
121
122    /// Makes a key. A tenant key holds any of write, read, delete, legal
123    /// and audit; a key with no tenant is an operator key, holds only admin,
124    /// and only the command line may make one.
125    pub fn create_key(
126        &self,
127        who: Actor,
128        tenant: Option<TenantId>,
129        scopes: Scopes,
130        label: &str,
131    ) -> Result<NewKey, TenantError> {
132        let entry = |status, target: Option<String>| Entry {
133            actor: who,
134            action: Action::KeyCreate,
135            target,
136            version: None,
137            status,
138        };
139        let shape = match tenant {
140            None if who != Actor::Cli => Some((403, TenantError::CliOnly)),
141            None if scopes != Scopes::of(&[Scope::Admin]) => Some((422, TenantError::BadScopes)),
142            Some(_) if scopes.is_empty() || scopes.contains(Scope::Admin) => {
143                Some((422, TenantError::BadScopes))
144            }
145            _ => None,
146        };
147        let closing = tenant.is_some_and(|t| matches!(self.stores().get(&t), Some(Slot::Closing)));
148        let mut conn = self.conn();
149        let tx = immediate(&mut conn)?;
150        let refused = match (shape, tenant) {
151            (Some(refused), _) => Some(refused),
152            (None, Some(t)) if !tenant_exists(&tx, t)? => Some((404, TenantError::NotFound)),
153            (None, Some(_)) if closing => Some((503, TenantError::Closing)),
154            _ => None,
155        };
156        if let Some((status, error)) = refused {
157            self.record(&tx, &entry(status, tenant.map(|t| t.to_string())))?;
158            tx.commit().map_err(io)?;
159            return Err(error);
160        }
161        let (id, token) = access::new_token();
162        tx.execute(
163            "INSERT INTO api_key (id, tenant, token_hash, scopes, label, created_at)
164             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
165            params![
166                id.bytes(),
167                tenant.map(|t| t.0),
168                access::token_hash(&token),
169                scopes.to_string(),
170                label,
171                nanos(self.clock.now())
172            ],
173        )
174        .map_err(io)?;
175        let made = entry(201, Some(id.to_string()));
176        self.record(&tx, &made)?;
177        tx.commit().map_err(io)?;
178        // The copy opens the tenant's Store, which reads server.sqlite.
179        drop(conn);
180        if let Some(t) = tenant {
181            self.copy_to_tenant(t, &made)?;
182        }
183        Ok(NewKey { id, token })
184    }
185
186    /// The keys of one tenant, or with None every key, oldest first.
187    pub fn list_keys(
188        &self,
189        who: Actor,
190        tenant: Option<TenantId>,
191    ) -> Result<Vec<KeyInfo>, TenantError> {
192        let entry = |status| Entry {
193            actor: who,
194            action: Action::KeyList,
195            target: tenant.map(|t| t.to_string()),
196            version: None,
197            status,
198        };
199        let mut conn = self.conn();
200        let tx = immediate(&mut conn)?;
201        if let Some(t) = tenant
202            && !tenant_exists(&tx, t)?
203        {
204            self.record(&tx, &entry(404))?;
205            tx.commit().map_err(io)?;
206            return Err(TenantError::NotFound);
207        }
208        let keys = tx
209            .prepare(
210                "SELECT id, tenant, scopes, label, created_at, revoked_at FROM api_key
211                 WHERE ?1 IS NULL OR tenant = ?1 ORDER BY created_at, id",
212            )
213            .map_err(io)?
214            .query_map([tenant.map(|t| t.0)], |r| {
215                Ok((
216                    r.get::<_, [u8; 8]>(0)?,
217                    r.get::<_, Option<[u8; 16]>>(1)?,
218                    r.get::<_, String>(2)?,
219                    r.get::<_, String>(3)?,
220                    r.get::<_, i64>(4)?,
221                    r.get::<_, Option<i64>>(5)?,
222                ))
223            })
224            .map_err(io)?
225            .map(|row| {
226                let (id, tenant, scopes, label, created_at, revoked_at) = row.map_err(io)?;
227                let time = |at| from_nanos(at).ok_or(TenantError::Io);
228                Ok(KeyInfo {
229                    id: KeyId(id),
230                    tenant: tenant.map(TenantId),
231                    // Scopes that do not read back are damage, not none.
232                    scopes: Scopes::parse(&scopes).ok_or(TenantError::Io)?,
233                    label,
234                    created_at: time(created_at)?,
235                    revoked_at: revoked_at.map(time).transpose()?,
236                })
237            })
238            .collect::<Result<Vec<_>, TenantError>>()?;
239        self.record(&tx, &entry(200))?;
240        tx.commit().map_err(io)?;
241        Ok(keys)
242    }
243
244    /// Revokes a key; revoking it again changes nothing.
245    pub fn revoke_key(&self, who: Actor, id: KeyId) -> Result<(), TenantError> {
246        let mut conn = self.conn();
247        let tx = immediate(&mut conn)?;
248        // The key's tenant, if the key is known: None for an operator key.
249        let found: Option<Option<[u8; 16]>> = tx
250            .query_row(
251                "SELECT tenant FROM api_key WHERE id = ?1",
252                [id.bytes()],
253                |r| r.get(0),
254            )
255            .optional()
256            .map_err(io)?;
257        if found.is_some() {
258            tx.execute(
259                "UPDATE api_key SET revoked_at = ?2 WHERE id = ?1 AND revoked_at IS NULL",
260                params![id.bytes(), nanos(self.clock.now())],
261            )
262            .map_err(io)?;
263        }
264        let entry = Entry {
265            actor: who,
266            action: Action::KeyRevoke,
267            target: Some(id.to_string()),
268            version: None,
269            status: if found.is_some() { 204 } else { 404 },
270        };
271        self.record(&tx, &entry)?;
272        tx.commit().map_err(io)?;
273        // The copy opens the tenant's Store, which reads server.sqlite.
274        drop(conn);
275        match found {
276            None => Err(TenantError::NotFound),
277            Some(Some(t)) => self.copy_to_tenant(TenantId(t), &entry),
278            Some(None) => Ok(()),
279        }
280    }
281}
282
283fn tenant_exists(tx: &Transaction, tenant: TenantId) -> Result<bool, TenantError> {
284    Ok(tx
285        .query_row("SELECT 1 FROM tenant WHERE id = ?1", [tenant.0], |_| Ok(()))
286        .optional()
287        .map_err(io)?
288        .is_some())
289}
290
291#[cfg(test)]
292mod tests {
293    use rusqlite::types::Value as Sql;
294    use time::macros::datetime;
295
296    use super::*;
297    use crate::access::{self, Scope};
298    use crate::tenants::Removal;
299    use crate::tenants::tests::{Fixture, fixture};
300
301    fn vcon_scopes() -> Scopes {
302        Scopes::of(&[Scope::Write, Scope::Read])
303    }
304
305    fn admin() -> Scopes {
306        Scopes::of(&[Scope::Admin])
307    }
308
309    fn tenant(f: &Fixture, name: &str) -> TenantId {
310        f.tenants.create(Actor::Cli, name, None).expect("made")
311    }
312
313    /// Every value in the key's row, as SQLite holds it.
314    fn row(f: &Fixture, id: KeyId) -> Vec<Sql> {
315        let conn = f.conn();
316        let mut stmt = conn
317            .prepare("SELECT * FROM api_key WHERE id = ?1")
318            .expect("prepare");
319        let columns = stmt.column_count();
320        stmt.query_row([id.bytes()], |r| {
321            (0..columns).map(|i| r.get::<_, Sql>(i)).collect()
322        })
323        .expect("a row")
324    }
325
326    fn holds(value: &Sql, needle: &[u8]) -> bool {
327        let bytes: &[u8] = match value {
328            Sql::Text(t) => t.as_bytes(),
329            Sql::Blob(b) => b,
330            _ => return false,
331        };
332        bytes.windows(needle.len()).any(|w| w == needle)
333    }
334
335    fn revoked_at(f: &Fixture, id: KeyId) -> Option<i64> {
336        f.conn()
337            .query_row(
338                "SELECT revoked_at FROM api_key WHERE id = ?1",
339                [id.bytes()],
340                |r| r.get(0),
341            )
342            .expect("a row")
343    }
344
345    #[test]
346    fn a_key_is_stored_as_its_hash_and_its_token_is_shown_once() {
347        let f = fixture();
348        let t = tenant(&f, "acme");
349        let key = f
350            .tenants
351            .create_key(Actor::Cli, Some(t), vcon_scopes(), "ingest")
352            .expect("made");
353        assert_eq!(access::parse(&key.token), Ok(key.id));
354        let secret = &key.token[21..];
355        let values = row(&f, key.id);
356        for value in &values {
357            assert!(!holds(value, key.token.as_bytes()), "the token is stored");
358            assert!(!holds(value, secret.as_bytes()), "the secret is stored");
359        }
360        let hash: Vec<u8> = f
361            .conn()
362            .query_row(
363                "SELECT token_hash FROM api_key WHERE id = ?1",
364                [key.id.bytes()],
365                |r| r.get(0),
366            )
367            .expect("a row");
368        assert_eq!(hash, access::token_hash(&key.token));
369        let made = f.admin_records("key_create");
370        assert_eq!(made.len(), 1);
371        assert_eq!(made[0]["actor"], "cli");
372        assert_eq!(made[0]["target"], key.id.to_string());
373        assert_eq!(made[0]["status"], 201);
374    }
375
376    #[test]
377    fn an_operator_key_holds_only_admin_and_only_the_command_line_makes_one() {
378        let f = fixture();
379        let key = f
380            .tenants
381            .create_key(Actor::Cli, None, admin(), "operator")
382            .expect("made");
383        let listed = f.tenants.list_keys(Actor::Cli, None).expect("listed");
384        assert_eq!(listed[0].id, key.id);
385        assert_eq!(listed[0].tenant, None);
386        assert_eq!(listed[0].scopes, admin());
387
388        let api = Actor::Key(key.id);
389        assert_eq!(
390            f.tenants.create_key(api, None, admin(), "another").err(),
391            Some(TenantError::CliOnly)
392        );
393        for scopes in [
394            Scopes::of(&[]),
395            Scopes::of(&[Scope::Admin, Scope::Read]),
396            vcon_scopes(),
397        ] {
398            assert_eq!(
399                f.tenants.create_key(Actor::Cli, None, scopes, "x").err(),
400                Some(TenantError::BadScopes),
401                "{scopes}"
402            );
403        }
404        let statuses: Vec<_> = f
405            .admin_records("key_create")
406            .into_iter()
407            .map(|r| r["status"].clone())
408            .collect();
409        assert_eq!(statuses, [201, 403, 422, 422, 422]);
410        assert_eq!(
411            f.tenants.list_keys(Actor::Cli, None).map(|k| k.len()),
412            Ok(1)
413        );
414    }
415
416    #[test]
417    fn a_tenant_key_holds_only_vcon_scopes() {
418        let f = fixture();
419        let t = tenant(&f, "acme");
420        for scopes in [
421            Scopes::of(&[]),
422            admin(),
423            Scopes::of(&[Scope::Write, Scope::Admin]),
424        ] {
425            assert_eq!(
426                f.tenants.create_key(Actor::Cli, Some(t), scopes, "x").err(),
427                Some(TenantError::BadScopes),
428                "{scopes}"
429            );
430        }
431        let all = Scopes::of(&[
432            Scope::Write,
433            Scope::Read,
434            Scope::Delete,
435            Scope::Legal,
436            Scope::Audit,
437        ]);
438        assert!(
439            f.tenants
440                .create_key(Actor::Cli, Some(t), all, "all")
441                .is_ok()
442        );
443    }
444
445    #[test]
446    fn a_key_for_an_unknown_or_closing_tenant_is_refused() {
447        let f = fixture();
448        let unknown = TenantId::generate();
449        assert_eq!(
450            f.tenants
451                .create_key(Actor::Cli, Some(unknown), vcon_scopes(), "x")
452                .err(),
453            Some(TenantError::NotFound)
454        );
455        let t = tenant(&f, "acme");
456        f.tenants.drain(t).expect("drains");
457        assert_eq!(
458            f.tenants
459                .create_key(Actor::Cli, Some(t), vcon_scopes(), "x")
460                .err(),
461            Some(TenantError::Closing)
462        );
463        let refused = f.admin_records("key_create");
464        assert_eq!(refused[0]["status"], 404);
465        assert_eq!(refused[0]["target"], unknown.to_string());
466        assert_eq!(refused[1]["status"], 503);
467        assert_eq!(f.count_keys(), 0);
468    }
469
470    #[test]
471    fn a_key_whose_record_fails_is_not_made() {
472        let f = fixture();
473        let t = tenant(&f, "acme");
474        f.conn()
475            .execute_batch(
476                "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
477                 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
478            )
479            .expect("trigger");
480        assert_eq!(
481            f.tenants
482                .create_key(Actor::Cli, Some(t), vcon_scopes(), "x")
483                .err(),
484            Some(TenantError::AuditFailed)
485        );
486        assert_eq!(f.count_keys(), 0);
487    }
488
489    #[test]
490    fn keys_are_listed_per_tenant_or_all_oldest_first() {
491        let f = fixture();
492        let (a, b) = (tenant(&f, "a"), tenant(&f, "b"));
493        let first = f
494            .tenants
495            .create_key(Actor::Cli, Some(a), vcon_scopes(), "first")
496            .expect("made");
497        f.clock.set(datetime!(2026-10-23 13:00 UTC));
498        let other = f
499            .tenants
500            .create_key(Actor::Cli, Some(b), vcon_scopes(), "other")
501            .expect("made");
502        f.clock.set(datetime!(2026-10-23 14:00 UTC));
503        let second = f
504            .tenants
505            .create_key(Actor::Cli, Some(a), Scopes::of(&[Scope::Legal]), "second")
506            .expect("made");
507        f.tenants
508            .revoke_key(Actor::Cli, second.id)
509            .expect("revoked");
510
511        let a_keys = f.tenants.list_keys(Actor::Cli, Some(a)).expect("listed");
512        assert_eq!(
513            a_keys,
514            [
515                KeyInfo {
516                    id: first.id,
517                    tenant: Some(a),
518                    scopes: vcon_scopes(),
519                    label: "first".to_owned(),
520                    created_at: datetime!(2026-10-23 12:00 UTC),
521                    revoked_at: None,
522                },
523                KeyInfo {
524                    id: second.id,
525                    tenant: Some(a),
526                    scopes: Scopes::of(&[Scope::Legal]),
527                    label: "second".to_owned(),
528                    created_at: datetime!(2026-10-23 14:00 UTC),
529                    revoked_at: Some(datetime!(2026-10-23 14:00 UTC)),
530                },
531            ]
532        );
533        let all: Vec<KeyId> = f
534            .tenants
535            .list_keys(Actor::Cli, None)
536            .expect("listed")
537            .into_iter()
538            .map(|k| k.id)
539            .collect();
540        assert_eq!(all, [first.id, other.id, second.id]);
541        assert_eq!(
542            f.tenants
543                .list_keys(Actor::Cli, Some(TenantId::generate()))
544                .err(),
545            Some(TenantError::NotFound)
546        );
547        let lists = f.admin_records("key_list");
548        let statuses: Vec<_> = lists.iter().map(|r| r["status"].clone()).collect();
549        assert_eq!(statuses, [200, 200, 404]);
550        assert_eq!(lists[0]["target"], a.to_string());
551        assert_eq!(lists[1]["target"], serde_json::Value::Null);
552    }
553
554    #[test]
555    fn a_stored_scope_that_does_not_read_back_is_damage() {
556        let f = fixture();
557        let t = tenant(&f, "acme");
558        let key = f
559            .tenants
560            .create_key(Actor::Cli, Some(t), vcon_scopes(), "x")
561            .expect("made");
562        f.conn()
563            .execute(
564                "UPDATE api_key SET scopes = 'owner' WHERE id = ?1",
565                [key.id.bytes()],
566            )
567            .expect("damage");
568        assert_eq!(
569            f.tenants.list_keys(Actor::Cli, None).err(),
570            Some(TenantError::Io)
571        );
572    }
573
574    #[test]
575    fn a_key_is_revoked_once_and_an_unknown_one_is_not_found() {
576        let f = fixture();
577        let t = tenant(&f, "acme");
578        let key = f
579            .tenants
580            .create_key(Actor::Cli, Some(t), vcon_scopes(), "x")
581            .expect("made");
582        f.clock.set(datetime!(2026-10-23 13:00 UTC));
583        f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
584        let first = revoked_at(&f, key.id);
585        assert!(first.is_some());
586        f.clock.set(datetime!(2026-10-23 14:00 UTC));
587        f.tenants
588            .revoke_key(Actor::Cli, key.id)
589            .expect("again, no change");
590        assert_eq!(revoked_at(&f, key.id), first, "the first time stands");
591        let unknown = KeyId::generate();
592        assert_eq!(
593            f.tenants.revoke_key(Actor::Cli, unknown),
594            Err(TenantError::NotFound)
595        );
596        let records = f.admin_records("key_revoke");
597        let statuses: Vec<_> = records.iter().map(|r| r["status"].clone()).collect();
598        assert_eq!(statuses, [204, 204, 404]);
599        assert_eq!(records[0]["target"], key.id.to_string());
600        assert_eq!(records[2]["target"], unknown.to_string());
601    }
602
603    fn tenant_records(f: &Fixture, t: TenantId, action: &str) -> Vec<serde_json::Value> {
604        crate::audit::testing::exported(&f.tenant_catalog(t), None)
605            .into_iter()
606            .filter(|r| r["action"] == action)
607            .collect()
608    }
609
610    #[test]
611    fn a_tenant_key_made_or_revoked_is_recorded_in_its_tenants_log_too() {
612        let f = fixture();
613        let t = tenant(&f, "acme");
614        let key = f
615            .tenants
616            .create_key(Actor::Cli, Some(t), vcon_scopes(), "x")
617            .expect("made");
618        f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
619        let made = tenant_records(&f, t, "key_create");
620        assert_eq!(made.len(), 1);
621        assert_eq!(made[0]["actor"], "cli");
622        assert_eq!(made[0]["target"], key.id.to_string());
623        assert_eq!(made[0]["status"], 201);
624        let revoked = tenant_records(&f, t, "key_revoke");
625        assert_eq!(revoked.len(), 1);
626        assert_eq!(revoked[0]["target"], key.id.to_string());
627        assert_eq!(revoked[0]["status"], 204);
628
629        let operator = f
630            .tenants
631            .create_key(Actor::Cli, None, admin(), "op")
632            .expect("made");
633        f.tenants
634            .revoke_key(Actor::Cli, operator.id)
635            .expect("revoked");
636        assert_eq!(tenant_records(&f, t, "key_create").len(), 1, "only its own");
637        let refused = f
638            .tenants
639            .create_key(Actor::Cli, Some(t), admin(), "x")
640            .err();
641        assert_eq!(refused, Some(TenantError::BadScopes));
642        assert_eq!(
643            tenant_records(&f, t, "key_create").len(),
644            1,
645            "a refusal stays in the admin log"
646        );
647    }
648
649    /// The change commits with its admin record; a failed copy in the
650    /// tenant's log leaves it standing and answers AuditFailed.
651    #[test]
652    fn a_key_change_whose_tenant_copy_fails_stands() {
653        let f = fixture();
654        let t = tenant(&f, "acme");
655        f.tenant_catalog(t)
656            .execute_batch(
657                "CREATE TRIGGER dead BEFORE INSERT ON audit
658                 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
659            )
660            .expect("trigger");
661        assert_eq!(
662            f.tenants
663                .create_key(Actor::Cli, Some(t), vcon_scopes(), "x")
664                .err(),
665            Some(TenantError::AuditFailed)
666        );
667        let keys = f.tenants.list_keys(Actor::Cli, Some(t)).expect("listed");
668        assert_eq!(keys.len(), 1, "the key stands");
669        assert_eq!(
670            f.tenants.revoke_key(Actor::Cli, keys[0].id),
671            Err(TenantError::AuditFailed)
672        );
673        assert!(
674            revoked_at(&f, keys[0].id).is_some(),
675            "the revocation stands"
676        );
677    }
678
679    #[test]
680    fn removing_a_tenant_revokes_its_keys_in_the_same_transaction() {
681        let f = fixture();
682        let (a, b) = (tenant(&f, "a"), tenant(&f, "b"));
683        let mine: Vec<KeyId> = (0..2)
684            .map(|_| {
685                f.tenants
686                    .create_key(Actor::Cli, Some(a), vcon_scopes(), "a")
687                    .expect("made")
688                    .id
689            })
690            .collect();
691        let theirs = f
692            .tenants
693            .create_key(Actor::Cli, Some(b), vcon_scopes(), "b")
694            .expect("made")
695            .id;
696        let operator = f
697            .tenants
698            .create_key(Actor::Cli, None, admin(), "op")
699            .expect("made")
700            .id;
701
702        f.conn()
703            .execute_batch(
704                "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
705                 WHEN NEW.action = 'tenant_remove'
706                 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
707            )
708            .expect("trigger");
709        assert_eq!(
710            f.tenants.remove(Actor::Cli, a, Removal::Empty),
711            Err(TenantError::AuditFailed)
712        );
713        for id in &mine {
714            assert_eq!(
715                revoked_at(&f, *id),
716                None,
717                "a failed removal revokes nothing"
718            );
719        }
720        f.conn()
721            .execute_batch("DROP TRIGGER no_audit")
722            .expect("drop trigger");
723
724        f.clock.set(datetime!(2026-10-23 13:00 UTC));
725        f.tenants
726            .remove(Actor::Cli, a, Removal::Empty)
727            .expect("removed");
728        let at = crate::clock::nanos(datetime!(2026-10-23 13:00 UTC));
729        for id in &mine {
730            assert_eq!(revoked_at(&f, *id), Some(at));
731        }
732        assert_eq!(revoked_at(&f, theirs), None);
733        assert_eq!(revoked_at(&f, operator), None);
734    }
735
736    impl Fixture {
737        fn count_keys(&self) -> i64 {
738            self.conn()
739                .query_row("SELECT count(*) FROM api_key", [], |r| r.get(0))
740                .expect("count")
741        }
742    }
743}