1use 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#[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.registry.snapshot()?.key(id)?;
94 row.map(|row| {
95 Ok(StoredKey {
96 hash: row.token_hash,
97 tenant: row.tenant,
98 scopes: Scopes::parse(&row.scopes).ok_or(TenantError::Io)?,
100 revoked: row.revoked_at.is_some(),
101 })
102 })
103 .transpose()
104 }
105
106 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 commit(tx)?;
158 if let Some(t) = tenant {
159 self.copy_to_tenant(t, &made)?;
160 }
161 Ok(NewKey { id, token })
162 }
163
164 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: 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 pub fn revoke_key(&self, who: Actor, id: KeyId) -> Result<(), TenantError> {
208 let mut tx = self.registry.immediate()?;
209 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 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 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 #[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}