1use 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#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct KeyInfo {
17 pub id: KeyId,
18 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 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#[derive(Debug)]
49pub struct NewKey {
50 pub id: KeyId,
51 pub token: String,
52}
53
54#[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 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 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: Scopes::parse(&scopes).ok_or(TenantError::Io)?,
116 revoked,
117 })
118 })
119 .transpose()
120 }
121
122 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 drop(conn);
180 if let Some(t) = tenant {
181 self.copy_to_tenant(t, &made)?;
182 }
183 Ok(NewKey { id, token })
184 }
185
186 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: 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 pub fn revoke_key(&self, who: Actor, id: KeyId) -> Result<(), TenantError> {
246 let mut conn = self.conn();
247 let tx = immediate(&mut conn)?;
248 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 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 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 #[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}