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