Skip to main content

vcon_server/
api.rs

1//! The HTTP API, served over TLS only.
2
3use std::collections::BTreeSet;
4use std::future::Future;
5use std::io;
6use std::path::Path;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use axum::Router;
11use axum::body::Body;
12use axum::body::Bytes;
13use axum::extract::{MatchedPath, Path as UriPath, Request, State};
14use axum::http::header::{
15    AUTHORIZATION, CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE, ETAG, IF_MATCH, LOCATION,
16    WWW_AUTHENTICATE,
17};
18use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode, Uri};
19use axum::middleware::Next;
20use axum::response::{IntoResponse, Response};
21use axum::routing::{get, patch, post, put};
22use http_body_util::LengthLimitError;
23use hyper_util::rt::{TokioExecutor, TokioIo};
24use hyper_util::server::conn::auto;
25use hyper_util::service::TowerToHyperService;
26use rustls::ServerConfig;
27use rustls::pki_types::pem::PemObject;
28use rustls::pki_types::{CertificateDer, PrivateKeyDer};
29use serde::de::{Deserialize, DeserializeOwned, Deserializer, Error as _};
30use tokio::net::TcpListener;
31use tokio_rustls::TlsAcceptor;
32
33use crate::access::{self, AuthError, Caller, Scope, Scopes};
34use crate::audit::{Action, Actor, AuditQuery, Entry};
35use crate::ids::TenantId;
36use crate::ids::{KeyId, VersionId};
37use crate::retention::Retention;
38use crate::store::{Caller as StoreCaller, Created, ListQuery, Store, StoreError};
39use crate::tenants::{KeyInfo, Removal, TenantError, TenantInfo, Tenants};
40use time::format_description::well_known::Rfc3339;
41use uuid::Uuid;
42use vcon::read::{Inspection, Rejection};
43
44/// The limits from configuration that the API applies to every request.
45#[derive(Debug, Clone, Copy)]
46pub struct Limits {
47    /// The most bytes a request body may hold, as received.
48    pub body: usize,
49    /// The most bytes gzip inflation may produce while a body is inspected.
50    pub inflated: u64,
51    /// How long a request body may take to arrive.
52    pub body_timeout: Duration,
53}
54
55/// What the API serves from.
56pub struct App {
57    pub tenants: Arc<Tenants>,
58    pub limits: Limits,
59    /// The must_support extensions this server supports.
60    pub supported: BTreeSet<String>,
61    /// The retention period of a tenant that sets none.
62    pub retention_default: Retention,
63}
64
65/// The routes, served from `app`.
66pub fn router(app: Arc<App>) -> Router {
67    Router::new()
68        .route("/vcons", post(create).get(list))
69        .route("/vcons/{uuid}", get(read).put(replace).delete(delete))
70        .route("/health", get(health))
71        .route("/key", get(key_info))
72        .route("/audit", get(audit_read))
73        .route("/admin/audit", get(admin_audit_read))
74        .route("/openapi.json", get(openapi))
75        .route("/legal/vcons/{uuid}", get(legal_read))
76        .route("/legal/vcons/{uuid}/versions", get(legal_versions))
77        .route("/legal/vcons/{uuid}/versions/{version}", get(legal_version))
78        .route("/legal/vcons/{uuid}/restore", post(legal_restore))
79        .route("/legal/vcons/{uuid}/hold", put(hold_on).delete(hold_off))
80        .route("/admin/tenants", post(tenant_create).get(tenant_list))
81        .route(
82            "/admin/tenants/{id}",
83            patch(tenant_retention).delete(tenant_remove),
84        )
85        .route("/admin/tenants/{id}/keys", post(key_create).get(key_list))
86        .route("/admin/keys/{id}", axum::routing::delete(key_revoke))
87        .merge(crate::console::router())
88        .layer(axum::middleware::from_fn(log_request))
89        .with_state(app)
90}
91
92/// Logs each request once it is answered: a request id, the method, the
93/// route template, never the path, which holds ids, the status and how
94/// long it took. Bodies, tokens, uuids and tenant ids are in the audit
95/// only.
96async fn log_request(matched: Option<MatchedPath>, request: Request, next: Next) -> Response {
97    let started = Instant::now();
98    let method = request.method().clone();
99    let route = matched
100        .as_ref()
101        .map_or("unmatched", MatchedPath::as_str)
102        .to_owned();
103    let answer = next.run(request).await;
104    // In the unit tests every answer must be one the description lists, so
105    // a route or status added without its description fails the test
106    // that produced it. A method a path does not take (405) and a path no
107    // route matches are the router's, not a route's.
108    #[cfg(test)]
109    if route != "unmatched" && answer.status() != StatusCode::METHOD_NOT_ALLOWED {
110        let status = answer.status().as_u16();
111        assert!(
112            description::lists(method.as_str(), &route, status),
113            "openapi.json does not list {status} for {method} {route}"
114        );
115    }
116    let mut id = [0; 8];
117    // AWS-LC's RAND_bytes aborts the process when it cannot get enough
118    // randomness, so `fill` never returns its error.
119    aws_lc_rs::rand::fill(&mut id).expect("AWS-LC aborts instead of failing");
120    tracing::info!(
121        request_id = %u64::from_be_bytes(id),
122        %method,
123        %route,
124        status = answer.status().as_u16(),
125        duration_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
126        "request"
127    );
128    answer
129}
130
131/// The OpenAPI description of every route, as `GET /openapi.json` serves
132/// it and the website publishes it.
133pub const OPENAPI: &str = include_str!("../openapi.json");
134
135/// `GET /openapi.json`: the description, to any valid key, recorded as
136/// openapi in the key's tenant log or the admin log.
137async fn openapi(State(app): State<Arc<App>>, headers: HeaderMap) -> Response {
138    let caller = match authenticated(&app, &headers).await {
139        Ok(caller) => caller,
140        Err(refused) => return refused.into_response(),
141    };
142    let tenants = app.tenants.clone();
143    let recorded = blocking(move || {
144        let entry = |status| Entry {
145            actor: Actor::Key(caller.key),
146            action: Action::Openapi,
147            target: None,
148            version: None,
149            status,
150        };
151        let logged = |done: bool, answer| {
152            if done {
153                answer
154            } else {
155                Err(Problem::AuditFailed)
156            }
157        };
158        match caller.tenant.map(|t| tenants.store(t)) {
159            Some(Ok(store)) => logged(store.log(&entry(200)).is_ok(), Ok(())),
160            // A tenant being removed, or whose log cannot be opened, answers
161            // 503 here too, recorded in the admin log.
162            Some(Err(_)) => logged(
163                tenants.record_alone(&entry(503)).is_ok(),
164                Err(Problem::Unavailable),
165            ),
166            None => logged(tenants.record_alone(&entry(200)).is_ok(), Ok(())),
167        }
168    })
169    .await;
170    match recorded {
171        Ok(()) => (
172            StatusCode::OK,
173            [(CONTENT_TYPE, "application/json")],
174            OPENAPI,
175        )
176            .into_response(),
177        Err(refused) => refused.into_response(),
178    }
179}
180
181/// `POST /vcons`: stores a new vCon, or answers 200 when the same bytes
182/// are already current.
183async fn create(State(app): State<Arc<App>>, headers: HeaderMap, body: Body) -> Response {
184    let admitted = match admit(&app, &headers, body, Scope::Write, Action::Create).await {
185        Ok(admitted) => admitted,
186        Err(refused) => return refused.into_response(),
187    };
188    let found = match inspect(&app, &admitted, &headers, Action::Create).await {
189        Ok(found) => found,
190        Err(refused) => return refused.into_response(),
191    };
192    let (store, body, key) = (
193        admitted.store.clone(),
194        admitted.body.clone(),
195        admitted.caller.key,
196    );
197    // Store records its own outcomes.
198    match blocking(move || store.create(&StoreCaller { key }, &found, &body)).await {
199        Ok(Created::New(version)) => (
200            StatusCode::CREATED,
201            [
202                (LOCATION, format!("/vcons/{}", found.uuid)),
203                (ETAG, entity_tag(version)),
204            ],
205        )
206            .into_response(),
207        Ok(Created::Same(version)) => {
208            (StatusCode::OK, [(ETAG, entity_tag(version))]).into_response()
209        }
210        Err(error) => failed(error).into_response(),
211    }
212}
213
214/// Reads an admitted body as a vCon: its media type must be one a vCon is
215/// sent as (415), and Format must take it (422, or 413 when it inflates
216/// too far). A refusal here is recorded here, since Store never sees it.
217async fn inspect(
218    app: &Arc<App>,
219    admitted: &Admitted,
220    headers: &HeaderMap,
221    action: Action,
222) -> Result<Inspection, Problem> {
223    let content_type = headers
224        .get(CONTENT_TYPE)
225        .map(|v| v.to_str().unwrap_or("\u{0}").to_owned());
226    let found = if content_type.as_deref().is_none_or(is_vcon_media_type) {
227        let (worker, body) = (app.clone(), admitted.body.clone());
228        blocking(move || {
229            let limits = vcon::read::Limits::new(worker.limits.inflated);
230            vcon::read::inspect(&body, content_type.as_deref(), &limits, &worker.supported)
231                .map_err(|r| rejected(&r))
232        })
233        .await
234    } else {
235        Err(Problem::UnsupportedMediaType)
236    };
237    match found {
238        Ok(found) => Ok(found),
239        Err(refused) => {
240            let entry = refusal(admitted.caller.key, action, refused);
241            Err(app.refuse(Some(admitted.tenant), entry, refused).await)
242        }
243    }
244}
245
246/// `GET /vcons`: a page of the tenant's live vCons, newest stored first,
247/// as JSON, with the cursor of the next page.
248async fn list(State(app): State<Arc<App>>, uri: Uri, headers: HeaderMap, body: Body) -> Response {
249    let admitted = match admit(&app, &headers, body, Scope::Read, Action::List).await {
250        Ok(admitted) => admitted,
251        Err(refused) => return refused.into_response(),
252    };
253    let Ok(query) = ListQuery::from_query(uri.query()) else {
254        let refused = Problem::BadQuery;
255        let entry = refusal(admitted.caller.key, Action::List, refused);
256        return app
257            .refuse(Some(admitted.tenant), entry, refused)
258            .await
259            .into_response();
260    };
261    let (store, key) = (admitted.store.clone(), admitted.caller.key);
262    // Store records its own outcomes.
263    match blocking(move || store.list(&StoreCaller { key }, &query)).await {
264        Ok(listing) => json_answer(StatusCode::OK, &listing.to_json()),
265        Err(error) => failed(error).into_response(),
266    }
267}
268
269/// The media type of an audit page: JSON lines, one record a line.
270const JSON_LINES: &str = "application/jsonl";
271
272/// `GET /audit`: a page of the key's tenant's log, the lines `audit export`
273/// writes for the same records.
274async fn audit_read(
275    State(app): State<Arc<App>>,
276    uri: Uri,
277    headers: HeaderMap,
278    body: Body,
279) -> Response {
280    let admitted = match admit(&app, &headers, body, Scope::Audit, Action::AuditRead).await {
281        Ok(admitted) => admitted,
282        Err(refused) => return refused.into_response(),
283    };
284    let Ok(query) = AuditQuery::from_query(uri.query()) else {
285        let refused = Problem::BadQuery;
286        let entry = refusal(admitted.caller.key, Action::AuditRead, refused);
287        return app
288            .refuse(Some(admitted.tenant), entry, refused)
289            .await
290            .into_response();
291    };
292    let (store, key) = (admitted.store.clone(), admitted.caller.key);
293    // Store records its own outcomes.
294    match blocking(move || store.audit_page(&StoreCaller { key }, &query)).await {
295        Ok(lines) => (StatusCode::OK, [(CONTENT_TYPE, JSON_LINES)], lines).into_response(),
296        Err(error) => failed(error).into_response(),
297    }
298}
299
300/// `GET /admin/audit`: a page of the admin log, the lines
301/// `audit export --admin` writes for the same records.
302async fn admin_audit_read(
303    State(app): State<Arc<App>>,
304    uri: Uri,
305    headers: HeaderMap,
306    body: Body,
307) -> Response {
308    let action = Action::AuditRead;
309    let read = async {
310        let (key, _) = admin_admit(&app, &headers, body, action).await?;
311        let Ok(query) = AuditQuery::from_query(uri.query()) else {
312            let refused = Problem::BadQuery;
313            return Err(app
314                .refuse(None, refusal(key, action, refused), refused)
315                .await);
316        };
317        let tenants = app.tenants.clone();
318        // Tenants records its own outcomes.
319        blocking(move || tenants.audit_page(Actor::Key(key), &query))
320            .await
321            .map_err(Problem::Tenant)
322    };
323    match read.await {
324        Ok(lines) => (StatusCode::OK, [(CONTENT_TYPE, JSON_LINES)], lines).into_response(),
325        Err(refused) => refused.into_response(),
326    }
327}
328
329/// `GET /vcons/{uuid}`: the current version, as stored.
330async fn read(
331    State(app): State<Arc<App>>,
332    UriPath(uuid): UriPath<String>,
333    headers: HeaderMap,
334    body: Body,
335) -> Response {
336    let admitted = match admit(&app, &headers, body, Scope::Read, Action::Read).await {
337        Ok(admitted) => admitted,
338        Err(refused) => return refused.into_response(),
339    };
340    let Some(uuid) = vcon::parse_uuid(&uuid) else {
341        let refused = Problem::BadPathUuid;
342        let entry = refusal(admitted.caller.key, Action::Read, refused);
343        return app
344            .refuse(Some(admitted.tenant), entry, refused)
345            .await
346            .into_response();
347    };
348    let (store, key) = (admitted.store.clone(), admitted.caller.key);
349    match blocking(move || store.get(&StoreCaller { key }, uuid)).await {
350        Ok(stored) => {
351            let content_type = if stored.gzip {
352                "application/vcon+gzip"
353            } else {
354                "application/vcon"
355            };
356            (
357                StatusCode::OK,
358                [
359                    (CONTENT_TYPE, content_type.to_owned()),
360                    (ETAG, entity_tag(stored.version)),
361                ],
362                stored.body,
363            )
364                .into_response()
365        }
366        // Store records its own outcomes.
367        Err(error) => failed(error).into_response(),
368    }
369}
370
371/// `PUT /vcons/{uuid}`: replaces the current version named by `If-Match`.
372async fn replace(
373    State(app): State<Arc<App>>,
374    UriPath(uuid): UriPath<String>,
375    headers: HeaderMap,
376    body: Body,
377) -> Response {
378    let admitted = match admit(&app, &headers, body, Scope::Write, Action::Replace).await {
379        Ok(admitted) => admitted,
380        Err(refused) => return refused.into_response(),
381    };
382    let (shared, key, tenant) = (&app, admitted.caller.key, admitted.tenant);
383    let refuse = |refused| async move {
384        let entry = refusal(key, Action::Replace, refused);
385        shared
386            .refuse(Some(tenant), entry, refused)
387            .await
388            .into_response()
389    };
390    let Some(uuid) = vcon::parse_uuid(&uuid) else {
391        return refuse(Problem::BadPathUuid).await;
392    };
393    let current = match if_match(&headers) {
394        None => return refuse(Problem::PreconditionRequired).await,
395        Some(None) => return refuse(Problem::Store(StoreError::PreconditionFailed)).await,
396        Some(Some(current)) => current,
397    };
398    let found = match inspect(&app, &admitted, &headers, Action::Replace).await {
399        Ok(found) => found,
400        Err(refused) => return refused.into_response(),
401    };
402    if found.uuid != uuid {
403        return refuse(Problem::PathUuidMismatch).await;
404    }
405    let (store, body, key) = (
406        admitted.store.clone(),
407        admitted.body.clone(),
408        admitted.caller.key,
409    );
410    // Store records its own outcomes.
411    match blocking(move || store.replace(&StoreCaller { key }, current, &found, &body)).await {
412        Ok(version) => (StatusCode::OK, [(ETAG, entity_tag(version))]).into_response(),
413        Err(error) => failed(error).into_response(),
414    }
415}
416
417/// The version an `If-Match` header names: None without the header, and
418/// Some(None) when it is anything but one strong tag holding a version id,
419/// such as `*`, a weak tag or a list, since that names no version this
420/// server could replace.
421fn if_match(headers: &HeaderMap) -> Option<Option<VersionId>> {
422    let value = headers.get(IF_MATCH)?;
423    Some(
424        value
425            .to_str()
426            .ok()
427            .and_then(|tag| tag.trim().strip_prefix('"')?.strip_suffix('"'))
428            .and_then(VersionId::parse),
429    )
430}
431
432/// `DELETE /vcons/{uuid}`: hides the vCon; also 204 when it is already
433/// deleted or purged.
434async fn delete(
435    State(app): State<Arc<App>>,
436    UriPath(uuid): UriPath<String>,
437    headers: HeaderMap,
438    body: Body,
439) -> Response {
440    let admitted = match admit(&app, &headers, body, Scope::Delete, Action::Delete).await {
441        Ok(admitted) => admitted,
442        Err(refused) => return refused.into_response(),
443    };
444    let Some(uuid) = vcon::parse_uuid(&uuid) else {
445        let refused = Problem::BadPathUuid;
446        let entry = refusal(admitted.caller.key, Action::Delete, refused);
447        return app
448            .refuse(Some(admitted.tenant), entry, refused)
449            .await
450            .into_response();
451    };
452    // The tenant's own period, else the server's, read now so a change
453    // counts from the next delete. Unread, nothing is deleted.
454    let (tenants, tenant) = (app.tenants.clone(), admitted.tenant);
455    let retention = match blocking(move || tenants.retention(tenant)).await {
456        Ok(own) => own.unwrap_or(app.retention_default),
457        Err(_) => {
458            let refused = Problem::Unavailable;
459            let entry = refusal(admitted.caller.key, Action::Delete, refused);
460            return app
461                .refuse(Some(tenant), entry, refused)
462                .await
463                .into_response();
464        }
465    };
466    let (store, key) = (admitted.store.clone(), admitted.caller.key);
467    // Store records its own outcomes.
468    match blocking(move || store.delete(&StoreCaller { key }, uuid, retention)).await {
469        Ok(()) => StatusCode::NO_CONTENT.into_response(),
470        Err(error) => failed(error).into_response(),
471    }
472}
473
474/// Whether a legal read is of a live or a deleted uuid.
475const VCON_STATE: HeaderName = HeaderName::from_static("vcon-state");
476
477/// The legal routes' shared start: a key with the legal scope, then the
478/// path's uuid, each refusal recorded as `action`.
479async fn legal_admit(
480    app: &Arc<App>,
481    headers: &HeaderMap,
482    body: Body,
483    action: Action,
484    uuid: &str,
485) -> Result<(Admitted, Uuid), Problem> {
486    let admitted = admit(app, headers, body, Scope::Legal, action).await?;
487    let Some(uuid) = vcon::parse_uuid(uuid) else {
488        let refused = Problem::BadPathUuid;
489        let entry = refusal(admitted.caller.key, action, refused);
490        return Err(app.refuse(Some(admitted.tenant), entry, refused).await);
491    };
492    Ok((admitted, uuid))
493}
494
495/// `GET /legal/vcons/{uuid}`: the current version, or the latest kept one
496/// of a deleted uuid, as stored, with `Vcon-State`.
497async fn legal_read(
498    State(app): State<Arc<App>>,
499    UriPath(uuid): UriPath<String>,
500    headers: HeaderMap,
501    body: Body,
502) -> Response {
503    match legal_admit(&app, &headers, body, Action::LegalRead, &uuid).await {
504        Ok((admitted, uuid)) => legal_body(&admitted, uuid, None).await,
505        Err(refused) => refused.into_response(),
506    }
507}
508
509/// `GET /legal/vcons/{uuid}/versions/{version}`: that kept version, as
510/// stored.
511async fn legal_version(
512    State(app): State<Arc<App>>,
513    UriPath((uuid, version)): UriPath<(String, String)>,
514    headers: HeaderMap,
515    body: Body,
516) -> Response {
517    let (admitted, uuid) = match legal_admit(&app, &headers, body, Action::LegalRead, &uuid).await {
518        Ok(admitted) => admitted,
519        Err(refused) => return refused.into_response(),
520    };
521    let Some(version) = VersionId::parse(&version) else {
522        let refused = Problem::BadPathVersion;
523        let entry = refusal(admitted.caller.key, Action::LegalRead, refused);
524        return app
525            .refuse(Some(admitted.tenant), entry, refused)
526            .await
527            .into_response();
528    };
529    legal_body(&admitted, uuid, Some(version)).await
530}
531
532/// A legal read's answer: the body as stored, its tag and the uuid's state.
533async fn legal_body(admitted: &Admitted, uuid: Uuid, version: Option<VersionId>) -> Response {
534    let (store, key) = (admitted.store.clone(), admitted.caller.key);
535    // Store records its own outcomes.
536    match blocking(move || store.legal_get(&StoreCaller { key }, uuid, version)).await {
537        Ok(read) => {
538            let content_type = if read.stored.gzip {
539                "application/vcon+gzip"
540            } else {
541                "application/vcon"
542            };
543            let state = if read.deleted { "deleted" } else { "live" };
544            (
545                StatusCode::OK,
546                [
547                    (CONTENT_TYPE, content_type.to_owned()),
548                    (ETAG, entity_tag(read.stored.version)),
549                    (VCON_STATE, state.to_owned()),
550                ],
551                read.stored.body,
552            )
553                .into_response()
554        }
555        Err(error) => failed(error).into_response(),
556    }
557}
558
559/// `GET /legal/vcons/{uuid}/versions`: every kept version, oldest first,
560/// as JSON: its id, when it was stored and hidden (RFC 3339, UTC), and
561/// whether it is current.
562async fn legal_versions(
563    State(app): State<Arc<App>>,
564    UriPath(uuid): UriPath<String>,
565    headers: HeaderMap,
566    body: Body,
567) -> Response {
568    let (admitted, uuid) =
569        match legal_admit(&app, &headers, body, Action::LegalVersions, &uuid).await {
570            Ok(admitted) => admitted,
571            Err(refused) => return refused.into_response(),
572        };
573    let (store, key) = (admitted.store.clone(), admitted.caller.key);
574    match blocking(move || store.legal_versions(&StoreCaller { key }, uuid)).await {
575        Ok(versions) => {
576            let time = |at: time::OffsetDateTime| at.format(&Rfc3339).ok();
577            let listed: Option<Vec<serde_json::Value>> = versions
578                .iter()
579                .map(|v| {
580                    let hidden_at = match v.hidden_at {
581                        Some(at) => Some(time(at)?),
582                        None => None,
583                    };
584                    Some(serde_json::json!({
585                        "version": v.version.to_string(),
586                        "stored_at": time(v.stored_at)?,
587                        "hidden_at": hidden_at,
588                        "current": v.current,
589                    }))
590                })
591                .collect();
592            match listed {
593                Some(listed) => (
594                    StatusCode::OK,
595                    [(CONTENT_TYPE, "application/json")],
596                    serde_json::Value::Array(listed).to_string(),
597                )
598                    .into_response(),
599                None => failed(StoreError::Damaged).into_response(),
600            }
601        }
602        Err(error) => failed(error).into_response(),
603    }
604}
605
606/// `POST /legal/vcons/{uuid}/restore`: makes a deleted vCon live again and
607/// answers 200 with the restored version's tag.
608async fn legal_restore(
609    State(app): State<Arc<App>>,
610    UriPath(uuid): UriPath<String>,
611    headers: HeaderMap,
612    body: Body,
613) -> Response {
614    let (admitted, uuid) = match legal_admit(&app, &headers, body, Action::Restore, &uuid).await {
615        Ok(admitted) => admitted,
616        Err(refused) => return refused.into_response(),
617    };
618    let (store, key) = (admitted.store.clone(), admitted.caller.key);
619    match blocking(move || store.restore(&StoreCaller { key }, uuid)).await {
620        Ok(version) => (StatusCode::OK, [(ETAG, entity_tag(version))]).into_response(),
621        Err(error) => failed(error).into_response(),
622    }
623}
624
625/// `PUT /legal/vcons/{uuid}/hold`: puts the uuid under a legal hold.
626async fn hold_on(
627    State(app): State<Arc<App>>,
628    UriPath(uuid): UriPath<String>,
629    headers: HeaderMap,
630    body: Body,
631) -> Response {
632    hold(app, uuid, headers, body, true).await
633}
634
635/// `DELETE /legal/vcons/{uuid}/hold`: releases the uuid's legal hold.
636async fn hold_off(
637    State(app): State<Arc<App>>,
638    UriPath(uuid): UriPath<String>,
639    headers: HeaderMap,
640    body: Body,
641) -> Response {
642    hold(app, uuid, headers, body, false).await
643}
644
645async fn hold(app: Arc<App>, uuid: String, headers: HeaderMap, body: Body, on: bool) -> Response {
646    // A release is its own action, so the log tells it from a hold.
647    let action = if on { Action::Hold } else { Action::Release };
648    let (admitted, uuid) = match legal_admit(&app, &headers, body, action, &uuid).await {
649        Ok(admitted) => admitted,
650        Err(refused) => return refused.into_response(),
651    };
652    let (store, key) = (admitted.store.clone(), admitted.caller.key);
653    match blocking(move || store.set_hold(&StoreCaller { key }, uuid, on)).await {
654        Ok(()) => StatusCode::NO_CONTENT.into_response(),
655        Err(error) => failed(error).into_response(),
656    }
657}
658
659/// The admin routes' shared start: an operator key holding the admin
660/// scope, then the body within the limits, each refusal recorded as
661/// `action`.
662async fn admin_admit(
663    app: &Arc<App>,
664    headers: &HeaderMap,
665    body: Body,
666    action: Action,
667) -> Result<(KeyId, Bytes), Problem> {
668    let caller = authenticated(app, headers).await?;
669    let entry = |refused| refusal(caller.key, action, refused);
670    if access::require(&caller, Scope::Admin).is_err() {
671        let refused = Problem::Forbidden;
672        return Err(app.refuse(caller.tenant, entry(refused), refused).await);
673    }
674    match read_body(body, headers, &app.limits).await {
675        Ok(body) => Ok((caller.key, body)),
676        Err(refused) => Err(app.refuse(None, entry(refused), refused).await),
677    }
678}
679
680/// An admin body as JSON, or the refusal recorded as `action`.
681async fn admin_body<T: DeserializeOwned>(
682    app: &Arc<App>,
683    key: KeyId,
684    action: Action,
685    body: &Bytes,
686) -> Result<T, Problem> {
687    match serde_json::from_slice(body) {
688        Ok(read) => Ok(read),
689        Err(_) => {
690            let refused = Problem::InvalidBody;
691            Err(app
692                .refuse(None, refusal(key, action, refused), refused)
693                .await)
694        }
695    }
696}
697
698/// A retention period in a body: a string `Retention` reads, or null for
699/// the server default.
700fn period<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Retention>, D::Error> {
701    Option::<String>::deserialize(d)?
702        .map(|text| Retention::parse(&text).ok_or_else(|| D::Error::custom("a bad period")))
703        .transpose()
704}
705
706/// The body of `POST /admin/tenants`.
707#[derive(serde::Deserialize)]
708#[serde(deny_unknown_fields)]
709struct NewTenant {
710    name: String,
711    #[serde(default, deserialize_with = "period")]
712    retention: Option<Retention>,
713}
714
715/// The body of `PATCH /admin/tenants/{id}`. The period must be there,
716/// null included, so an empty body changes nothing by mistake.
717#[derive(serde::Deserialize)]
718#[serde(deny_unknown_fields)]
719struct NewRetention {
720    #[serde(deserialize_with = "period")]
721    retention: Option<Retention>,
722}
723
724/// A JSON answer.
725fn json_answer(status: StatusCode, body: &serde_json::Value) -> Response {
726    (
727        status,
728        [(CONTENT_TYPE, "application/json")],
729        body.to_string(),
730    )
731        .into_response()
732}
733
734/// `POST /admin/tenants`: makes a tenant and answers 201 with its id.
735async fn tenant_create(State(app): State<Arc<App>>, headers: HeaderMap, body: Body) -> Response {
736    let action = Action::TenantCreate;
737    let made = async {
738        let (key, body) = admin_admit(&app, &headers, body, action).await?;
739        let new: NewTenant = admin_body(&app, key, action, &body).await?;
740        let tenants = app.tenants.clone();
741        // Tenants records its own outcomes.
742        blocking(move || tenants.create(Actor::Key(key), &new.name, new.retention))
743            .await
744            .map_err(Problem::Tenant)
745    };
746    match made.await {
747        Ok(id) => json_answer(
748            StatusCode::CREATED,
749            &serde_json::json!({ "id": id.to_string() }),
750        ),
751        Err(refused) => refused.into_response(),
752    }
753}
754
755/// `GET /admin/tenants`: every tenant by name, with its id and its own
756/// retention period, null for the server default.
757async fn tenant_list(State(app): State<Arc<App>>, headers: HeaderMap, body: Body) -> Response {
758    let listed = async {
759        let (key, _) = admin_admit(&app, &headers, body, Action::TenantList).await?;
760        let tenants = app.tenants.clone();
761        blocking(move || tenants.list(Actor::Key(key)))
762            .await
763            .map_err(Problem::Tenant)
764    };
765    match listed.await {
766        Ok(listed) => {
767            let listed: Vec<serde_json::Value> = listed.iter().map(TenantInfo::to_json).collect();
768            json_answer(StatusCode::OK, &serde_json::Value::Array(listed))
769        }
770        Err(refused) => refused.into_response(),
771    }
772}
773
774/// `PATCH /admin/tenants/{id}`: sets the tenant's retention period, or
775/// with null returns it to the server default, and answers 200 with it.
776async fn tenant_retention(
777    State(app): State<Arc<App>>,
778    UriPath(id): UriPath<String>,
779    headers: HeaderMap,
780    body: Body,
781) -> Response {
782    let action = Action::TenantRetention;
783    let changed = async {
784        let (key, body) = admin_admit(&app, &headers, body, action).await?;
785        let Some(id) = TenantId::parse(&id) else {
786            let refused = Problem::Tenant(TenantError::NotFound);
787            return Err(app
788                .refuse(None, refusal(key, action, refused), refused)
789                .await);
790        };
791        let new: NewRetention = admin_body(&app, key, action, &body).await?;
792        let tenants = app.tenants.clone();
793        blocking(move || tenants.set_retention(Actor::Key(key), id, new.retention))
794            .await
795            .map_err(Problem::Tenant)?;
796        Ok((id, new.retention))
797    };
798    match changed.await {
799        Ok((id, retention)) => json_answer(
800            StatusCode::OK,
801            &serde_json::json!({
802                "id": id.to_string(),
803                "retention": retention.map(|r| r.to_string()),
804            }),
805        ),
806        Err(refused) => refused.into_response(),
807    }
808}
809
810/// `DELETE /admin/tenants/{id}`: removes a tenant that keeps no version,
811/// or with `?force=true` one that does, unless a uuid of it is under a
812/// legal hold; answers 204.
813async fn tenant_remove(
814    State(app): State<Arc<App>>,
815    UriPath(id): UriPath<String>,
816    uri: Uri,
817    headers: HeaderMap,
818    body: Body,
819) -> Response {
820    let action = Action::TenantRemove;
821    let removed = async {
822        let (key, _) = admin_admit(&app, &headers, body, action).await?;
823        let refuse = |refused| app.refuse(None, refusal(key, action, refused), refused);
824        let Some(id) = TenantId::parse(&id) else {
825            return Err(refuse(Problem::Tenant(TenantError::NotFound)).await);
826        };
827        let Some(removal) = removal(uri.query()) else {
828            return Err(refuse(Problem::BadQuery).await);
829        };
830        let tenants = app.tenants.clone();
831        // Tenants records its own outcomes.
832        blocking(move || tenants.remove(Actor::Key(key), id, removal))
833            .await
834            .map_err(Problem::Tenant)
835    };
836    match removed.await {
837        Ok(()) => StatusCode::NO_CONTENT.into_response(),
838        Err(refused) => refused.into_response(),
839    }
840}
841
842/// How far a removal asked over HTTP may go: with no query or
843/// `force=false` only an empty tenant, with `force=true` its vCons too.
844/// No query overrides a legal hold; that is the command line's alone.
845fn removal(query: Option<&str>) -> Option<Removal> {
846    match query {
847        None | Some("force=false") => Some(Removal::Empty),
848        Some("force=true") => Some(Removal::Force),
849        Some(_) => None,
850    }
851}
852
853/// The body of `POST /admin/tenants/{id}/keys`.
854#[derive(serde::Deserialize)]
855#[serde(deny_unknown_fields)]
856struct NewKeyBody {
857    scopes: Vec<String>,
858    label: String,
859}
860
861/// `POST /admin/tenants/{id}/keys`: makes a key for the tenant and
862/// answers 201 with its id and token. The token is shown this once; only
863/// its hash is kept.
864async fn key_create(
865    State(app): State<Arc<App>>,
866    UriPath(id): UriPath<String>,
867    headers: HeaderMap,
868    body: Body,
869) -> Response {
870    let action = Action::KeyCreate;
871    let made = async {
872        let (key, body) = admin_admit(&app, &headers, body, action).await?;
873        let refuse = |refused| app.refuse(None, refusal(key, action, refused), refused);
874        let Some(id) = TenantId::parse(&id) else {
875            return Err(refuse(Problem::Tenant(TenantError::NotFound)).await);
876        };
877        let new: NewKeyBody = admin_body(&app, key, action, &body).await?;
878        let Some(scopes) = Scopes::named(&new.scopes) else {
879            return Err(refuse(Problem::Tenant(TenantError::BadScopes)).await);
880        };
881        let tenants = app.tenants.clone();
882        // Tenants records its own outcomes.
883        blocking(move || tenants.create_key(Actor::Key(key), Some(id), scopes, &new.label))
884            .await
885            .map_err(Problem::Tenant)
886    };
887    match made.await {
888        Ok(made) => {
889            let body = serde_json::json!({ "id": made.id.to_string(), "token": made.token });
890            let mut answer = json_answer(StatusCode::CREATED, &body);
891            answer
892                .headers_mut()
893                .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
894            answer
895        }
896        Err(refused) => refused.into_response(),
897    }
898}
899
900/// `GET /admin/tenants/{id}/keys`: the tenant's keys, oldest first, with
901/// their ids, scopes, labels and times (RFC 3339, UTC), never a token.
902async fn key_list(
903    State(app): State<Arc<App>>,
904    UriPath(id): UriPath<String>,
905    headers: HeaderMap,
906    body: Body,
907) -> Response {
908    let action = Action::KeyList;
909    let listed = async {
910        let (key, _) = admin_admit(&app, &headers, body, action).await?;
911        let Some(id) = TenantId::parse(&id) else {
912            let refused = Problem::Tenant(TenantError::NotFound);
913            return Err(app
914                .refuse(None, refusal(key, action, refused), refused)
915                .await);
916        };
917        let tenants = app.tenants.clone();
918        blocking(move || tenants.list_keys(Actor::Key(key), Some(id)))
919            .await
920            .map_err(Problem::Tenant)
921    };
922    let keys = match listed.await {
923        Ok(keys) => keys,
924        Err(refused) => return refused.into_response(),
925    };
926    let listed: Option<Vec<serde_json::Value>> = keys.iter().map(KeyInfo::to_json).collect();
927    match listed {
928        Some(listed) => json_answer(StatusCode::OK, &serde_json::Value::Array(listed)),
929        None => Problem::Tenant(TenantError::Io).into_response(),
930    }
931}
932
933/// `DELETE /admin/keys/{id}`: revokes the key, so its next request is
934/// refused; 204 again for a key already revoked.
935async fn key_revoke(
936    State(app): State<Arc<App>>,
937    UriPath(id): UriPath<String>,
938    headers: HeaderMap,
939    body: Body,
940) -> Response {
941    let action = Action::KeyRevoke;
942    let revoked = async {
943        let (key, _) = admin_admit(&app, &headers, body, action).await?;
944        let Some(id) = KeyId::parse(&id) else {
945            let refused = Problem::UnknownKey;
946            return Err(app
947                .refuse(None, refusal(key, action, refused), refused)
948                .await);
949        };
950        let tenants = app.tenants.clone();
951        // Tenants records its own outcomes.
952        blocking(move || tenants.revoke_key(Actor::Key(key), id))
953            .await
954            .map_err(|error| match error {
955                TenantError::NotFound => Problem::UnknownKey,
956                error => Problem::Tenant(error),
957            })
958    };
959    match revoked.await {
960        Ok(()) => StatusCode::NO_CONTENT.into_response(),
961        Err(refused) => refused.into_response(),
962    }
963}
964
965/// `GET /health`: any valid key may ask; 200 when server.sqlite answers,
966/// and 503 for a key whose tenant is being removed.
967///
968/// Health requests are counted, one record per key, status and minute, in
969/// the key's tenant log, or the admin log for an operator key or a tenant
970/// whose log cannot be opened; a health request that cannot be counted is
971/// not healthy.
972async fn health(State(app): State<Arc<App>>, headers: HeaderMap) -> Response {
973    let caller = match authenticated(&app, &headers).await {
974        Ok(caller) => caller,
975        Err(refused) => return refused.into_response(),
976    };
977    let tenants = app.tenants.clone();
978    let (healthy, counted) = blocking(move || {
979        // A tenant being removed answers 503 here too, as on every route.
980        let store = caller.tenant.map(|t| tenants.store(t));
981        let closing = matches!(store, Some(Err(TenantError::Closing)));
982        let healthy = !closing && tenants.check().is_ok();
983        let entry = Entry {
984            actor: Actor::Key(caller.key),
985            action: Action::Health,
986            target: None,
987            version: None,
988            status: if healthy { 200 } else { 503 },
989        };
990        let counted = match store {
991            Some(Ok(store)) => store.count(&entry).is_ok(),
992            _ => tenants.count_admin(&entry).is_ok(),
993        };
994        (healthy, counted)
995    })
996    .await;
997    match (healthy, counted) {
998        (_, false) => Problem::AuditFailed.into_response(),
999        (false, true) => Problem::Unavailable.into_response(),
1000        (true, true) => (
1001            StatusCode::OK,
1002            [(CONTENT_TYPE, "application/json")],
1003            r#"{"status":"ok"}"#,
1004        )
1005            .into_response(),
1006    }
1007}
1008
1009/// `GET /key`: any valid key reads what it may do: its own id, its tenant
1010/// (null for an operator key) and its scopes, so a client can tell which
1011/// requests it may make.
1012///
1013/// The read is recorded before the answer, in the key's tenant log, or the
1014/// admin log for an operator key. A key whose tenant is being removed, or
1015/// whose tenant's log cannot be opened, is refused with 503, recorded in
1016/// the admin log.
1017async fn key_info(State(app): State<Arc<App>>, headers: HeaderMap) -> Response {
1018    let caller = match authenticated(&app, &headers).await {
1019        Ok(caller) => caller,
1020        Err(refused) => return refused.into_response(),
1021    };
1022    let entry = |status| Entry {
1023        actor: Actor::Key(caller.key),
1024        action: Action::KeyInfo,
1025        target: Some(caller.key.to_string()),
1026        version: None,
1027        status,
1028    };
1029    let store = match caller.tenant {
1030        Some(t) => {
1031            let tenants = app.tenants.clone();
1032            match blocking(move || tenants.store(t)).await {
1033                Ok(store) => Some(store),
1034                Err(_) => {
1035                    let refused = Problem::Unavailable;
1036                    let entry = entry(refused.status().as_u16());
1037                    return app.refuse(None, entry, refused).await.into_response();
1038                }
1039            }
1040        }
1041        None => None,
1042    };
1043    let tenants = app.tenants.clone();
1044    let record = entry(200);
1045    let recorded = blocking(move || match store {
1046        Some(store) => store.log(&record).is_ok(),
1047        None => tenants.record_alone(&record).is_ok(),
1048    })
1049    .await;
1050    if !recorded {
1051        return Problem::AuditFailed.into_response();
1052    }
1053    let body = serde_json::json!({
1054        "id": caller.key.to_string(),
1055        "tenant": caller.tenant.map(|t| t.to_string()),
1056        "scopes": caller.scopes.names(),
1057    });
1058    let mut answer = json_answer(StatusCode::OK, &body);
1059    answer
1060        .headers_mut()
1061        .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
1062    answer
1063}
1064
1065/// The audit record of a request refused before Store saw it.
1066fn refusal(key: KeyId, action: Action, refused: Problem) -> Entry {
1067    Entry {
1068        actor: Actor::Key(key),
1069        action,
1070        target: None,
1071        version: None,
1072        status: refused.status().as_u16(),
1073    }
1074}
1075
1076/// A version id as a strong entity tag.
1077fn entity_tag(version: VersionId) -> String {
1078    format!("\"{version}\"")
1079}
1080
1081/// Whether a request's media type is one a vCon may be sent as:
1082/// `application/vcon`, `application/vcon+gzip` or `application/json`, in
1083/// any case and with any parameters.
1084fn is_vcon_media_type(content_type: &str) -> bool {
1085    let essence = content_type.split(';').next().unwrap_or("").trim();
1086    [
1087        "application/vcon",
1088        "application/vcon+gzip",
1089        "application/json",
1090    ]
1091    .iter()
1092    .any(|taken| essence.eq_ignore_ascii_case(taken))
1093}
1094
1095/// A request the pipeline let through, with its body read.
1096struct Admitted {
1097    caller: Caller,
1098    tenant: TenantId,
1099    store: Arc<Store>,
1100    body: Bytes,
1101}
1102
1103/// The steps every vCon request takes before its route's own work, in
1104/// order: authenticate, check the scope, open the caller's tenant, read
1105/// the body within the limits. A refusal at any step is recorded here,
1106/// since Store never sees the request.
1107async fn admit(
1108    app: &Arc<App>,
1109    headers: &HeaderMap,
1110    body: Body,
1111    scope: Scope,
1112    action: Action,
1113) -> Result<Admitted, Problem> {
1114    let caller = authenticated(app, headers).await?;
1115    let entry = |refused| refusal(caller.key, action, refused);
1116    let Ok(tenant) = access::tenant_for(&caller, scope) else {
1117        let refused = Problem::Forbidden;
1118        return Err(app.refuse(caller.tenant, entry(refused), refused).await);
1119    };
1120    let tenants = app.tenants.clone();
1121    let Ok(store) = blocking(move || tenants.store(tenant)).await else {
1122        let refused = Problem::Unavailable;
1123        return Err(app.refuse(None, entry(refused), refused).await);
1124    };
1125    match read_body(body, headers, &app.limits).await {
1126        Ok(body) => Ok(Admitted {
1127            caller,
1128            tenant,
1129            store,
1130            body,
1131        }),
1132        Err(refused) => Err(app.refuse(Some(tenant), entry(refused), refused).await),
1133    }
1134}
1135
1136/// The caller a request's bearer key names: 401 for a missing or bad
1137/// key, and 503 when the key table cannot be read.
1138async fn authenticated(app: &Arc<App>, headers: &HeaderMap) -> Result<Caller, Problem> {
1139    let authorization = headers
1140        .get(AUTHORIZATION)
1141        .map(|v| v.to_str().unwrap_or_default().to_owned());
1142    let tenants = app.tenants.clone();
1143    blocking(move || access::authenticate(&tenants, authorization.as_deref()))
1144        .await
1145        .map_err(|e| match e {
1146            AuthError::Io => Problem::Unavailable,
1147            _ => Problem::Unauthorized,
1148        })
1149}
1150
1151impl App {
1152    /// Records a refusal in the tenant's log, or the admin log when there
1153    /// is no tenant or its log cannot be opened, and answers it; a refusal
1154    /// that cannot be recorded answers that the audit failed.
1155    async fn refuse(&self, tenant: Option<TenantId>, entry: Entry, refused: Problem) -> Problem {
1156        let tenants = self.tenants.clone();
1157        let recorded = blocking(move || match tenant.and_then(|t| tenants.store(t).ok()) {
1158            Some(store) => store.log(&entry).is_ok(),
1159            None => tenants.record_alone(&entry).is_ok(),
1160        })
1161        .await;
1162        if recorded {
1163            refused
1164        } else {
1165            Problem::AuditFailed
1166        }
1167    }
1168}
1169
1170/// Reads a body within the size limit and the time limit. A declared
1171/// length over the limit is refused before anything is read, and a body
1172/// that passes it is refused as soon as it does.
1173async fn read_body(body: Body, headers: &HeaderMap, limits: &Limits) -> Result<Bytes, Problem> {
1174    let declared = headers
1175        .get(CONTENT_LENGTH)
1176        .and_then(|v| v.to_str().ok())
1177        .and_then(|v| v.parse::<u64>().ok());
1178    if declared.is_some_and(|length| length > limits.body as u64) {
1179        return Err(Problem::TooLarge);
1180    }
1181    match tokio::time::timeout(limits.body_timeout, axum::body::to_bytes(body, limits.body)).await {
1182        Err(_) => Err(Problem::Timeout),
1183        Ok(Ok(body)) => Ok(body),
1184        Ok(Err(e)) if std::error::Error::source(&e).is_some_and(|s| s.is::<LengthLimitError>()) => {
1185            Err(Problem::TooLarge)
1186        }
1187        Ok(Err(_)) => Err(Problem::BadBody),
1188    }
1189}
1190
1191/// Runs blocking work, SQLite and files, off the async workers.
1192async fn blocking<T: Send + 'static>(work: impl FnOnce() -> T + Send + 'static) -> T {
1193    tokio::task::spawn_blocking(work)
1194        .await
1195        .expect("blocking work does not panic")
1196}
1197
1198/// An error answer: an RFC 9457 problem document with one type per error.
1199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1200enum Problem {
1201    Unauthorized,
1202    Forbidden,
1203    TooLarge,
1204    Timeout,
1205    BadBody,
1206    Unavailable,
1207    AuditFailed,
1208    UnsupportedMediaType,
1209    /// A uuid in the path is not in RFC 9562 text form.
1210    BadPathUuid,
1211    /// A version in the path is not a version id.
1212    BadPathVersion,
1213    /// A replacement came without `If-Match`.
1214    PreconditionRequired,
1215    /// The body's uuid is not the one in the path.
1216    PathUuidMismatch,
1217    /// Format refused the body.
1218    Invalid(Invalid),
1219    /// An admin body is not one the route takes.
1220    InvalidBody,
1221    /// The query is not one the route takes.
1222    BadQuery,
1223    /// No key has the id.
1224    UnknownKey,
1225    /// Store refused or failed the request.
1226    Store(StoreError),
1227    /// Tenants refused or failed the request.
1228    Tenant(TenantError),
1229}
1230
1231/// Why Format refused a body, one problem type each.
1232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1233enum Invalid {
1234    BadGzip,
1235    NotJson,
1236    DuplicateMember,
1237    AmbiguousForm,
1238    NoForm,
1239    MissingUuid,
1240    BadUuid,
1241    MissingCreatedAt,
1242    BadPayload,
1243    UuidMismatch,
1244    BadCritical,
1245    UnsupportedExtension,
1246    TooDeep,
1247    /// A refusal from the vcon crate that this server does not name.
1248    Other,
1249}
1250
1251/// The problem for Format's refusal. Inflating past the limit is a body
1252/// too large; the others are unprocessable. No problem repeats the body,
1253/// so unsupported extensions are not named.
1254fn rejected(rejection: &Rejection) -> Problem {
1255    Problem::Invalid(match rejection {
1256        Rejection::TooLarge => return Problem::TooLarge,
1257        Rejection::BadGzip => Invalid::BadGzip,
1258        Rejection::NotJson => Invalid::NotJson,
1259        Rejection::DuplicateKey => Invalid::DuplicateMember,
1260        Rejection::AmbiguousForm => Invalid::AmbiguousForm,
1261        Rejection::NoForm => Invalid::NoForm,
1262        Rejection::MissingUuid => Invalid::MissingUuid,
1263        Rejection::BadUuid => Invalid::BadUuid,
1264        Rejection::MissingCreatedAt => Invalid::MissingCreatedAt,
1265        Rejection::BadPayload => Invalid::BadPayload,
1266        Rejection::UuidMismatch => Invalid::UuidMismatch,
1267        Rejection::BadCritical => Invalid::BadCritical,
1268        Rejection::UnsupportedCritical(_) => Invalid::UnsupportedExtension,
1269        Rejection::TooDeep => Invalid::TooDeep,
1270        // A refusal a later vcon release adds, before this server names it.
1271        // Every refusal there is today is named above, so no test reaches
1272        // this arm; the problem it answers with is tested on its own.
1273        _ => Invalid::Other,
1274    })
1275}
1276
1277/// The problem for Store's refusal or failure.
1278fn failed(error: StoreError) -> Problem {
1279    Problem::Store(error)
1280}
1281
1282impl Problem {
1283    fn status(self) -> StatusCode {
1284        match self {
1285            Problem::Unauthorized => StatusCode::UNAUTHORIZED,
1286            Problem::Forbidden => StatusCode::FORBIDDEN,
1287            Problem::UnknownKey => StatusCode::NOT_FOUND,
1288            Problem::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
1289            Problem::Timeout => StatusCode::REQUEST_TIMEOUT,
1290            Problem::BadBody
1291            | Problem::BadPathUuid
1292            | Problem::BadPathVersion
1293            | Problem::BadQuery => StatusCode::BAD_REQUEST,
1294            Problem::PreconditionRequired => StatusCode::PRECONDITION_REQUIRED,
1295            Problem::PathUuidMismatch => StatusCode::UNPROCESSABLE_ENTITY,
1296            Problem::Unavailable | Problem::AuditFailed => StatusCode::SERVICE_UNAVAILABLE,
1297            Problem::UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE,
1298            Problem::Invalid(_) | Problem::InvalidBody => StatusCode::UNPROCESSABLE_ENTITY,
1299            Problem::Store(error) => match error {
1300                StoreError::Conflict | StoreError::NotDeleted | StoreError::Immutable => {
1301                    StatusCode::CONFLICT
1302                }
1303                StoreError::Gone => StatusCode::GONE,
1304                StoreError::NotFound => StatusCode::NOT_FOUND,
1305                StoreError::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
1306                StoreError::LimitReached | StoreError::AuditFailed => {
1307                    StatusCode::SERVICE_UNAVAILABLE
1308                }
1309                StoreError::Damaged | StoreError::Io => StatusCode::INTERNAL_SERVER_ERROR,
1310            },
1311            Problem::Tenant(error) => match error {
1312                TenantError::NotFound => StatusCode::NOT_FOUND,
1313                TenantError::NameTaken | TenantError::HoldsVcons | TenantError::UnderHold => {
1314                    StatusCode::CONFLICT
1315                }
1316                TenantError::BadName | TenantError::BadScopes => StatusCode::UNPROCESSABLE_ENTITY,
1317                TenantError::CliOnly => StatusCode::FORBIDDEN,
1318                TenantError::LimitReached | TenantError::AuditFailed | TenantError::Closing => {
1319                    StatusCode::SERVICE_UNAVAILABLE
1320                }
1321                TenantError::WrongKey | TenantError::Io => StatusCode::INTERNAL_SERVER_ERROR,
1322            },
1323        }
1324    }
1325
1326    /// The last part of its type URI, and its title.
1327    fn name(self) -> (&'static str, &'static str) {
1328        match self {
1329            Problem::Unauthorized => ("unauthorized", "A valid bearer key is needed"),
1330            Problem::Forbidden => ("forbidden", "The key does not allow this"),
1331            Problem::TooLarge => ("too-large", "The body is over the size limit"),
1332            Problem::Timeout => ("timeout", "The body did not arrive in time"),
1333            Problem::BadBody => ("bad-body", "The body could not be read"),
1334            Problem::BadPathUuid => (
1335                "bad-path-uuid",
1336                "The path's uuid is not in RFC 9562 text form",
1337            ),
1338            Problem::BadPathVersion => {
1339                ("bad-path-version", "The path's version is not a version id")
1340            }
1341            Problem::PreconditionRequired => (
1342                "precondition-required",
1343                "If-Match must name the current version",
1344            ),
1345            Problem::PathUuidMismatch => (
1346                "path-uuid-mismatch",
1347                "The body's uuid is not the one in the path",
1348            ),
1349            Problem::Unavailable => ("unavailable", "The service cannot answer now"),
1350            Problem::AuditFailed => ("audit-failed", "The audit record could not be written"),
1351            Problem::UnsupportedMediaType => {
1352                ("unsupported-media-type", "This media type is not taken")
1353            }
1354            Problem::Invalid(invalid) => match invalid {
1355                Invalid::BadGzip => ("bad-gzip", "The body is marked gzip but is not"),
1356                Invalid::NotJson => ("not-json", "The body is not one JSON object"),
1357                Invalid::DuplicateMember => ("duplicate-member", "An object repeats a member"),
1358                Invalid::AmbiguousForm => {
1359                    ("ambiguous-form", "The body is both signed and encrypted")
1360                }
1361                Invalid::NoForm => ("no-form", "The body is in no vCon form"),
1362                Invalid::MissingUuid => ("missing-uuid", "The vCon has no uuid"),
1363                Invalid::BadUuid => ("bad-uuid", "The uuid is not in RFC 9562 text form"),
1364                Invalid::MissingCreatedAt => ("missing-created-at", "The vCon has no created_at"),
1365                Invalid::BadPayload => ("bad-payload", "The signed payload cannot be read"),
1366                Invalid::UuidMismatch => ("uuid-mismatch", "A signature names another uuid"),
1367                Invalid::BadCritical => (
1368                    "bad-critical",
1369                    "critical or must_support is not a list of names",
1370                ),
1371                Invalid::UnsupportedExtension => (
1372                    "unsupported-extension",
1373                    "An extension the vCon needs is not supported",
1374                ),
1375                Invalid::TooDeep => ("too-deep", "The body nests too deeply"),
1376                Invalid::Other => ("not-a-vcon", "The body is not a vCon this server can read"),
1377            },
1378            Problem::Store(error) => match error {
1379                StoreError::Conflict => ("conflict", "Another vCon holds the uuid"),
1380                StoreError::Gone => ("gone", "The vCon was deleted or purged"),
1381                StoreError::NotFound => ("not-found", "No vCon has the uuid"),
1382                StoreError::NotDeleted => ("not-deleted", "The vCon is not deleted"),
1383                StoreError::PreconditionFailed => (
1384                    "precondition-failed",
1385                    "If-Match does not name the current version",
1386                ),
1387                StoreError::Immutable => {
1388                    ("immutable", "A signed or encrypted vCon is not replaced")
1389                }
1390                StoreError::LimitReached => ("limit-reached", "The tenant key must be rotated"),
1391                StoreError::Damaged => ("damaged", "A stored version is damaged"),
1392                StoreError::AuditFailed => {
1393                    ("audit-failed", "The audit record could not be written")
1394                }
1395                StoreError::Io => ("internal", "The server failed"),
1396            },
1397            Problem::BadQuery => ("bad-query", "The query is not one this route takes"),
1398            Problem::UnknownKey => ("unknown-key", "No key has the id"),
1399            Problem::InvalidBody | Problem::Tenant(TenantError::BadName) => {
1400                ("invalid-body", "The body is not one this route takes")
1401            }
1402            Problem::Tenant(error) => match error {
1403                TenantError::NotFound => ("unknown-tenant", "No tenant has the id"),
1404                TenantError::NameTaken => ("name-taken", "Another tenant has the name"),
1405                TenantError::HoldsVcons => (
1406                    "holds-vcons",
1407                    "The tenant holds vCons; removing them needs force",
1408                ),
1409                TenantError::UnderHold => {
1410                    ("under-hold", "A vCon of the tenant is under a legal hold")
1411                }
1412                TenantError::BadScopes => ("bad-scopes", "A tenant key cannot hold these scopes"),
1413                TenantError::CliOnly => ("forbidden", "The key does not allow this"),
1414                TenantError::LimitReached => ("master-key-limit", "The master key must be rotated"),
1415                TenantError::AuditFailed => {
1416                    ("audit-failed", "The audit record could not be written")
1417                }
1418                TenantError::Closing => ("unavailable", "The service cannot answer now"),
1419                TenantError::BadName | TenantError::WrongKey | TenantError::Io => {
1420                    ("internal", "The server failed")
1421                }
1422            },
1423        }
1424    }
1425}
1426
1427impl IntoResponse for Problem {
1428    fn into_response(self) -> Response {
1429        let (name, title) = self.name();
1430        let status = self.status();
1431        let body = serde_json::json!({
1432            "type": format!("https://vcon-server.com/problems/{name}"),
1433            "title": title,
1434            "status": status.as_u16(),
1435        });
1436        let mut answer = (
1437            status,
1438            [(CONTENT_TYPE, "application/problem+json")],
1439            body.to_string(),
1440        )
1441            .into_response();
1442        if self == Problem::Unauthorized {
1443            answer
1444                .headers_mut()
1445                .insert(WWW_AUTHENTICATE, HeaderValue::from_static("Bearer"));
1446        }
1447        answer
1448    }
1449}
1450
1451/// Why the TLS configuration could not be made.
1452#[derive(Debug)]
1453pub enum TlsError {
1454    /// The certificate file could not be read, or holds no certificate.
1455    Certificate,
1456    /// The key file could not be read, or holds no key.
1457    Key,
1458    /// rustls refused the pair, for instance a key that is not the
1459    /// certificate's.
1460    Refused(rustls::Error),
1461}
1462
1463/// The server's TLS configuration from a PEM certificate chain and a PEM
1464/// private key.
1465pub fn tls_config(certificate: &Path, key: &Path) -> Result<Arc<ServerConfig>, TlsError> {
1466    let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(certificate)
1467        .map_err(|_| TlsError::Certificate)?
1468        .collect::<Result<_, _>>()
1469        .map_err(|_| TlsError::Certificate)?;
1470    if chain.is_empty() {
1471        return Err(TlsError::Certificate);
1472    }
1473    let key = PrivateKeyDer::from_pem_file(key).map_err(|_| TlsError::Key)?;
1474    let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
1475    let mut config = ServerConfig::builder_with_provider(provider)
1476        .with_safe_default_protocol_versions()
1477        .map_err(TlsError::Refused)?
1478        .with_no_client_auth()
1479        .with_single_cert(chain, key)
1480        .map_err(TlsError::Refused)?;
1481    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1482    Ok(Arc::new(config))
1483}
1484
1485/// Serves `app` on `listener` over TLS until `shutdown` completes. A
1486/// connection whose handshake fails, such as plain HTTP, is dropped; the
1487/// server goes on.
1488pub async fn serve(
1489    listener: TcpListener,
1490    tls: Arc<ServerConfig>,
1491    app: Router,
1492    shutdown: impl Future<Output = ()>,
1493) -> io::Result<()> {
1494    let acceptor = TlsAcceptor::from(tls);
1495    let mut shutdown = std::pin::pin!(shutdown);
1496    loop {
1497        let (tcp, _) = tokio::select! {
1498            accepted = listener.accept() => accepted?,
1499            () = &mut shutdown => return Ok(()),
1500        };
1501        let acceptor = acceptor.clone();
1502        let service = TowerToHyperService::new(app.clone());
1503        tokio::spawn(async move {
1504            let Ok(stream) = acceptor.accept(tcp).await else {
1505                return;
1506            };
1507            // The connection's own errors end only the connection.
1508            let _ = auto::Builder::new(TokioExecutor::new())
1509                .serve_connection(TokioIo::new(stream), service)
1510                .await;
1511        });
1512    }
1513}
1514
1515#[cfg(test)]
1516mod tests {
1517    use std::net::SocketAddr;
1518    use std::path::PathBuf;
1519
1520    use axum::routing::get;
1521
1522    use super::*;
1523
1524    /// A self-signed certificate for localhost and its key, as PEM files.
1525    struct Certificate {
1526        _dir: tempfile::TempDir,
1527        cert: PathBuf,
1528        key: PathBuf,
1529        pem: String,
1530    }
1531
1532    fn certificate() -> Certificate {
1533        let dir = tempfile::tempdir().expect("temp dir");
1534        let made = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()])
1535            .expect("a certificate");
1536        let pem = made.cert.pem();
1537        let (cert, key) = (dir.path().join("cert.pem"), dir.path().join("key.pem"));
1538        std::fs::write(&cert, &pem).expect("write");
1539        std::fs::write(&key, made.signing_key.serialize_pem()).expect("write");
1540        Certificate {
1541            _dir: dir,
1542            cert,
1543            key,
1544            pem,
1545        }
1546    }
1547
1548    fn client(c: &Certificate, addr: SocketAddr) -> reqwest::Client {
1549        reqwest::Client::builder()
1550            .tls_certs_only([reqwest::Certificate::from_pem(c.pem.as_bytes()).expect("pem")])
1551            .resolve("localhost", addr)
1552            .no_proxy()
1553            .build()
1554            .expect("a client")
1555    }
1556
1557    #[tokio::test]
1558    async fn the_listener_speaks_tls_only() {
1559        let c = certificate();
1560        let tls = tls_config(&c.cert, &c.key).expect("loads");
1561        let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
1562        let addr = listener.local_addr().expect("addr");
1563        let app = Router::new().route("/ping", get(|| async { "pong" }));
1564        let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
1565        let server = tokio::spawn(serve(listener, tls, app, async {
1566            stopped.await.ok();
1567        }));
1568        let plain = reqwest::Client::builder()
1569            .no_proxy()
1570            .build()
1571            .expect("a client")
1572            .get(format!("http://{addr}/ping"))
1573            .send()
1574            .await;
1575        assert!(plain.is_err(), "plain HTTP is not served: {plain:?}");
1576        let answer = client(&c, addr)
1577            .get(format!("https://localhost:{}/ping", addr.port()))
1578            .send()
1579            .await
1580            .expect("TLS is served, after a plain-HTTP attempt too");
1581        assert_eq!(answer.status(), 200);
1582        assert_eq!(answer.version(), reqwest::Version::HTTP_2, "h2 by ALPN");
1583        assert_eq!(answer.text().await.expect("body"), "pong");
1584        stop.send(()).expect("stop");
1585        server.await.expect("joins").expect("serves");
1586    }
1587
1588    #[test]
1589    fn a_missing_or_unusable_certificate_or_key_is_refused() {
1590        let c = certificate();
1591        let other = certificate();
1592        let dir = tempfile::tempdir().expect("temp dir");
1593        let (missing, empty) = (dir.path().join("missing"), dir.path().join("empty"));
1594        std::fs::write(&empty, "").expect("write");
1595        assert!(matches!(
1596            tls_config(&missing, &c.key),
1597            Err(TlsError::Certificate)
1598        ));
1599        assert!(matches!(
1600            tls_config(&empty, &c.key),
1601            Err(TlsError::Certificate)
1602        ));
1603        assert!(matches!(tls_config(&c.cert, &missing), Err(TlsError::Key)));
1604        assert!(matches!(tls_config(&c.cert, &empty), Err(TlsError::Key)));
1605        assert!(matches!(
1606            tls_config(&c.cert, &other.key),
1607            Err(TlsError::Refused(_))
1608        ));
1609        assert!(tls_config(&c.cert, &c.key).is_ok());
1610    }
1611
1612    pub(crate) mod pipeline {
1613        use std::net::SocketAddr;
1614
1615        use serde_json::Value;
1616        use tokio::io::{AsyncReadExt, AsyncWriteExt};
1617
1618        use super::*;
1619        use crate::access::{Scope, Scopes};
1620        use crate::audit::Actor;
1621        use crate::clock::SystemClock;
1622        use crate::envelope::Key;
1623        use crate::fs::OsFs;
1624        use crate::ids::TenantId;
1625
1626        pub(crate) const LIMIT: usize = 64 * 1024;
1627
1628        /// A server with one tenant, on a free port.
1629        pub(crate) struct Server {
1630            _dir: tempfile::TempDir,
1631            certificate: Certificate,
1632            addr: SocketAddr,
1633            pub(crate) tenants: Arc<Tenants>,
1634            pub(crate) tenant: TenantId,
1635            stop: Option<tokio::sync::oneshot::Sender<()>>,
1636        }
1637
1638        impl Drop for Server {
1639            fn drop(&mut self) {
1640                if let Some(stop) = self.stop.take() {
1641                    let _ = stop.send(());
1642                }
1643            }
1644        }
1645
1646        pub(crate) async fn server() -> Server {
1647            server_with(&[]).await
1648        }
1649
1650        /// A server that supports the must_support extensions named.
1651        pub(crate) async fn server_with(supported: &[&str]) -> Server {
1652            server_from(supported, Retention::Years(7)).await
1653        }
1654
1655        /// A server whose tenants that set no retention period keep
1656        /// vCons for `default`.
1657        pub(crate) async fn server_keeping(default: Retention) -> Server {
1658            server_from(&[], default).await
1659        }
1660
1661        async fn server_from(supported: &[&str], retention_default: Retention) -> Server {
1662            let dir = tempfile::tempdir().expect("temp dir");
1663            let tenants = Arc::new(
1664                Tenants::open(
1665                    dir.path(),
1666                    Key::generate().expect("master key"),
1667                    Arc::new(OsFs),
1668                    Arc::new(SystemClock),
1669                )
1670                .expect("opens"),
1671            );
1672            let tenant = tenants.create(Actor::Cli, "acme", None).expect("a tenant");
1673            let app = Arc::new(App {
1674                tenants: tenants.clone(),
1675                limits: Limits {
1676                    body: LIMIT,
1677                    inflated: 1 << 20,
1678                    body_timeout: Duration::from_millis(300),
1679                },
1680                supported: supported.iter().map(|s| (*s).to_owned()).collect(),
1681                retention_default,
1682            });
1683            let certificate = certificate();
1684            let tls = tls_config(&certificate.cert, &certificate.key).expect("tls");
1685            let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
1686            let addr = listener.local_addr().expect("addr");
1687            let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
1688            tokio::spawn(serve(listener, tls, router(app), async {
1689                stopped.await.ok();
1690            }));
1691            Server {
1692                _dir: dir,
1693                certificate,
1694                addr,
1695                tenants,
1696                tenant,
1697                stop: Some(stop),
1698            }
1699        }
1700
1701        impl Server {
1702            pub(crate) fn dir(&self) -> &std::path::Path {
1703                self._dir.path()
1704            }
1705
1706            pub(crate) fn url(&self, path: &str) -> String {
1707                format!("https://localhost:{}{path}", self.addr.port())
1708            }
1709
1710            pub(crate) fn client(&self) -> reqwest::Client {
1711                client(&self.certificate, self.addr)
1712            }
1713
1714            /// A client that shows a redirect instead of following it.
1715            pub(crate) fn client_not_following(&self) -> reqwest::Client {
1716                let pem = self.certificate.pem.as_bytes();
1717                reqwest::Client::builder()
1718                    .tls_certs_only([reqwest::Certificate::from_pem(pem).expect("pem")])
1719                    .resolve("localhost", self.addr)
1720                    .no_proxy()
1721                    .redirect(reqwest::redirect::Policy::none())
1722                    .build()
1723                    .expect("a client")
1724            }
1725
1726            pub(crate) fn key(&self, scopes: &[Scope]) -> String {
1727                self.tenants
1728                    .create_key(Actor::Cli, Some(self.tenant), Scopes::of(scopes), "test")
1729                    .expect("a key")
1730                    .token
1731            }
1732
1733            pub(crate) fn tenant_log(&self, action: &str) -> Vec<Value> {
1734                let store = self.tenants.store(self.tenant).expect("its store");
1735                store
1736                    .records()
1737                    .into_iter()
1738                    .filter(|r| r["action"] == action)
1739                    .collect()
1740            }
1741
1742            /// Sends a POST /vcons over HTTP/1.1 with a chunked body: the
1743            /// chunks given, and then, unless `finish`, nothing more while
1744            /// the connection stays open. Returns the status line.
1745            async fn chunked(&self, token: &str, chunks: &[usize], finish: bool) -> String {
1746                let mut body = String::new();
1747                for &size in chunks {
1748                    body.push_str(&format!("{size:x}\r\n{}\r\n", "x".repeat(size)));
1749                }
1750                if finish {
1751                    body.push_str("0\r\n\r\n");
1752                }
1753                self.raw(token, "transfer-encoding: chunked", body.as_bytes())
1754                    .await
1755            }
1756
1757            /// Sends a POST /vcons over HTTP/1.1 with one header of the
1758            /// caller's and the bytes given, keeping the connection open.
1759            /// Returns the status line.
1760            async fn raw(&self, token: &str, header: &str, body: &[u8]) -> String {
1761                let mut roots = rustls::RootCertStore::empty();
1762                for cert in CertificateDer::pem_slice_iter(self.certificate.pem.as_bytes()) {
1763                    roots.add(cert.expect("pem")).expect("a root");
1764                }
1765                let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
1766                let mut config = rustls::ClientConfig::builder_with_provider(provider)
1767                    .with_safe_default_protocol_versions()
1768                    .expect("versions")
1769                    .with_root_certificates(roots)
1770                    .with_no_client_auth();
1771                config.alpn_protocols = vec![b"http/1.1".to_vec()];
1772                let tcp = tokio::net::TcpStream::connect(self.addr)
1773                    .await
1774                    .expect("connect");
1775                let mut tls = tokio_rustls::TlsConnector::from(Arc::new(config))
1776                    .connect("localhost".try_into().expect("name"), tcp)
1777                    .await
1778                    .expect("handshake");
1779                let head = format!(
1780                    "POST /vcons HTTP/1.1\r\nhost: localhost\r\nauthorization: Bearer {token}\r\n\
1781                     content-type: application/vcon\r\n{header}\r\n\r\n"
1782                );
1783                tls.write_all(head.as_bytes()).await.expect("head");
1784                // The server may answer and stop reading before all is sent.
1785                let _ = tls.write_all(body).await;
1786                let mut answer = vec![0; 4096];
1787                let read = tokio::time::timeout(Duration::from_secs(10), tls.read(&mut answer))
1788                    .await
1789                    .expect("an answer while the body is still open")
1790                    .expect("read");
1791                let answer = String::from_utf8_lossy(&answer[..read]).into_owned();
1792                answer.lines().next().unwrap_or_default().to_owned()
1793            }
1794        }
1795
1796        async fn post(s: &Server, token: Option<&str>, body: Vec<u8>) -> reqwest::Response {
1797            let mut request = s
1798                .client()
1799                .post(s.url("/vcons"))
1800                .header("content-type", "application/vcon")
1801                .body(body);
1802            if let Some(token) = token {
1803                request = request.bearer_auth(token);
1804            }
1805            request.send().await.expect("an answer")
1806        }
1807
1808        /// Checks the answer is a problem document for its status, and
1809        /// returns its type's last part.
1810        pub(crate) async fn problem(answer: reqwest::Response) -> String {
1811            assert_eq!(
1812                answer.headers()["content-type"],
1813                "application/problem+json",
1814                "{answer:?}"
1815            );
1816            let status = answer.status().as_u16();
1817            let body: Value =
1818                serde_json::from_str(&answer.text().await.expect("text")).expect("JSON");
1819            assert_eq!(body["status"], status);
1820            assert!(body["title"].as_str().is_some_and(|t| !t.is_empty()));
1821            body["type"]
1822                .as_str()
1823                .and_then(|t| t.strip_prefix("https://vcon-server.com/problems/"))
1824                .expect("a type under the server's domain")
1825                .to_owned()
1826        }
1827
1828        #[tokio::test]
1829        async fn a_request_without_a_valid_key_is_unauthorized() {
1830            let s = server().await;
1831            for token in [None, Some("vcs_nonsense")] {
1832                let answer = post(&s, token, b"{}".to_vec()).await;
1833                assert_eq!(answer.status(), 401, "{token:?}");
1834                assert_eq!(answer.headers()["www-authenticate"], "Bearer");
1835                assert_eq!(problem(answer).await, "unauthorized");
1836            }
1837        }
1838
1839        #[tokio::test]
1840        async fn a_key_table_that_cannot_be_read_makes_the_service_unavailable() {
1841            let s = server().await;
1842            let writer = s.key(&[Scope::Write]);
1843            s.tenants
1844                .conn_for_test()
1845                .execute_batch("ALTER TABLE api_key RENAME TO gone")
1846                .expect("lose the table");
1847            let answer = post(&s, Some(&writer), b"{}".to_vec()).await;
1848            assert_eq!(answer.status(), 503);
1849            assert_eq!(problem(answer).await, "unavailable");
1850        }
1851
1852        #[tokio::test]
1853        async fn a_refusal_that_cannot_be_recorded_answers_that_the_audit_failed() {
1854            let s = server().await;
1855            let reader = s.key(&[Scope::Read]);
1856            s.tenants.store(s.tenant).expect("its store").break_audit();
1857            let answer = post(&s, Some(&reader), b"{}".to_vec()).await;
1858            assert_eq!(answer.status(), 503);
1859            assert_eq!(problem(answer).await, "audit-failed");
1860
1861            let operator = s
1862                .tenants
1863                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
1864                .expect("made")
1865                .token;
1866            s.tenants.break_audit();
1867            let answer = post(&s, Some(&operator), b"{}".to_vec()).await;
1868            assert_eq!(answer.status(), 503);
1869            assert_eq!(problem(answer).await, "audit-failed");
1870        }
1871
1872        #[tokio::test]
1873        async fn a_key_without_the_scope_is_forbidden_and_recorded() {
1874            let s = server().await;
1875            let reader = s.key(&[Scope::Read]);
1876            let answer = post(&s, Some(&reader), b"{}".to_vec()).await;
1877            assert_eq!(answer.status(), 403);
1878            assert_eq!(problem(answer).await, "forbidden");
1879            let refused = s.tenant_log("create");
1880            assert_eq!(refused.len(), 1);
1881            assert_eq!(refused[0]["status"], 403);
1882
1883            let operator = s
1884                .tenants
1885                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
1886                .expect("made")
1887                .token;
1888            let answer = post(&s, Some(&operator), b"{}".to_vec()).await;
1889            assert_eq!(answer.status(), 403);
1890            let admin: Vec<Value> = s
1891                .tenants
1892                .admin_records()
1893                .into_iter()
1894                .filter(|r| r["action"] == "create")
1895                .collect();
1896            assert_eq!(admin.len(), 1, "an operator's refusal is in the admin log");
1897            assert_eq!(admin[0]["status"], 403);
1898        }
1899
1900        #[tokio::test]
1901        async fn a_closing_tenant_is_unavailable() {
1902            let s = server().await;
1903            let writer = s.key(&[Scope::Write]);
1904            s.tenants.close(s.tenant);
1905            let answer = post(&s, Some(&writer), b"{}".to_vec()).await;
1906            assert_eq!(answer.status(), 503);
1907            assert_eq!(problem(answer).await, "unavailable");
1908        }
1909
1910        #[tokio::test]
1911        async fn a_declared_length_over_the_limit_is_refused_at_once() {
1912            let s = server().await;
1913            let writer = s.key(&[Scope::Write]);
1914            let answer = post(&s, Some(&writer), vec![b'x'; LIMIT + 1]).await;
1915            assert_eq!(answer.status(), 413);
1916            assert_eq!(problem(answer).await, "too-large");
1917            assert_eq!(s.tenant_log("create")[0]["status"], 413);
1918            let declared = format!("content-length: {}", LIMIT + 1);
1919            assert_eq!(
1920                s.raw(&writer, &declared, b"").await,
1921                "HTTP/1.1 413 Payload Too Large",
1922                "refused on the declared length, with no byte of the body sent"
1923            );
1924        }
1925
1926        #[tokio::test]
1927        async fn a_body_over_the_limit_is_refused_before_it_is_read_in_full() {
1928            let s = server().await;
1929            let writer = s.key(&[Scope::Write]);
1930            let status = s.chunked(&writer, &[LIMIT / 2, LIMIT / 2 + 1], false).await;
1931            assert_eq!(status, "HTTP/1.1 413 Payload Too Large");
1932        }
1933
1934        #[tokio::test]
1935        async fn a_body_that_stops_arriving_times_out() {
1936            let s = server().await;
1937            let writer = s.key(&[Scope::Write]);
1938            let status = s.chunked(&writer, &[10], false).await;
1939            assert_eq!(status, "HTTP/1.1 408 Request Timeout");
1940            assert_eq!(s.tenant_log("create")[0]["status"], 408);
1941        }
1942
1943        #[tokio::test]
1944        /// The body reaches Format whole: a body of x's at the limit is
1945        /// refused as not JSON, not as too large.
1946        async fn a_request_within_the_limits_goes_on_to_its_route() {
1947            let s = server().await;
1948            let writer = s.key(&[Scope::Write]);
1949            let status = s.chunked(&writer, &[LIMIT / 2, LIMIT / 2], true).await;
1950            assert_eq!(status, "HTTP/1.1 422 Unprocessable Entity");
1951            let answer = post(&s, Some(&writer), b"{}".to_vec()).await;
1952            assert_eq!(answer.status(), 422);
1953            assert_eq!(problem(answer).await, "no-form");
1954        }
1955    }
1956
1957    #[test]
1958    fn a_removal_query_is_none_or_force_true_or_false_and_nothing_else() {
1959        assert_eq!(removal(None), Some(Removal::Empty));
1960        assert_eq!(removal(Some("force=false")), Some(Removal::Empty));
1961        assert_eq!(removal(Some("force=true")), Some(Removal::Force));
1962        for query in [
1963            "",
1964            "force",
1965            "force=",
1966            "force=1",
1967            "Force=true",
1968            "force=true&",
1969        ] {
1970            assert_eq!(removal(Some(query)), None, "{query:?}");
1971        }
1972    }
1973
1974    /// A refusal the vcon crate adds in a later release, which this
1975    /// server does not name yet, still has a problem: not-a-vcon. No test
1976    /// can send one, since every refusal there is today is named above; this
1977    /// holds the problem the catch-all arm answers with.
1978    #[test]
1979    fn a_refusal_this_server_does_not_name_is_not_a_vcon() {
1980        let problem = Problem::Invalid(Invalid::Other);
1981        assert_eq!(
1982            (problem.status().as_u16(), problem.name().0),
1983            (422, "not-a-vcon")
1984        );
1985    }
1986
1987    #[test]
1988    fn every_refusal_from_format_has_its_own_problem() {
1989        let cases = [
1990            (Rejection::TooLarge, 413, "too-large"),
1991            (Rejection::TooDeep, 422, "too-deep"),
1992            (Rejection::BadGzip, 422, "bad-gzip"),
1993            (Rejection::NotJson, 422, "not-json"),
1994            (Rejection::DuplicateKey, 422, "duplicate-member"),
1995            (Rejection::AmbiguousForm, 422, "ambiguous-form"),
1996            (Rejection::NoForm, 422, "no-form"),
1997            (Rejection::MissingUuid, 422, "missing-uuid"),
1998            (Rejection::BadUuid, 422, "bad-uuid"),
1999            (Rejection::MissingCreatedAt, 422, "missing-created-at"),
2000            (Rejection::BadPayload, 422, "bad-payload"),
2001            (Rejection::UuidMismatch, 422, "uuid-mismatch"),
2002            (Rejection::BadCritical, 422, "bad-critical"),
2003            (
2004                Rejection::UnsupportedCritical(vec!["secret-name".to_owned()]),
2005                422,
2006                "unsupported-extension",
2007            ),
2008        ];
2009        for (rejection, status, name) in &cases {
2010            let problem = rejected(rejection);
2011            assert_eq!(
2012                (problem.status().as_u16(), problem.name().0),
2013                (*status, *name),
2014                "{rejection:?}"
2015            );
2016            let answer = format!("{:?}", problem.into_response().into_body());
2017            assert!(!answer.contains("secret-name"), "the body is not repeated");
2018        }
2019    }
2020
2021    #[test]
2022    fn every_store_outcome_has_its_problem() {
2023        let cases = [
2024            (StoreError::Conflict, 409, "conflict"),
2025            (StoreError::Gone, 410, "gone"),
2026            (StoreError::NotFound, 404, "not-found"),
2027            (StoreError::NotDeleted, 409, "not-deleted"),
2028            (StoreError::PreconditionFailed, 412, "precondition-failed"),
2029            (StoreError::Immutable, 409, "immutable"),
2030            (StoreError::LimitReached, 503, "limit-reached"),
2031            (StoreError::Damaged, 500, "damaged"),
2032            (StoreError::AuditFailed, 503, "audit-failed"),
2033            (StoreError::Io, 500, "internal"),
2034        ];
2035        for (error, status, name) in cases {
2036            let problem = failed(error);
2037            assert_eq!(
2038                (problem.status().as_u16(), problem.name().0),
2039                (status, name),
2040                "{error:?}"
2041            );
2042        }
2043    }
2044
2045    #[test]
2046    fn every_tenants_outcome_has_its_problem() {
2047        let cases = [
2048            (TenantError::NotFound, 404, "unknown-tenant"),
2049            (TenantError::NameTaken, 409, "name-taken"),
2050            (TenantError::BadName, 422, "invalid-body"),
2051            (TenantError::HoldsVcons, 409, "holds-vcons"),
2052            (TenantError::UnderHold, 409, "under-hold"),
2053            (TenantError::LimitReached, 503, "master-key-limit"),
2054            (TenantError::AuditFailed, 503, "audit-failed"),
2055            (TenantError::Closing, 503, "unavailable"),
2056            (TenantError::CliOnly, 403, "forbidden"),
2057            (TenantError::BadScopes, 422, "bad-scopes"),
2058            (TenantError::WrongKey, 500, "internal"),
2059            (TenantError::Io, 500, "internal"),
2060        ];
2061        for (error, status, name) in cases {
2062            let problem = Problem::Tenant(error);
2063            assert_eq!(
2064                (problem.status().as_u16(), problem.name().0),
2065                (status, name),
2066                "{error:?}"
2067            );
2068        }
2069    }
2070
2071    mod vcons {
2072        use std::io::Write;
2073
2074        use super::pipeline::{Server, problem, server};
2075        use crate::access::Scope;
2076        use crate::store::Caller as StoreCaller;
2077        use crate::store::testing::vcon;
2078
2079        const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
2080
2081        fn uuid(n: u8) -> String {
2082            format!("01a07da8-c2bb-83e5-b9a2-279e0d16bc{n:02x}")
2083        }
2084
2085        async fn post(
2086            s: &Server,
2087            token: &str,
2088            content_type: Option<&str>,
2089            body: Vec<u8>,
2090        ) -> reqwest::Response {
2091            let mut request = s
2092                .client()
2093                .post(s.url("/vcons"))
2094                .bearer_auth(token)
2095                .body(body);
2096            if let Some(content_type) = content_type {
2097                request = request.header("content-type", content_type);
2098            }
2099            request.send().await.expect("an answer")
2100        }
2101
2102        fn gzip(body: &[u8]) -> Vec<u8> {
2103            let mut encoder =
2104                flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2105            encoder.write_all(body).expect("gzip");
2106            encoder.finish().expect("gzip")
2107        }
2108
2109        fn store_caller() -> StoreCaller {
2110            StoreCaller {
2111                key: crate::ids::KeyId([9; 8]),
2112            }
2113        }
2114
2115        #[tokio::test]
2116        async fn a_new_vcon_is_created_with_its_location_and_tag() {
2117            let s = server().await;
2118            let writer = s.key(&[Scope::Write]);
2119            let (found, body) = vcon(UUID, "first");
2120            let answer = post(&s, &writer, Some("application/vcon"), body.clone()).await;
2121            assert_eq!(answer.status(), 201);
2122            assert_eq!(answer.headers()["location"], format!("/vcons/{UUID}"));
2123            let tag = answer.headers()["etag"].to_str().expect("ascii").to_owned();
2124            let stored = s
2125                .tenants
2126                .store(s.tenant)
2127                .expect("its store")
2128                .get(&store_caller(), found.uuid)
2129                .expect("stored");
2130            assert_eq!(tag, format!("\"{}\"", stored.version), "a strong tag");
2131            assert_eq!(stored.body, body);
2132
2133            let again = post(&s, &writer, Some("application/vcon"), body).await;
2134            assert_eq!(again.status(), 200, "the same bytes are already current");
2135            assert_eq!(again.headers()["etag"], tag.as_str());
2136        }
2137
2138        #[tokio::test]
2139        async fn another_vcon_with_the_uuid_conflicts() {
2140            let s = server().await;
2141            let writer = s.key(&[Scope::Write]);
2142            let (_, first) = vcon(UUID, "first");
2143            let (_, other) = vcon(UUID, "other");
2144            assert_eq!(post(&s, &writer, None, first).await.status(), 201);
2145            let answer = post(&s, &writer, None, other).await;
2146            assert_eq!(answer.status(), 409);
2147            assert_eq!(problem(answer).await, "conflict");
2148            let statuses: Vec<_> = s
2149                .tenant_log("create")
2150                .into_iter()
2151                .map(|r| r["status"].clone())
2152                .collect();
2153            assert_eq!(statuses, [201, 409], "Store records its own, once");
2154        }
2155
2156        #[tokio::test]
2157        async fn a_vcon_needing_an_unsupported_extension_is_unprocessable() {
2158            let s = super::pipeline::server_with(&["known"]).await;
2159            let writer = s.key(&[Scope::Write]);
2160            let needing = |n: u8, extension: &str| {
2161                format!(
2162                    r#"{{"uuid": "{}", "created_at": "2026-10-23T12:00:00Z", "must_support": ["{extension}"], "parties": []}}"#,
2163                    uuid(n)
2164                )
2165                .into_bytes()
2166            };
2167            assert_eq!(
2168                post(&s, &writer, None, needing(1, "known")).await.status(),
2169                201
2170            );
2171            let answer = post(&s, &writer, None, needing(2, "unknown")).await;
2172            assert_eq!(answer.status(), 422);
2173            assert_eq!(problem(answer).await, "unsupported-extension");
2174        }
2175
2176        #[tokio::test]
2177        async fn a_media_type_that_is_not_text_is_unsupported() {
2178            let s = server().await;
2179            let writer = s.key(&[Scope::Write]);
2180            let (_, body) = vcon(UUID, "first");
2181            let answer = s
2182                .client()
2183                .post(s.url("/vcons"))
2184                .bearer_auth(&writer)
2185                .header(
2186                    "content-type",
2187                    reqwest::header::HeaderValue::from_bytes(b"application/vcon\xff")
2188                        .expect("a header value"),
2189                )
2190                .body(body)
2191                .send()
2192                .await
2193                .expect("an answer");
2194            assert_eq!(answer.status(), 415);
2195        }
2196
2197        #[tokio::test]
2198        async fn a_deleted_vcon_is_gone() {
2199            let s = server().await;
2200            let writer = s.key(&[Scope::Write]);
2201            let (found, body) = vcon(UUID, "first");
2202            assert_eq!(post(&s, &writer, None, body.clone()).await.status(), 201);
2203            s.tenants
2204                .store(s.tenant)
2205                .expect("its store")
2206                .delete(
2207                    &store_caller(),
2208                    found.uuid,
2209                    crate::retention::Retention::Years(7),
2210                )
2211                .expect("deleted");
2212            let answer = post(&s, &writer, None, body).await;
2213            assert_eq!(answer.status(), 410);
2214            assert_eq!(problem(answer).await, "gone");
2215        }
2216
2217        #[tokio::test]
2218        async fn the_vcon_media_types_json_and_none_are_taken() {
2219            let s = server().await;
2220            let writer = s.key(&[Scope::Write]);
2221            let cases: [(Option<&str>, bool); 6] = [
2222                (None, false),
2223                (Some("application/vcon"), false),
2224                (Some("application/vcon+gzip"), true),
2225                (Some("application/json"), false),
2226                (Some("Application/VCON; charset=utf-8"), false),
2227                (Some("APPLICATION/VCON+GZIP ; q=1"), true),
2228            ];
2229            for (n, (content_type, gzipped)) in cases.into_iter().enumerate() {
2230                let (_, body) = vcon(&uuid(n as u8), "typed");
2231                let body = if gzipped { gzip(&body) } else { body };
2232                let answer = post(&s, &writer, content_type, body).await;
2233                assert_eq!(answer.status(), 201, "{content_type:?}");
2234            }
2235        }
2236
2237        #[tokio::test]
2238        async fn another_media_type_is_unsupported_and_recorded() {
2239            let s = server().await;
2240            let writer = s.key(&[Scope::Write]);
2241            let (_, body) = vcon(UUID, "first");
2242            for content_type in ["text/plain", "application/vcon-json", "application/xml"] {
2243                let answer = post(&s, &writer, Some(content_type), body.clone()).await;
2244                assert_eq!(answer.status(), 415, "{content_type}");
2245                assert_eq!(problem(answer).await, "unsupported-media-type");
2246            }
2247            assert_eq!(s.tenant_log("create")[0]["status"], 415);
2248        }
2249
2250        #[tokio::test]
2251        async fn a_body_format_refuses_is_unprocessable_and_recorded() {
2252            let s = server().await;
2253            let writer = s.key(&[Scope::Write]);
2254            let answer = post(&s, &writer, None, b"not json".to_vec()).await;
2255            assert_eq!(answer.status(), 422);
2256            assert_eq!(problem(answer).await, "not-json");
2257            let (_, body) = vcon(UUID, "first");
2258            let answer = post(&s, &writer, Some("application/vcon+gzip"), body).await;
2259            assert_eq!(answer.status(), 422);
2260            assert_eq!(problem(answer).await, "bad-gzip");
2261            let refused = s.tenant_log("create");
2262            assert_eq!(refused.len(), 2);
2263            assert_eq!(refused[0]["status"], 422);
2264        }
2265
2266        #[tokio::test]
2267        async fn a_body_that_inflates_past_its_limit_is_too_large() {
2268            let s = server().await;
2269            let writer = s.key(&[Scope::Write]);
2270            let bomb = gzip(&vec![b' '; 2 << 20]);
2271            assert!(bomb.len() < 64 * 1024, "small on the wire");
2272            let answer = post(&s, &writer, Some("application/vcon+gzip"), bomb).await;
2273            assert_eq!(answer.status(), 413);
2274            assert_eq!(problem(answer).await, "too-large");
2275        }
2276
2277        #[tokio::test]
2278        async fn a_body_nested_too_deeply_is_unprocessable_and_recorded() {
2279            use base64::Engine;
2280            use base64::engine::general_purpose::URL_SAFE_NO_PAD;
2281            let s = server().await;
2282            let writer = s.key(&[Scope::Write]);
2283            let (_, mut signed) = vcon(UUID, "first");
2284            for _ in 0..=vcon::read::MAX_SIGNED_LAYERS {
2285                signed = serde_json::json!({
2286                    "payload": URL_SAFE_NO_PAD.encode(gzip(&signed)),
2287                    "signatures": [{}],
2288                })
2289                .to_string()
2290                .into_bytes();
2291            }
2292            let open = "[".repeat(vcon::read::MAX_JSON_DEPTH);
2293            let close = "]".repeat(vcon::read::MAX_JSON_DEPTH);
2294            let json = format!(r#"{{"uuid":"{UUID}","created_at":"x","a":{open}{close}}}"#);
2295            for body in [signed, json.into_bytes()] {
2296                let answer = post(&s, &writer, None, body).await;
2297                assert_eq!(answer.status(), 422);
2298                assert_eq!(problem(answer).await, "too-deep");
2299            }
2300            let refused = s.tenant_log("create");
2301            assert_eq!(refused.len(), 2);
2302            assert!(refused.iter().all(|r| r["status"] == 422));
2303        }
2304
2305        #[tokio::test]
2306        async fn a_create_whose_record_fails_is_unavailable() {
2307            let s = server().await;
2308            let writer = s.key(&[Scope::Write]);
2309            s.tenants.store(s.tenant).expect("its store").break_audit();
2310            let (_, body) = vcon(UUID, "first");
2311            let answer = post(&s, &writer, None, body).await;
2312            assert_eq!(answer.status(), 503);
2313            assert_eq!(problem(answer).await, "audit-failed");
2314        }
2315    }
2316
2317    mod legal {
2318        use reqwest::Method;
2319        use serde_json::Value;
2320
2321        use super::pipeline::{Server, problem, server};
2322        use crate::access::{Scope, Scopes};
2323        use crate::audit::Actor;
2324        use crate::ids::VersionId;
2325        use crate::retention::Retention;
2326
2327        const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
2328        const NEVER: &str = "0199e5c6-0d7a-7c11-8000-000000000001";
2329
2330        fn key(s: &Server, scopes: &[Scope]) -> String {
2331            s.tenants
2332                .create_key(Actor::Cli, Some(s.tenant), Scopes::of(scopes), "test")
2333                .expect("a key")
2334                .token
2335        }
2336
2337        fn a4() -> Vec<u8> {
2338            std::fs::read(format!(
2339                "{}/../vcon/tests/fixtures/ietf/ab_call_ext_rec.vcon",
2340                env!("CARGO_MANIFEST_DIR")
2341            ))
2342            .expect("A.4")
2343        }
2344
2345        async fn call(s: &Server, method: Method, path: &str, token: &str) -> reqwest::Response {
2346            s.client()
2347                .request(method, s.url(path))
2348                .bearer_auth(token)
2349                .send()
2350                .await
2351                .expect("an answer")
2352        }
2353
2354        /// Stores A.4 and returns its entity tag.
2355        async fn stored(s: &Server) -> String {
2356            let writer = key(s, &[Scope::Write]);
2357            let answer = s
2358                .client()
2359                .post(s.url("/vcons"))
2360                .bearer_auth(&writer)
2361                .header("content-type", "application/vcon")
2362                .body(a4())
2363                .send()
2364                .await
2365                .expect("an answer");
2366            assert_eq!(answer.status(), 201);
2367            answer.headers()["etag"].to_str().expect("tag").to_owned()
2368        }
2369
2370        async fn deleted(s: &Server) {
2371            let deleter = key(s, &[Scope::Delete]);
2372            let path = format!("/vcons/{UUID}");
2373            assert_eq!(call(s, Method::DELETE, &path, &deleter).await.status(), 204);
2374        }
2375
2376        /// Every legal route for `uuid`, with `version` for the one that
2377        /// names a version.
2378        fn routes(uuid: &str, version: &str) -> Vec<(Method, String)> {
2379            vec![
2380                (Method::GET, format!("/legal/vcons/{uuid}")),
2381                (Method::GET, format!("/legal/vcons/{uuid}/versions")),
2382                (
2383                    Method::GET,
2384                    format!("/legal/vcons/{uuid}/versions/{version}"),
2385                ),
2386                (Method::POST, format!("/legal/vcons/{uuid}/restore")),
2387                (Method::PUT, format!("/legal/vcons/{uuid}/hold")),
2388                (Method::DELETE, format!("/legal/vcons/{uuid}/hold")),
2389            ]
2390        }
2391
2392        fn bare(tag: &str) -> String {
2393            tag.trim_matches('"').to_owned()
2394        }
2395
2396        #[tokio::test]
2397        async fn every_legal_route_needs_the_legal_scope() {
2398            let s = server().await;
2399            let tag = stored(&s).await;
2400            let others = key(&s, &[Scope::Write, Scope::Read, Scope::Delete]);
2401            for (method, path) in routes(UUID, &bare(&tag)) {
2402                let answer = call(&s, method.clone(), &path, &others).await;
2403                assert_eq!(answer.status(), 403, "{method} {path}");
2404            }
2405        }
2406
2407        #[tokio::test]
2408        async fn a_deleted_vcon_reads_back_byte_for_byte_with_a_legal_key() {
2409            let s = server().await;
2410            let tag = stored(&s).await;
2411            let legal = key(&s, &[Scope::Legal]);
2412            let path = format!("/legal/vcons/{UUID}");
2413
2414            let live = call(&s, Method::GET, &path, &legal).await;
2415            assert_eq!(live.status(), 200);
2416            assert_eq!(live.headers()["vcon-state"], "live");
2417
2418            deleted(&s).await;
2419            let answer = call(&s, Method::GET, &path, &legal).await;
2420            assert_eq!(answer.status(), 200);
2421            assert_eq!(answer.headers()["vcon-state"], "deleted");
2422            assert_eq!(answer.headers()["etag"], tag.as_str());
2423            assert_eq!(answer.headers()["content-type"], "application/vcon");
2424            assert_eq!(
2425                answer.bytes().await.expect("body").as_ref(),
2426                a4().as_slice()
2427            );
2428            assert_eq!(
2429                s.tenant_log("legal_read").last().expect("recorded")["status"],
2430                200
2431            );
2432        }
2433
2434        #[tokio::test]
2435        async fn the_version_list_and_a_named_version() {
2436            let s = server().await;
2437            let tag = stored(&s).await;
2438            let legal = key(&s, &[Scope::Legal]);
2439            let live = call(
2440                &s,
2441                Method::GET,
2442                &format!("/legal/vcons/{UUID}/versions"),
2443                &legal,
2444            )
2445            .await;
2446            let live: Value =
2447                serde_json::from_slice(&live.bytes().await.expect("body")).expect("json");
2448            assert_eq!(live[0]["current"], true, "live, so current");
2449            assert!(live[0]["hidden_at"].is_null(), "not hidden while live");
2450            deleted(&s).await;
2451
2452            let list = call(
2453                &s,
2454                Method::GET,
2455                &format!("/legal/vcons/{UUID}/versions"),
2456                &legal,
2457            )
2458            .await;
2459            assert_eq!(list.status(), 200);
2460            assert_eq!(list.headers()["content-type"], "application/json");
2461            let list: Value =
2462                serde_json::from_slice(&list.bytes().await.expect("body")).expect("json");
2463            let versions = list.as_array().expect("a list");
2464            assert_eq!(versions.len(), 1);
2465            assert_eq!(versions[0]["version"], bare(&tag));
2466            assert_eq!(versions[0]["current"], false);
2467            assert!(
2468                versions[0]["stored_at"]
2469                    .as_str()
2470                    .expect("a time")
2471                    .ends_with('Z')
2472            );
2473            assert!(versions[0]["hidden_at"].is_string(), "hidden by the delete");
2474
2475            let one = format!("/legal/vcons/{UUID}/versions/{}", bare(&tag));
2476            let answer = call(&s, Method::GET, &one, &legal).await;
2477            assert_eq!(answer.status(), 200);
2478            assert_eq!(answer.headers()["etag"], tag.as_str());
2479            assert_eq!(
2480                answer.bytes().await.expect("body").as_ref(),
2481                a4().as_slice()
2482            );
2483
2484            let unknown = format!("/legal/vcons/{UUID}/versions/{}", VersionId::generate());
2485            let answer = call(&s, Method::GET, &unknown, &legal).await;
2486            assert_eq!(answer.status(), 404);
2487            assert_eq!(problem(answer).await, "not-found");
2488
2489            let bad = format!("/legal/vcons/{UUID}/versions/not-a-version");
2490            let answer = call(&s, Method::GET, &bad, &legal).await;
2491            assert_eq!(answer.status(), 400);
2492            assert_eq!(problem(answer).await, "bad-path-version");
2493            assert_eq!(
2494                s.tenant_log("legal_read").last().expect("recorded")["status"],
2495                400
2496            );
2497        }
2498
2499        #[tokio::test]
2500        async fn restore_answers_its_tag_then_409_once_live() {
2501            let s = server().await;
2502            let tag = stored(&s).await;
2503            deleted(&s).await;
2504            let token = key(&s, &[Scope::Legal, Scope::Read]);
2505            let path = format!("/legal/vcons/{UUID}/restore");
2506
2507            let answer = call(&s, Method::POST, &path, &token).await;
2508            assert_eq!(answer.status(), 200);
2509            assert_eq!(answer.headers()["etag"], tag.as_str());
2510            let read = call(&s, Method::GET, &format!("/vcons/{UUID}"), &token).await;
2511            assert_eq!(read.status(), 200, "live again");
2512
2513            let again = call(&s, Method::POST, &path, &token).await;
2514            assert_eq!(again.status(), 409);
2515            assert_eq!(problem(again).await, "not-deleted");
2516        }
2517
2518        #[tokio::test]
2519        async fn a_hold_is_set_and_released_with_204() {
2520            let s = server().await;
2521            stored(&s).await;
2522            let legal = key(&s, &[Scope::Legal]);
2523            let path = format!("/legal/vcons/{UUID}/hold");
2524            for method in [Method::PUT, Method::DELETE] {
2525                let answer = call(&s, method, &path, &legal).await;
2526                assert_eq!(answer.status(), 204);
2527                assert!(answer.bytes().await.expect("body").is_empty());
2528            }
2529            assert_eq!(s.tenant_log("hold").len(), 1, "the hold");
2530            assert_eq!(s.tenant_log("release").len(), 1, "the release, as itself");
2531        }
2532
2533        #[tokio::test]
2534        async fn a_held_vcon_survives_a_zero_period_delete_until_released() {
2535            let s = server().await;
2536            stored(&s).await;
2537            let legal = key(&s, &[Scope::Legal]);
2538            let hold = format!("/legal/vcons/{UUID}/hold");
2539            let read = format!("/legal/vcons/{UUID}");
2540            assert_eq!(call(&s, Method::PUT, &hold, &legal).await.status(), 204);
2541            s.tenants
2542                .set_retention(Actor::Cli, s.tenant, Some(Retention::Days(0)))
2543                .expect("set");
2544            deleted(&s).await;
2545            assert_eq!(
2546                call(&s, Method::GET, &read, &legal).await.status(),
2547                200,
2548                "held"
2549            );
2550
2551            assert_eq!(call(&s, Method::DELETE, &hold, &legal).await.status(), 204);
2552            s.tenants
2553                .store(s.tenant)
2554                .expect("its store")
2555                .purge_due(Retention::Days(0))
2556                .expect("swept");
2557            assert_eq!(
2558                call(&s, Method::GET, &read, &legal).await.status(),
2559                410,
2560                "released"
2561            );
2562        }
2563
2564        #[tokio::test]
2565        async fn a_gzipped_vcon_reads_back_gzipped() {
2566            use std::io::Write;
2567            let s = server().await;
2568            let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2569            gz.write_all(&a4()).expect("gzip");
2570            let gzipped = gz.finish().expect("gzip");
2571            let writer = key(&s, &[Scope::Write]);
2572            let answer = s
2573                .client()
2574                .post(s.url("/vcons"))
2575                .bearer_auth(&writer)
2576                .header("content-type", "application/vcon+gzip")
2577                .body(gzipped.clone())
2578                .send()
2579                .await
2580                .expect("an answer");
2581            assert_eq!(answer.status(), 201);
2582            deleted(&s).await;
2583            let legal = key(&s, &[Scope::Legal]);
2584            let answer = call(&s, Method::GET, &format!("/legal/vcons/{UUID}"), &legal).await;
2585            assert_eq!(answer.status(), 200);
2586            assert_eq!(answer.headers()["content-type"], "application/vcon+gzip");
2587            assert_eq!(
2588                answer.bytes().await.expect("body").as_ref(),
2589                gzipped.as_slice()
2590            );
2591        }
2592
2593        #[tokio::test]
2594        async fn a_purged_uuid_is_410_on_every_legal_route() {
2595            let s = server().await;
2596            let tag = stored(&s).await;
2597            s.tenants
2598                .set_retention(Actor::Cli, s.tenant, Some(Retention::Days(0)))
2599                .expect("set");
2600            deleted(&s).await;
2601            let legal = key(&s, &[Scope::Legal]);
2602            for (method, path) in routes(UUID, &bare(&tag)) {
2603                let answer = call(&s, method.clone(), &path, &legal).await;
2604                assert_eq!(answer.status(), 410, "{method} {path}");
2605                assert_eq!(problem(answer).await, "gone");
2606            }
2607        }
2608
2609        #[tokio::test]
2610        async fn a_uuid_never_stored_is_404_and_a_bad_one_400_on_every_legal_route() {
2611            let s = server().await;
2612            let legal = key(&s, &[Scope::Legal]);
2613            let version = VersionId::generate().to_string();
2614            for (method, path) in routes(NEVER, &version) {
2615                let answer = call(&s, method.clone(), &path, &legal).await;
2616                assert_eq!(answer.status(), 404, "{method} {path}");
2617                assert_eq!(problem(answer).await, "not-found");
2618            }
2619            for (method, path) in routes("not-a-uuid", &version) {
2620                let answer = call(&s, method.clone(), &path, &legal).await;
2621                assert_eq!(answer.status(), 400, "{method} {path}");
2622                assert_eq!(problem(answer).await, "bad-path-uuid");
2623            }
2624            for action in ["legal_read", "legal_versions", "restore", "hold", "release"] {
2625                let last = s.tenant_log(action).last().expect("recorded").clone();
2626                assert_eq!(last["status"], 400, "{action}'s refusal is recorded");
2627            }
2628        }
2629
2630        #[tokio::test]
2631        async fn damage_is_500_on_every_legal_route() {
2632            let s = server().await;
2633            let tag = stored(&s).await;
2634            deleted(&s).await;
2635            let legal = key(&s, &[Scope::Legal]);
2636            let store = s.tenants.store(s.tenant).expect("its store");
2637            store
2638                .conn_for_test()
2639                .execute_batch(
2640                    "UPDATE version SET wrapped_key = x'00', stored_at = 'x';
2641                     CREATE TEMP TRIGGER broken BEFORE UPDATE ON vcon
2642                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;",
2643                )
2644                .expect("damaged");
2645            for (method, path) in routes(UUID, &bare(&tag)) {
2646                let answer = call(&s, method.clone(), &path, &legal).await;
2647                assert_eq!(answer.status(), 500, "{method} {path}");
2648            }
2649        }
2650
2651        #[tokio::test]
2652        async fn an_answer_that_cannot_be_recorded_is_503_on_every_legal_route() {
2653            let s = server().await;
2654            let tag = stored(&s).await;
2655            let legal = key(&s, &[Scope::Legal]);
2656            s.tenants.store(s.tenant).expect("its store").break_audit();
2657            for (method, path) in routes(UUID, &bare(&tag)) {
2658                let answer = call(&s, method.clone(), &path, &legal).await;
2659                assert_eq!(answer.status(), 503, "{method} {path}");
2660                assert_eq!(problem(answer).await, "audit-failed");
2661            }
2662        }
2663    }
2664
2665    mod admin {
2666        use reqwest::Method;
2667        use serde_json::{Value, json};
2668
2669        use super::pipeline::{Server, problem, server};
2670        use crate::access::{Scope, Scopes};
2671        use crate::audit::Actor;
2672        use time::format_description::well_known::Rfc3339;
2673
2674        use crate::access;
2675        use crate::ids::{KeyId, TenantId};
2676        use crate::retention::Retention;
2677
2678        fn operator(s: &Server) -> String {
2679            s.tenants
2680                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
2681                .expect("an operator key")
2682                .token
2683        }
2684
2685        async fn call(
2686            s: &Server,
2687            method: Method,
2688            path: &str,
2689            token: Option<&str>,
2690            body: Option<&str>,
2691        ) -> reqwest::Response {
2692            let mut request = s.client().request(method, s.url(path));
2693            if let Some(token) = token {
2694                request = request.bearer_auth(token);
2695            }
2696            if let Some(body) = body {
2697                request = request
2698                    .header("content-type", "application/json")
2699                    .body(body.to_owned());
2700            }
2701            request.send().await.expect("an answer")
2702        }
2703
2704        async fn json(answer: reqwest::Response) -> Value {
2705            assert_eq!(answer.headers()["content-type"], "application/json");
2706            serde_json::from_slice(&answer.bytes().await.expect("body")).expect("JSON")
2707        }
2708
2709        /// The admin log's records of `action` written for requests, not
2710        /// for the tests' own setup from the command line.
2711        fn admin_log(s: &Server, action: &str) -> Vec<Value> {
2712            s.tenants
2713                .admin_records()
2714                .into_iter()
2715                .filter(|r| r["action"] == action && r["actor"] != "cli")
2716                .collect()
2717        }
2718
2719        fn key_id(token: &str) -> KeyId {
2720            access::parse(token).expect("a token")
2721        }
2722
2723        /// Each admin route, with a body it takes, and its audit action.
2724        fn routes(
2725            id: TenantId,
2726            key: KeyId,
2727        ) -> Vec<(Method, String, Option<&'static str>, &'static str)> {
2728            vec![
2729                (
2730                    Method::POST,
2731                    "/admin/tenants".to_owned(),
2732                    Some(r#"{"name":"beta"}"#),
2733                    "tenant_create",
2734                ),
2735                (
2736                    Method::GET,
2737                    "/admin/tenants".to_owned(),
2738                    None,
2739                    "tenant_list",
2740                ),
2741                (
2742                    Method::PATCH,
2743                    format!("/admin/tenants/{id}"),
2744                    Some(r#"{"retention":"5y"}"#),
2745                    "tenant_retention",
2746                ),
2747                (
2748                    Method::DELETE,
2749                    format!("/admin/tenants/{id}"),
2750                    None,
2751                    "tenant_remove",
2752                ),
2753                (
2754                    Method::POST,
2755                    format!("/admin/tenants/{id}/keys"),
2756                    Some(r#"{"scopes":["read"],"label":"ingest"}"#),
2757                    "key_create",
2758                ),
2759                (
2760                    Method::GET,
2761                    format!("/admin/tenants/{id}/keys"),
2762                    None,
2763                    "key_list",
2764                ),
2765                (
2766                    Method::DELETE,
2767                    format!("/admin/keys/{key}"),
2768                    None,
2769                    "key_revoke",
2770                ),
2771            ]
2772        }
2773
2774        #[tokio::test]
2775        async fn a_tenant_key_is_forbidden_and_no_key_unauthorized_on_every_admin_route() {
2776            let s = server().await;
2777            let every = [Scope::Write, Scope::Read, Scope::Delete, Scope::Legal];
2778            let tenant_key = s.key(&every);
2779            for (method, path, body, action) in routes(s.tenant, key_id(&tenant_key)) {
2780                let answer = call(&s, method.clone(), &path, Some(&tenant_key), body).await;
2781                assert_eq!(answer.status(), 403, "{method} {path}");
2782                assert_eq!(problem(answer).await, "forbidden");
2783                let refused: Vec<Value> = s
2784                    .tenant_log(action)
2785                    .into_iter()
2786                    .filter(|r| r["actor"] != "cli")
2787                    .collect();
2788                assert_eq!(refused.len(), 1, "{action} refusal in the tenant's log");
2789                assert_eq!(refused[0]["status"], 403);
2790
2791                let answer = call(&s, method.clone(), &path, None, body).await;
2792                assert_eq!(answer.status(), 401, "{method} {path}");
2793            }
2794            assert_eq!(s.tenants.list(Actor::Cli).expect("listed").len(), 1);
2795            assert_eq!(s.tenants.retention(s.tenant), Ok(None), "nothing changed");
2796            let keys = s.tenants.list_keys(Actor::Cli, Some(s.tenant));
2797            let keys = keys.expect("listed");
2798            assert_eq!(keys.len(), 1, "no key made");
2799            assert_eq!(keys[0].revoked_at, None, "nor revoked");
2800        }
2801
2802        #[tokio::test]
2803        async fn a_tenant_is_created_with_201_and_its_id_and_listed() {
2804            let s = server().await;
2805            let op = operator(&s);
2806            let body = r#"{"name":"beta","retention":"5y"}"#;
2807            let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(body)).await;
2808            assert_eq!(answer.status(), 201);
2809            let made = json(answer).await;
2810            let id = TenantId::parse(made["id"].as_str().expect("an id")).expect("a tenant id");
2811            assert_eq!(made, json!({ "id": id.to_string() }));
2812            assert_eq!(s.tenants.retention(id), Ok(Some(Retention::Years(5))));
2813            let created = admin_log(&s, "tenant_create");
2814            assert_eq!(created.last().expect("recorded")["status"], 201);
2815
2816            let body = r#"{"name":"gamma"}"#;
2817            let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(body)).await;
2818            assert_eq!(answer.status(), 201);
2819
2820            let answer = call(&s, Method::GET, "/admin/tenants", Some(&op), None).await;
2821            assert_eq!(answer.status(), 200);
2822            let listed = json(answer).await;
2823            let gamma = s
2824                .tenants
2825                .list(Actor::Cli)
2826                .expect("listed")
2827                .into_iter()
2828                .find(|t| t.name == "gamma")
2829                .expect("gamma")
2830                .id;
2831            assert_eq!(
2832                listed,
2833                json!([
2834                    { "id": s.tenant.to_string(), "name": "acme", "retention": null },
2835                    { "id": id.to_string(), "name": "beta", "retention": "5y" },
2836                    { "id": gamma.to_string(), "name": "gamma", "retention": null },
2837                ])
2838            );
2839            assert_eq!(admin_log(&s, "tenant_list")[0]["status"], 200);
2840        }
2841
2842        #[tokio::test]
2843        async fn a_taken_name_is_409() {
2844            let s = server().await;
2845            let op = operator(&s);
2846            let body = r#"{"name":"acme"}"#;
2847            let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(body)).await;
2848            assert_eq!(answer.status(), 409);
2849            assert_eq!(problem(answer).await, "name-taken");
2850            assert_eq!(s.tenants.list(Actor::Cli).expect("listed").len(), 1);
2851        }
2852
2853        #[tokio::test]
2854        async fn a_create_body_it_does_not_take_is_422_and_recorded_once() {
2855            let s = server().await;
2856            let op = operator(&s);
2857            let bodies = [
2858                "",
2859                "not json",
2860                "[]",
2861                "{}",
2862                r#"{"name":7}"#,
2863                r#"{"name":null}"#,
2864                r#"{"name":"beta","colour":"red"}"#,
2865                r#"{"name":"beta","name":"gamma"}"#,
2866                r#"{"name":"beta","retention":"7x"}"#,
2867                r#"{"name":"beta","retention":7}"#,
2868                r#"{"name":" beta"}"#,
2869                r#"{"name":""}"#,
2870            ];
2871            for body in bodies {
2872                let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(body)).await;
2873                assert_eq!(answer.status(), 422, "{body}");
2874                assert_eq!(problem(answer).await, "invalid-body", "{body}");
2875            }
2876            let refused = admin_log(&s, "tenant_create");
2877            assert_eq!(refused.len(), bodies.len(), "one per refusal");
2878            assert!(refused.iter().all(|r| r["status"] == 422));
2879            assert_eq!(s.tenants.list(Actor::Cli).expect("listed").len(), 1);
2880        }
2881
2882        #[tokio::test]
2883        async fn a_create_past_the_master_keys_wrap_limit_is_503() {
2884            let s = server().await;
2885            let op = operator(&s);
2886            s.tenants
2887                .conn_for_test()
2888                .execute("UPDATE master_key_use SET wraps = ?1", [1_i64 << 32])
2889                .expect("at the limit");
2890            let body = r#"{"name":"beta"}"#;
2891            let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(body)).await;
2892            assert_eq!(answer.status(), 503);
2893            assert_eq!(problem(answer).await, "master-key-limit");
2894        }
2895
2896        #[tokio::test]
2897        async fn a_retention_change_answers_200_with_the_period() {
2898            let s = server().await;
2899            let op = operator(&s);
2900            let path = format!("/admin/tenants/{}", s.tenant);
2901            let id = s.tenant.to_string();
2902            for (body, period, kept) in [
2903                (
2904                    r#"{"retention":"30d"}"#,
2905                    json!("30d"),
2906                    Some(Retention::Days(30)),
2907                ),
2908                (r#"{"retention":null}"#, Value::Null, None),
2909            ] {
2910                let answer = call(&s, Method::PATCH, &path, Some(&op), Some(body)).await;
2911                assert_eq!(answer.status(), 200, "{body}");
2912                assert_eq!(json(answer).await, json!({ "id": id, "retention": period }));
2913                assert_eq!(s.tenants.retention(s.tenant), Ok(kept), "{body}");
2914            }
2915            let changed = admin_log(&s, "tenant_retention");
2916            assert_eq!(changed.len(), 2);
2917            assert!(changed.iter().all(|r| r["status"] == 200));
2918            assert_eq!(s.tenant_log("retention").len(), 2, "the tenant's log too");
2919        }
2920
2921        #[tokio::test]
2922        async fn a_retention_change_for_no_tenant_is_404_and_recorded() {
2923            let s = server().await;
2924            let op = operator(&s);
2925            let body = Some(r#"{"retention":"5y"}"#);
2926            let unknown = TenantId([7; 16]).to_string();
2927            for id in [
2928                unknown.as_str(),
2929                "acme",
2930                "0123",
2931                &s.tenant.to_string().to_uppercase(),
2932            ] {
2933                let path = format!("/admin/tenants/{id}");
2934                let answer = call(&s, Method::PATCH, &path, Some(&op), body).await;
2935                assert_eq!(answer.status(), 404, "{id}");
2936                assert_eq!(problem(answer).await, "unknown-tenant", "{id}");
2937            }
2938            let refused = admin_log(&s, "tenant_retention");
2939            assert_eq!(refused.len(), 4);
2940            assert!(refused.iter().all(|r| r["status"] == 404));
2941            assert_eq!(s.tenants.retention(s.tenant), Ok(None));
2942        }
2943
2944        #[tokio::test]
2945        async fn a_retention_body_it_does_not_take_is_422_and_recorded() {
2946            let s = server().await;
2947            let op = operator(&s);
2948            let path = format!("/admin/tenants/{}", s.tenant);
2949            let bodies = [
2950                "",
2951                "{}",
2952                r#"{"retention":"7"}"#,
2953                r#"{"retention":"07y"}"#,
2954                r#"{"retention":5}"#,
2955                r#"{"retention":"5y","name":"beta"}"#,
2956            ];
2957            for body in bodies {
2958                let answer = call(&s, Method::PATCH, &path, Some(&op), Some(body)).await;
2959                assert_eq!(answer.status(), 422, "{body}");
2960                assert_eq!(problem(answer).await, "invalid-body", "{body}");
2961            }
2962            let refused = admin_log(&s, "tenant_retention");
2963            assert_eq!(refused.len(), bodies.len());
2964            assert!(refused.iter().all(|r| r["status"] == 422));
2965            assert_eq!(s.tenants.retention(s.tenant), Ok(None));
2966        }
2967
2968        #[tokio::test]
2969        async fn a_retention_change_for_a_closing_tenant_is_503() {
2970            let s = server().await;
2971            let op = operator(&s);
2972            s.tenants.close(s.tenant);
2973            let path = format!("/admin/tenants/{}", s.tenant);
2974            let body = Some(r#"{"retention":"5y"}"#);
2975            let answer = call(&s, Method::PATCH, &path, Some(&op), body).await;
2976            assert_eq!(answer.status(), 503);
2977            assert_eq!(problem(answer).await, "unavailable");
2978        }
2979
2980        #[tokio::test]
2981        async fn damage_is_500_on_every_admin_route() {
2982            let s = server().await;
2983            let op = operator(&s);
2984            let kept = key_id(&s.key(&[Scope::Read]));
2985            s.tenants
2986                .conn_for_test()
2987                .execute_batch(
2988                    "UPDATE tenant SET retention = 'x';
2989                     UPDATE api_key SET created_at = 'x' WHERE tenant IS NOT NULL;
2990                     CREATE TEMP TRIGGER no_key BEFORE INSERT ON api_key
2991                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;
2992                     CREATE TEMP TRIGGER no_revoke BEFORE UPDATE ON api_key
2993                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;
2994                     CREATE TEMP TRIGGER broken BEFORE INSERT ON tenant
2995                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;
2996                     CREATE TEMP TRIGGER broken_too BEFORE UPDATE ON tenant
2997                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;
2998                     CREATE TEMP TRIGGER broken_three BEFORE DELETE ON tenant
2999                     BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;",
3000                )
3001                .expect("damaged");
3002            for (method, path, body, _) in routes(s.tenant, kept) {
3003                let answer = call(&s, method.clone(), &path, Some(&op), body).await;
3004                assert_eq!(answer.status(), 500, "{method} {path}");
3005                assert_eq!(problem(answer).await, "internal");
3006            }
3007        }
3008
3009        #[tokio::test]
3010        async fn an_answer_that_cannot_be_recorded_is_503_on_every_admin_route() {
3011            let s = server().await;
3012            let op = operator(&s);
3013            let kept = key_id(&s.key(&[Scope::Read]));
3014            s.tenants.break_audit();
3015            for (method, path, body, _) in routes(s.tenant, kept) {
3016                let answer = call(&s, method.clone(), &path, Some(&op), body).await;
3017                assert_eq!(answer.status(), 503, "{method} {path}");
3018                assert_eq!(problem(answer).await, "audit-failed");
3019            }
3020            for body in ["{}", r#"{"retention":"5y"}"#] {
3021                let path = if body == "{}" {
3022                    "/admin/tenants".to_owned()
3023                } else {
3024                    format!("/admin/tenants/{}", TenantId([7; 16]))
3025                };
3026                let method = if body == "{}" {
3027                    Method::POST
3028                } else {
3029                    Method::PATCH
3030                };
3031                let answer = call(&s, method, &path, Some(&op), Some(body)).await;
3032                assert_eq!(problem(answer).await, "audit-failed", "a refusal too");
3033            }
3034            assert_eq!(
3035                s.tenants.list(Actor::Cli).err(),
3036                Some(crate::tenants::TenantError::AuditFailed)
3037            );
3038        }
3039
3040        /// Stores A.4 in the server's tenant with a key of its own.
3041        async fn stored(s: &Server) {
3042            let body = std::fs::read(format!(
3043                "{}/../vcon/tests/fixtures/ietf/ab_call_ext_rec.vcon",
3044                env!("CARGO_MANIFEST_DIR")
3045            ))
3046            .expect("A.4");
3047            let answer = s
3048                .client()
3049                .post(s.url("/vcons"))
3050                .bearer_auth(s.key(&[Scope::Write]))
3051                .header("content-type", "application/vcon")
3052                .body(body)
3053                .send()
3054                .await
3055                .expect("an answer");
3056            assert_eq!(answer.status(), 201);
3057        }
3058
3059        fn listed(s: &Server) -> Vec<TenantId> {
3060            let tenants = s.tenants.list(Actor::Cli).expect("listed");
3061            tenants.into_iter().map(|t| t.id).collect()
3062        }
3063
3064        #[tokio::test]
3065        async fn an_empty_tenant_is_removed_with_204_then_unknown() {
3066            let s = server().await;
3067            let op = operator(&s);
3068            let beta = s.tenants.create(Actor::Cli, "beta", None).expect("made");
3069            let path = format!("/admin/tenants/{beta}");
3070            let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3071            assert_eq!(answer.status(), 204);
3072            assert_eq!(listed(&s), vec![s.tenant]);
3073            let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3074            assert_eq!(answer.status(), 404);
3075            assert_eq!(problem(answer).await, "unknown-tenant");
3076            let removed = admin_log(&s, "tenant_remove");
3077            let statuses: Vec<&Value> = removed.iter().map(|r| &r["status"]).collect();
3078            assert_eq!(statuses, [204, 404]);
3079        }
3080
3081        #[tokio::test]
3082        async fn a_tenant_holding_vcons_is_409_without_force_and_removed_with_it() {
3083            let s = server().await;
3084            let op = operator(&s);
3085            stored(&s).await;
3086            let reader = s.key(&[Scope::Read]);
3087            let path = format!("/admin/tenants/{}", s.tenant);
3088            for query in ["", "?force=false"] {
3089                let path = format!("{path}{query}");
3090                let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3091                assert_eq!(answer.status(), 409, "{query}");
3092                assert_eq!(problem(answer).await, "holds-vcons", "{query}");
3093            }
3094            assert_eq!(listed(&s), vec![s.tenant], "still there");
3095
3096            let forced = format!("{path}?force=true");
3097            let answer = call(&s, Method::DELETE, &forced, Some(&op), None).await;
3098            assert_eq!(answer.status(), 204);
3099            assert_eq!(listed(&s), Vec::<TenantId>::new());
3100            let health = s
3101                .client()
3102                .get(s.url("/health"))
3103                .bearer_auth(&reader)
3104                .send()
3105                .await
3106                .expect("an answer");
3107            assert_eq!(health.status(), 401, "its keys went with it");
3108        }
3109
3110        #[tokio::test]
3111        async fn a_legal_hold_is_409_even_with_force() {
3112            let s = server().await;
3113            let op = operator(&s);
3114            stored(&s).await;
3115            let legal = s.key(&[Scope::Legal]);
3116            let hold = s
3117                .client()
3118                .put(s.url("/legal/vcons/01a07da8-c2bb-83e5-b9a2-279e0d16bc46/hold"))
3119                .bearer_auth(&legal)
3120                .send()
3121                .await
3122                .expect("an answer");
3123            assert_eq!(hold.status(), 204);
3124            let path = format!("/admin/tenants/{}?force=true", s.tenant);
3125            let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3126            assert_eq!(answer.status(), 409);
3127            assert_eq!(problem(answer).await, "under-hold");
3128            assert_eq!(listed(&s), vec![s.tenant], "still there");
3129            let removed = admin_log(&s, "tenant_remove");
3130            assert_eq!(removed.len(), 1);
3131            assert_eq!(removed[0]["status"], 409);
3132            let read = s
3133                .client()
3134                .get(s.url("/legal/vcons/01a07da8-c2bb-83e5-b9a2-279e0d16bc46"))
3135                .bearer_auth(&legal)
3136                .send()
3137                .await
3138                .expect("an answer");
3139            assert_eq!(read.status(), 200, "the tenant is served again");
3140        }
3141
3142        #[tokio::test]
3143        async fn a_query_it_does_not_take_is_400_and_recorded() {
3144            let s = server().await;
3145            let op = operator(&s);
3146            let queries = [
3147                "force=yes",
3148                "force=TRUE",
3149                "force",
3150                "force=",
3151                "force=true&force=true",
3152                "force=true&other=1",
3153                "other=1",
3154            ];
3155            for query in queries {
3156                let path = format!("/admin/tenants/{}?{query}", s.tenant);
3157                let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3158                assert_eq!(answer.status(), 400, "{query}");
3159                assert_eq!(problem(answer).await, "bad-query", "{query}");
3160            }
3161            let refused = admin_log(&s, "tenant_remove");
3162            assert_eq!(refused.len(), queries.len());
3163            assert!(refused.iter().all(|r| r["status"] == 400));
3164            assert_eq!(listed(&s), vec![s.tenant]);
3165        }
3166
3167        #[tokio::test]
3168        async fn a_removal_of_no_tenant_is_404_and_recorded() {
3169            let s = server().await;
3170            let op = operator(&s);
3171            let unknown = TenantId([7; 16]).to_string();
3172            for id in [unknown.as_str(), "acme"] {
3173                let path = format!("/admin/tenants/{id}?force=true");
3174                let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3175                assert_eq!(answer.status(), 404, "{id}");
3176                assert_eq!(problem(answer).await, "unknown-tenant", "{id}");
3177            }
3178            let refused = admin_log(&s, "tenant_remove");
3179            assert_eq!(refused.len(), 2);
3180            assert!(refused.iter().all(|r| r["status"] == 404));
3181        }
3182
3183        #[tokio::test]
3184        async fn a_removal_of_a_closing_tenant_is_503() {
3185            let s = server().await;
3186            let op = operator(&s);
3187            s.tenants.close(s.tenant);
3188            let path = format!("/admin/tenants/{}", s.tenant);
3189            let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3190            assert_eq!(answer.status(), 503);
3191            assert_eq!(problem(answer).await, "unavailable");
3192        }
3193
3194        async fn health(s: &Server, token: &str) -> u16 {
3195            let answer = s.client().get(s.url("/health")).bearer_auth(token).send();
3196            answer.await.expect("an answer").status().as_u16()
3197        }
3198
3199        /// Issues a key over HTTP and returns its id and token.
3200        async fn issued(s: &Server, op: &str, body: &str) -> (String, String) {
3201            let path = format!("/admin/tenants/{}/keys", s.tenant);
3202            let answer = call(s, Method::POST, &path, Some(op), Some(body)).await;
3203            assert_eq!(answer.status(), 201, "{body}");
3204            assert_eq!(answer.headers()["cache-control"], "no-store");
3205            let made = json(answer).await;
3206            let id = made["id"].as_str().expect("an id").to_owned();
3207            let token = made["token"].as_str().expect("a token").to_owned();
3208            assert_eq!(made, json!({ "id": id, "token": token }), "nothing else");
3209            assert_eq!(key_id(&token).to_string(), id, "the token names its key");
3210            (id, token)
3211        }
3212
3213        async fn key_list(s: &Server, op: &str) -> (String, Value) {
3214            let path = format!("/admin/tenants/{}/keys", s.tenant);
3215            let answer = call(s, Method::GET, &path, Some(op), None).await;
3216            assert_eq!(answer.status(), 200);
3217            assert_eq!(answer.headers()["content-type"], "application/json");
3218            let text = answer.text().await.expect("text");
3219            let listed = serde_json::from_str(&text).expect("JSON");
3220            (text, listed)
3221        }
3222
3223        #[tokio::test]
3224        async fn a_key_is_issued_with_201_and_its_token_shown_once() {
3225            let s = server().await;
3226            let op = operator(&s);
3227            let body = r#"{"scopes":["read","write"],"label":"ingest"}"#;
3228            let (id, token) = issued(&s, &op, body).await;
3229            assert_eq!(health(&s, &token).await, 200, "the token works");
3230            let (again, other) = issued(&s, &op, body).await;
3231            assert_ne!((&again, &other), (&id, &token), "each key is new");
3232
3233            let (text, listed) = key_list(&s, &op).await;
3234            for token in [&token, &other] {
3235                // vcs_<key id>_<secret>: the secret is base64url, whose
3236                // alphabet includes `_`, so it is all after the second `_`.
3237                let secret = token.splitn(3, '_').nth(2).expect("a secret");
3238                assert_eq!(secret.len(), 43, "the whole 32-byte secret");
3239                assert!(!text.contains(secret), "no token is listed");
3240            }
3241            let first = &listed[0];
3242            assert_eq!(first["id"], id.as_str());
3243            assert_eq!(
3244                first["scopes"],
3245                json!(["write", "read"]),
3246                "in the fixed order"
3247            );
3248            assert_eq!(first["label"], "ingest");
3249            assert_eq!(first["revoked_at"], Value::Null);
3250            let stored = s.tenants.list_keys(Actor::Cli, Some(s.tenant));
3251            let stored = stored.expect("listed")[0].created_at;
3252            let stored = stored.format(&Rfc3339).expect("RFC 3339");
3253            assert_eq!(first["created_at"], stored.as_str());
3254            let fields: Vec<&String> = first.as_object().expect("an object").keys().collect();
3255            assert_eq!(
3256                fields,
3257                [
3258                    "created_at",
3259                    "id",
3260                    "label",
3261                    "revoked_at",
3262                    "scopes",
3263                    "tenant"
3264                ]
3265            );
3266            assert_eq!(first["tenant"], s.tenant.to_string().as_str());
3267
3268            let made = admin_log(&s, "key_create");
3269            assert_eq!(made.len(), 2);
3270            assert_eq!(made[0]["status"], 201);
3271            assert_eq!(made[0]["target"], id.as_str());
3272            let copied = s.tenant_log("key_create");
3273            assert_eq!(copied.len(), 2, "the tenant's log shows it");
3274            assert_eq!(copied[0]["target"], id.as_str());
3275            assert_eq!(admin_log(&s, "key_list")[0]["status"], 200);
3276        }
3277
3278        #[tokio::test]
3279        async fn a_revoked_key_answers_401_and_the_tenants_log_shows_it() {
3280            let s = server().await;
3281            let op = operator(&s);
3282            let (id, token) = issued(&s, &op, r#"{"scopes":["read"],"label":"a"}"#).await;
3283            let (_, kept) = issued(&s, &op, r#"{"scopes":["read"],"label":"b"}"#).await;
3284            let path = format!("/admin/keys/{id}");
3285            for _ in 0..2 {
3286                let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3287                assert_eq!(answer.status(), 204, "again too");
3288            }
3289            assert_eq!(health(&s, &token).await, 401);
3290            assert_eq!(health(&s, &kept).await, 200, "only that key");
3291            let (_, listed) = key_list(&s, &op).await;
3292            assert!(listed[0]["revoked_at"].is_string(), "{listed}");
3293            assert_eq!(listed[1]["revoked_at"], Value::Null);
3294            let revoked = s.tenant_log("key_revoke");
3295            assert_eq!(revoked.len(), 2);
3296            assert!(
3297                revoked
3298                    .iter()
3299                    .all(|r| r["status"] == 204 && r["target"] == id.as_str())
3300            );
3301            assert_eq!(admin_log(&s, "key_revoke").len(), 2);
3302        }
3303
3304        #[tokio::test]
3305        async fn scopes_a_tenant_key_cannot_hold_are_422_and_recorded() {
3306            let s = server().await;
3307            let op = operator(&s);
3308            let path = format!("/admin/tenants/{}/keys", s.tenant);
3309            let scopes = [
3310                "[]",
3311                r#"["admin"]"#,
3312                r#"["read","admin"]"#,
3313                r#"["superuser"]"#,
3314                r#"["read","superuser"]"#,
3315                r#"["Read"]"#,
3316                r#"["read","read"]"#,
3317            ];
3318            for scopes in scopes {
3319                let body = format!(r#"{{"scopes":{scopes},"label":"x"}}"#);
3320                let answer = call(&s, Method::POST, &path, Some(&op), Some(&body)).await;
3321                assert_eq!(answer.status(), 422, "{scopes}");
3322                assert_eq!(problem(answer).await, "bad-scopes", "{scopes}");
3323            }
3324            let refused = admin_log(&s, "key_create");
3325            assert_eq!(refused.len(), scopes.len(), "each recorded once");
3326            assert!(refused.iter().all(|r| r["status"] == 422));
3327            let keys = s.tenants.list_keys(Actor::Cli, Some(s.tenant));
3328            assert_eq!(keys.expect("listed").len(), 0, "no key made");
3329        }
3330
3331        #[tokio::test]
3332        async fn a_key_body_it_does_not_take_is_422_and_recorded() {
3333            let s = server().await;
3334            let op = operator(&s);
3335            let path = format!("/admin/tenants/{}/keys", s.tenant);
3336            let bodies = [
3337                "",
3338                "{}",
3339                r#"{"scopes":["read"]}"#,
3340                r#"{"label":"x"}"#,
3341                r#"{"scopes":"read","label":"x"}"#,
3342                r#"{"scopes":["read"],"label":7}"#,
3343                r#"{"scopes":["read"],"label":"x","tenant":"y"}"#,
3344            ];
3345            for body in bodies {
3346                let answer = call(&s, Method::POST, &path, Some(&op), Some(body)).await;
3347                assert_eq!(answer.status(), 422, "{body}");
3348                assert_eq!(problem(answer).await, "invalid-body", "{body}");
3349            }
3350            let refused = admin_log(&s, "key_create");
3351            assert_eq!(refused.len(), bodies.len());
3352            assert!(refused.iter().all(|r| r["status"] == 422));
3353        }
3354
3355        #[tokio::test]
3356        async fn key_routes_for_no_tenant_or_no_key_are_404_and_recorded() {
3357            let s = server().await;
3358            let op = operator(&s);
3359            let body = Some(r#"{"scopes":["read"],"label":"x"}"#);
3360            let unknown = TenantId([7; 16]).to_string();
3361            for id in [unknown.as_str(), "acme"] {
3362                let path = format!("/admin/tenants/{id}/keys");
3363                let answer = call(&s, Method::POST, &path, Some(&op), body).await;
3364                assert_eq!(answer.status(), 404, "POST {id}");
3365                assert_eq!(problem(answer).await, "unknown-tenant");
3366                let answer = call(&s, Method::GET, &path, Some(&op), None).await;
3367                assert_eq!(answer.status(), 404, "GET {id}");
3368                assert_eq!(problem(answer).await, "unknown-tenant");
3369            }
3370            let unknown_key = KeyId([7; 8]).to_string();
3371            for id in [unknown_key.as_str(), "acme", &unknown_key.to_uppercase()] {
3372                let path = format!("/admin/keys/{id}");
3373                let answer = call(&s, Method::DELETE, &path, Some(&op), None).await;
3374                assert_eq!(answer.status(), 404, "DELETE {id}");
3375                assert_eq!(problem(answer).await, "unknown-key");
3376            }
3377            for (action, count) in [("key_create", 2), ("key_list", 2), ("key_revoke", 3)] {
3378                let refused = admin_log(&s, action);
3379                assert_eq!(refused.len(), count, "{action}");
3380                assert!(refused.iter().all(|r| r["status"] == 404), "{action}");
3381            }
3382        }
3383
3384        #[tokio::test]
3385        async fn requests_during_a_removal_get_503_then_401() {
3386            let s = server().await;
3387            let op = operator(&s);
3388            stored(&s).await;
3389            let uuid = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
3390            let writer = s.key(&[Scope::Write]);
3391            let reader = s.key(&[Scope::Read]);
3392            let legal = s.key(&[Scope::Legal]);
3393            let a4 = std::fs::read(format!(
3394                "{}/../vcon/tests/fixtures/ietf/ab_call_ext_rec.vcon",
3395                env!("CARGO_MANIFEST_DIR")
3396            ))
3397            .expect("A.4");
3398            let requests = || {
3399                let client = s.client();
3400                [
3401                    client
3402                        .post(s.url("/vcons"))
3403                        .bearer_auth(&writer)
3404                        .header("content-type", "application/vcon")
3405                        .body(a4.clone()),
3406                    client
3407                        .get(s.url(&format!("/vcons/{uuid}")))
3408                        .bearer_auth(&reader),
3409                    client
3410                        .get(s.url(&format!("/legal/vcons/{uuid}")))
3411                        .bearer_auth(&legal),
3412                    client.get(s.url("/health")).bearer_auth(&reader),
3413                ]
3414            };
3415
3416            // A request still holding the tenant keeps the removal draining.
3417            let in_flight = s.tenants.store(s.tenant).expect("its store");
3418            let removal = {
3419                let request = s
3420                    .client()
3421                    .delete(s.url(&format!("/admin/tenants/{}?force=true", s.tenant)))
3422                    .bearer_auth(&op);
3423                tokio::spawn(async move { request.send().await.expect("an answer").status() })
3424            };
3425            let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(10);
3426            while s.tenants.store(s.tenant).is_ok() {
3427                assert!(
3428                    tokio::time::Instant::now() < deadline,
3429                    "the removal never began"
3430                );
3431                tokio::time::sleep(std::time::Duration::from_millis(5)).await;
3432            }
3433            for request in requests() {
3434                let answer = request.send().await.expect("an answer");
3435                let url = answer.url().path().to_owned();
3436                assert_eq!(answer.status(), 503, "{url} while removing");
3437                assert_eq!(problem(answer).await, "unavailable", "{url}");
3438            }
3439            assert!(!removal.is_finished(), "still draining");
3440
3441            drop(in_flight);
3442            assert_eq!(removal.await.expect("joined"), 204);
3443            for request in requests() {
3444                let answer = request.send().await.expect("an answer");
3445                let url = answer.url().path().to_owned();
3446                assert_eq!(answer.status(), 401, "{url} once removed");
3447            }
3448        }
3449
3450        #[tokio::test]
3451        async fn a_key_for_a_closing_tenant_is_503() {
3452            let s = server().await;
3453            let op = operator(&s);
3454            s.tenants.close(s.tenant);
3455            let path = format!("/admin/tenants/{}/keys", s.tenant);
3456            let body = Some(r#"{"scopes":["read"],"label":"x"}"#);
3457            let answer = call(&s, Method::POST, &path, Some(&op), body).await;
3458            assert_eq!(answer.status(), 503);
3459            assert_eq!(problem(answer).await, "unavailable");
3460        }
3461
3462        #[tokio::test]
3463        async fn a_body_over_the_limit_is_413() {
3464            let s = server().await;
3465            let op = operator(&s);
3466            let body = format!(r#"{{"name":"{}"}}"#, "x".repeat(super::pipeline::LIMIT));
3467            let answer = call(&s, Method::POST, "/admin/tenants", Some(&op), Some(&body)).await;
3468            assert_eq!(answer.status(), 413);
3469            assert_eq!(problem(answer).await, "too-large");
3470        }
3471    }
3472
3473    mod by_uuid {
3474        use super::pipeline::{Server, problem, server, server_keeping};
3475        use crate::access::{Scope, Scopes};
3476        use crate::audit::Actor;
3477        use crate::ids::TenantId;
3478        use crate::retention::Retention;
3479
3480        const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
3481
3482        fn fixture(name: &str) -> Vec<u8> {
3483            let path = format!(
3484                "{}/../vcon/tests/fixtures/ietf/{name}",
3485                env!("CARGO_MANIFEST_DIR")
3486            );
3487            std::fs::read(&path).unwrap_or_else(|e| panic!("{path}: {e}"))
3488        }
3489
3490        fn gzip(body: &[u8]) -> Vec<u8> {
3491            use std::io::Write;
3492            let mut encoder =
3493                flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
3494            encoder.write_all(body).expect("gzip");
3495            encoder.finish().expect("gzip")
3496        }
3497
3498        /// A key of this tenant's with these scopes.
3499        fn key_in(s: &Server, tenant: TenantId, scopes: &[Scope]) -> String {
3500            s.tenants
3501                .create_key(Actor::Cli, Some(tenant), Scopes::of(scopes), "test")
3502                .expect("a key")
3503                .token
3504        }
3505
3506        async fn get(s: &Server, token: &str, uuid: &str) -> reqwest::Response {
3507            s.client()
3508                .get(s.url(&format!("/vcons/{uuid}")))
3509                .bearer_auth(token)
3510                .send()
3511                .await
3512                .expect("an answer")
3513        }
3514
3515        async fn post(s: &Server, token: &str, content_type: &str, body: Vec<u8>) -> String {
3516            let answer = s
3517                .client()
3518                .post(s.url("/vcons"))
3519                .bearer_auth(token)
3520                .header("content-type", content_type)
3521                .body(body)
3522                .send()
3523                .await
3524                .expect("an answer");
3525            assert_eq!(answer.status(), 201);
3526            answer.headers()["etag"].to_str().expect("ascii").to_owned()
3527        }
3528
3529        /// A.4, A.6 and A.7 share one uuid, so each runs in its own tenant.
3530        #[tokio::test]
3531        async fn the_core_examples_come_back_byte_for_byte_plain_and_gzipped() {
3532            let s = server().await;
3533            let files = [
3534                "ab_call_ext_rec.vcon",
3535                "ab_call_ext_rec_signed.vcon",
3536                "ab_call_ext_rec_encrypted.vcon",
3537            ];
3538            for (n, file) in files.into_iter().enumerate() {
3539                for gzipped in [false, true] {
3540                    let tenant = s
3541                        .tenants
3542                        .create(Actor::Cli, &format!("t{n}{gzipped}"), None)
3543                        .expect("a tenant");
3544                    let token = key_in(&s, tenant, &[Scope::Write, Scope::Read]);
3545                    let plain = fixture(file);
3546                    let (sent, content_type) = if gzipped {
3547                        (gzip(&plain), "application/vcon+gzip")
3548                    } else {
3549                        (plain, "application/vcon")
3550                    };
3551                    let tag = post(&s, &token, content_type, sent.clone()).await;
3552                    let answer = get(&s, &token, UUID).await;
3553                    assert_eq!(answer.status(), 200, "{file} {gzipped}");
3554                    assert_eq!(answer.headers()["content-type"], content_type);
3555                    assert_eq!(answer.headers()["etag"], tag.as_str());
3556                    assert!(answer.headers().get("content-encoding").is_none());
3557                    let got = answer.bytes().await.expect("body");
3558                    assert_eq!(got.as_ref(), sent.as_slice(), "{file} {gzipped}");
3559                }
3560            }
3561        }
3562
3563        #[tokio::test]
3564        async fn a_uuid_is_found_in_either_case() {
3565            let s = server().await;
3566            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read]);
3567            post(
3568                &s,
3569                &token,
3570                "application/vcon",
3571                fixture("ab_call_ext_rec.vcon"),
3572            )
3573            .await;
3574            let answer = get(&s, &token, &UUID.to_uppercase()).await;
3575            assert_eq!(answer.status(), 200);
3576        }
3577
3578        #[tokio::test]
3579        async fn a_uuid_never_stored_is_not_found_and_a_deleted_one_is_gone() {
3580            let s = server().await;
3581            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read]);
3582            let answer = get(&s, &token, UUID).await;
3583            assert_eq!(answer.status(), 404);
3584            assert_eq!(problem(answer).await, "not-found");
3585            post(
3586                &s,
3587                &token,
3588                "application/vcon",
3589                fixture("ab_call_ext_rec.vcon"),
3590            )
3591            .await;
3592            s.tenants
3593                .store(s.tenant)
3594                .expect("its store")
3595                .delete(
3596                    &crate::store::Caller {
3597                        key: crate::ids::KeyId([9; 8]),
3598                    },
3599                    UUID.parse().expect("uuid"),
3600                    crate::retention::Retention::Years(7),
3601                )
3602                .expect("deleted");
3603            let answer = get(&s, &token, UUID).await;
3604            assert_eq!(answer.status(), 410);
3605            assert_eq!(problem(answer).await, "gone");
3606        }
3607
3608        #[tokio::test]
3609        async fn a_path_uuid_not_in_text_form_is_a_bad_request_and_recorded() {
3610            let s = server().await;
3611            let token = key_in(&s, s.tenant, &[Scope::Read]);
3612            let simple = UUID.replace('-', "");
3613            for bad in [
3614                "not-a-uuid".to_owned(),
3615                simple.clone(),
3616                format!("%7B{UUID}%7D"),
3617                format!("urn:uuid:{UUID}"),
3618                format!("{UUID}0"),
3619            ] {
3620                let answer = get(&s, &token, &bad).await;
3621                assert_eq!(answer.status(), 400, "{bad}");
3622                assert_eq!(problem(answer).await, "bad-path-uuid");
3623            }
3624            let refused = s.tenant_log("read");
3625            assert_eq!(refused.len(), 5);
3626            assert_eq!(refused[0]["status"], 400);
3627        }
3628
3629        #[tokio::test]
3630        async fn a_key_without_read_is_forbidden() {
3631            let s = server().await;
3632            let token = key_in(&s, s.tenant, &[Scope::Write]);
3633            let answer = get(&s, &token, UUID).await;
3634            assert_eq!(answer.status(), 403);
3635        }
3636
3637        #[tokio::test]
3638        async fn a_read_whose_record_fails_sends_nothing() {
3639            let s = server().await;
3640            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read]);
3641            post(
3642                &s,
3643                &token,
3644                "application/vcon",
3645                fixture("ab_call_ext_rec.vcon"),
3646            )
3647            .await;
3648            s.tenants.store(s.tenant).expect("its store").break_audit();
3649            let answer = get(&s, &token, UUID).await;
3650            assert_eq!(answer.status(), 503);
3651            assert_eq!(problem(answer).await, "audit-failed");
3652        }
3653
3654        #[tokio::test]
3655        async fn a_version_whose_file_is_missing_is_damaged() {
3656            let s = server().await;
3657            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read]);
3658            let tag = post(
3659                &s,
3660                &token,
3661                "application/vcon",
3662                fixture("ab_call_ext_rec.vcon"),
3663            )
3664            .await;
3665            let object = s
3666                .dir()
3667                .join("tenants")
3668                .join(s.tenant.to_string())
3669                .join("objects")
3670                .join(tag.trim_matches('"'));
3671            std::fs::remove_file(&object).expect("remove the object file");
3672            let answer = get(&s, &token, UUID).await;
3673            assert_eq!(answer.status(), 500);
3674            assert_eq!(problem(answer).await, "damaged");
3675        }
3676
3677        async fn put(
3678            s: &Server,
3679            token: &str,
3680            uuid: &str,
3681            if_match: Option<&str>,
3682            body: Vec<u8>,
3683        ) -> reqwest::Response {
3684            let mut request = s
3685                .client()
3686                .put(s.url(&format!("/vcons/{uuid}")))
3687                .bearer_auth(token)
3688                .header("content-type", "application/vcon")
3689                .body(body);
3690            if let Some(tag) = if_match {
3691                request = request.header("if-match", tag);
3692            }
3693            request.send().await.expect("an answer")
3694        }
3695
3696        /// A writer and reader in the fixture's tenant, with A.4 stored.
3697        async fn stored_a4(s: &Server) -> (String, String) {
3698            let token = key_in(s, s.tenant, &[Scope::Write, Scope::Read]);
3699            let tag = post(
3700                s,
3701                &token,
3702                "application/vcon",
3703                fixture("ab_call_ext_rec.vcon"),
3704            )
3705            .await;
3706            (token, tag)
3707        }
3708
3709        /// An unsigned vCon may be replaced by its signed form, and a signed
3710        /// one is replaced by nothing.
3711        #[tokio::test]
3712        async fn a4_then_a6_by_put_and_a7_after_a6_conflicts() {
3713            let s = server().await;
3714            let (token, a4) = stored_a4(&s).await;
3715            let a6 = fixture("ab_call_ext_rec_signed.vcon");
3716            let answer = put(&s, &token, UUID, Some(&a4), a6.clone()).await;
3717            assert_eq!(answer.status(), 200);
3718            let new_tag = answer.headers()["etag"].to_str().expect("ascii").to_owned();
3719            assert_ne!(new_tag, a4);
3720            let read = get(&s, &token, UUID).await;
3721            assert_eq!(read.headers()["etag"], new_tag.as_str());
3722            assert_eq!(read.bytes().await.expect("body").as_ref(), a6.as_slice());
3723
3724            let a7 = fixture("ab_call_ext_rec_encrypted.vcon");
3725            let answer = put(&s, &token, UUID, Some(&new_tag), a7).await;
3726            assert_eq!(answer.status(), 409);
3727            assert_eq!(problem(answer).await, "immutable");
3728        }
3729
3730        #[tokio::test]
3731        async fn a_put_without_if_match_is_refused_and_recorded() {
3732            let s = server().await;
3733            let (token, _) = stored_a4(&s).await;
3734            let a6 = fixture("ab_call_ext_rec_signed.vcon");
3735            let answer = put(&s, &token, UUID, None, a6).await;
3736            assert_eq!(answer.status(), 428);
3737            assert_eq!(problem(answer).await, "precondition-required");
3738            assert_eq!(s.tenant_log("replace")[0]["status"], 428);
3739        }
3740
3741        #[tokio::test]
3742        async fn an_if_match_that_does_not_name_the_current_version_fails() {
3743            let s = server().await;
3744            let (token, a4) = stored_a4(&s).await;
3745            let a6 = fixture("ab_call_ext_rec_signed.vcon");
3746            let weak = format!("W/{a4}");
3747            let listed = format!("{a4}, \"00000000000000000000000000000000\"");
3748            let stale = format!("\"{}\"", crate::ids::VersionId([7; 16]));
3749            for tag in [
3750                "*",
3751                weak.as_str(),
3752                listed.as_str(),
3753                "garbage",
3754                stale.as_str(),
3755            ] {
3756                let answer = put(&s, &token, UUID, Some(tag), a6.clone()).await;
3757                assert_eq!(answer.status(), 412, "{tag}");
3758                assert_eq!(problem(answer).await, "precondition-failed");
3759            }
3760            let current = get(&s, &token, UUID).await;
3761            assert_eq!(current.headers()["etag"], a4.as_str(), "nothing replaced");
3762        }
3763
3764        #[tokio::test]
3765        async fn a_body_naming_another_uuid_is_unprocessable() {
3766            let s = server().await;
3767            let (token, a4) = stored_a4(&s).await;
3768            let (_, other) =
3769                crate::store::testing::vcon("01a07da8-c2bb-83e5-b9a2-279e0d16bc47", "x");
3770            let answer = put(&s, &token, UUID, Some(&a4), other).await;
3771            assert_eq!(answer.status(), 422);
3772            assert_eq!(problem(answer).await, "path-uuid-mismatch");
3773            assert_eq!(s.tenant_log("replace")[0]["status"], 422);
3774        }
3775
3776        #[tokio::test]
3777        async fn a_put_to_a_uuid_never_stored_or_deleted_or_badly_written() {
3778            let s = server().await;
3779            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read]);
3780            let tag = format!("\"{}\"", crate::ids::VersionId([7; 16]));
3781            let a4 = fixture("ab_call_ext_rec.vcon");
3782            let answer = put(&s, &token, UUID, Some(&tag), a4.clone()).await;
3783            assert_eq!(answer.status(), 404);
3784            assert_eq!(problem(answer).await, "not-found");
3785
3786            let answer = put(&s, &token, "not-a-uuid", Some(&tag), a4.clone()).await;
3787            assert_eq!(answer.status(), 400);
3788            assert_eq!(problem(answer).await, "bad-path-uuid");
3789
3790            let a4_tag = post(&s, &token, "application/vcon", a4.clone()).await;
3791            s.tenants
3792                .store(s.tenant)
3793                .expect("its store")
3794                .delete(
3795                    &crate::store::Caller {
3796                        key: crate::ids::KeyId([9; 8]),
3797                    },
3798                    UUID.parse().expect("uuid"),
3799                    crate::retention::Retention::Years(7),
3800                )
3801                .expect("deleted");
3802            let answer = put(&s, &token, UUID, Some(&a4_tag), a4).await;
3803            assert_eq!(answer.status(), 410);
3804            assert_eq!(problem(answer).await, "gone");
3805        }
3806
3807        #[tokio::test]
3808        async fn a_put_needs_write_and_a_vcon_media_type() {
3809            let s = server().await;
3810            let (_, a4) = stored_a4(&s).await;
3811            let reader = key_in(&s, s.tenant, &[Scope::Read]);
3812            let a6 = fixture("ab_call_ext_rec_signed.vcon");
3813            let answer = put(&s, &reader, UUID, Some(&a4), a6.clone()).await;
3814            assert_eq!(answer.status(), 403);
3815            let writer = key_in(&s, s.tenant, &[Scope::Write]);
3816            let answer = s
3817                .client()
3818                .put(s.url(&format!("/vcons/{UUID}")))
3819                .bearer_auth(&writer)
3820                .header("content-type", "text/plain")
3821                .header("if-match", &a4)
3822                .body(a6)
3823                .send()
3824                .await
3825                .expect("an answer");
3826            assert_eq!(answer.status(), 415);
3827        }
3828
3829        async fn delete(s: &Server, token: &str, uuid: &str) -> reqwest::Response {
3830            s.client()
3831                .delete(s.url(&format!("/vcons/{uuid}")))
3832                .bearer_auth(token)
3833                .send()
3834                .await
3835                .expect("an answer")
3836        }
3837
3838        #[tokio::test]
3839        async fn a_delete_answers_204_again_and_again() {
3840            let s = server().await;
3841            let token = key_in(&s, s.tenant, &[Scope::Write, Scope::Read, Scope::Delete]);
3842            post(
3843                &s,
3844                &token,
3845                "application/vcon",
3846                fixture("ab_call_ext_rec.vcon"),
3847            )
3848            .await;
3849            for _ in 0..2 {
3850                let answer = delete(&s, &token, UUID).await;
3851                assert_eq!(answer.status(), 204);
3852                assert!(answer.bytes().await.expect("body").is_empty());
3853            }
3854            assert_eq!(get(&s, &token, UUID).await.status(), 410);
3855            let deletes = s.tenant_log("delete");
3856            assert_eq!(deletes.len(), 2, "Store records each");
3857        }
3858
3859        #[tokio::test]
3860        async fn a_delete_of_a_uuid_never_stored_or_badly_written() {
3861            let s = server().await;
3862            let token = key_in(&s, s.tenant, &[Scope::Delete]);
3863            let answer = delete(&s, &token, UUID).await;
3864            assert_eq!(answer.status(), 404);
3865            assert_eq!(problem(answer).await, "not-found");
3866            let answer = delete(&s, &token, "not-a-uuid").await;
3867            assert_eq!(answer.status(), 400);
3868            assert_eq!(problem(answer).await, "bad-path-uuid");
3869            assert_eq!(
3870                s.tenant_log("delete").last().expect("recorded")["status"],
3871                400
3872            );
3873        }
3874
3875        #[tokio::test]
3876        async fn a_delete_needs_the_delete_scope_and_its_record() {
3877            let s = server().await;
3878            let (writer, _) = stored_a4(&s).await;
3879            let answer = delete(&s, &writer, UUID).await;
3880            assert_eq!(answer.status(), 403);
3881            let deleter = key_in(&s, s.tenant, &[Scope::Delete]);
3882            s.tenants.store(s.tenant).expect("its store").break_audit();
3883            let answer = delete(&s, &deleter, UUID).await;
3884            assert_eq!(answer.status(), 503);
3885            assert_eq!(problem(answer).await, "audit-failed");
3886        }
3887
3888        /// The purge records of `s`'s tenant whose actor is `token`'s key.
3889        fn purged_by(s: &Server, token: &str) -> usize {
3890            s.tenant_log("purge")
3891                .iter()
3892                .filter(|r| r["actor"] == token[4..20])
3893                .count()
3894        }
3895
3896        #[tokio::test]
3897        async fn a_delete_erases_at_once_under_the_tenants_zero_period() {
3898            let s = server().await;
3899            s.tenants
3900                .set_retention(Actor::Cli, s.tenant, Some(Retention::Days(0)))
3901                .expect("set");
3902            let (_, _) = stored_a4(&s).await;
3903            let deleter = key_in(&s, s.tenant, &[Scope::Delete]);
3904            assert_eq!(delete(&s, &deleter, UUID).await.status(), 204);
3905            assert_eq!(purged_by(&s, &deleter), 1);
3906        }
3907
3908        #[tokio::test]
3909        async fn a_delete_uses_the_server_default_when_the_tenant_sets_none() {
3910            let s = server_keeping(Retention::Days(0)).await;
3911            let (_, _) = stored_a4(&s).await;
3912            let deleter = key_in(&s, s.tenant, &[Scope::Delete]);
3913            assert_eq!(delete(&s, &deleter, UUID).await.status(), 204);
3914            assert_eq!(purged_by(&s, &deleter), 1);
3915        }
3916
3917        #[tokio::test]
3918        async fn a_delete_keeps_a_young_vcon_for_legal_reads() {
3919            let s = server().await;
3920            let (_, _) = stored_a4(&s).await;
3921            let deleter = key_in(&s, s.tenant, &[Scope::Delete]);
3922            assert_eq!(delete(&s, &deleter, UUID).await.status(), 204);
3923            assert_eq!(purged_by(&s, &deleter), 0);
3924        }
3925
3926        #[tokio::test]
3927        async fn a_delete_whose_period_cannot_be_read_deletes_nothing() {
3928            let s = server().await;
3929            let (_, _) = stored_a4(&s).await;
3930            s.tenants
3931                .conn_for_test()
3932                .execute(
3933                    "UPDATE tenant SET retention = '7w' WHERE id = ?1",
3934                    [s.tenant.0],
3935                )
3936                .expect("damaged");
3937            let token = key_in(&s, s.tenant, &[Scope::Delete, Scope::Read]);
3938            let answer = delete(&s, &token, UUID).await;
3939            assert_eq!(answer.status(), 503);
3940            assert_eq!(problem(answer).await, "unavailable");
3941            assert_eq!(get(&s, &token, UUID).await.status(), 200);
3942            assert_eq!(
3943                s.tenant_log("delete").last().expect("recorded")["status"],
3944                503
3945            );
3946        }
3947
3948        async fn health(s: &Server, token: Option<&str>) -> reqwest::Response {
3949            let mut request = s.client().get(s.url("/health"));
3950            if let Some(token) = token {
3951                request = request.bearer_auth(token);
3952            }
3953            request.send().await.expect("an answer")
3954        }
3955
3956        #[tokio::test]
3957        async fn health_needs_a_key_of_any_scope_and_is_counted() {
3958            let s = server().await;
3959            assert_eq!(health(&s, None).await.status(), 401);
3960            let reader = key_in(&s, s.tenant, &[Scope::Read]);
3961            for _ in 0..2 {
3962                let answer = health(&s, Some(&reader)).await;
3963                assert_eq!(answer.status(), 200);
3964                assert_eq!(answer.headers()["content-type"], "application/json");
3965            }
3966            let counted = s.tenant_log("health");
3967            assert_eq!(counted.len(), 1, "one record a minute");
3968            assert_eq!(counted[0]["count"], 2);
3969            assert_eq!(counted[0]["status"], 200);
3970
3971            let operator = s
3972                .tenants
3973                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
3974                .expect("made")
3975                .token;
3976            assert_eq!(health(&s, Some(&operator)).await.status(), 200);
3977            let admin: Vec<_> = s
3978                .tenants
3979                .admin_records()
3980                .into_iter()
3981                .filter(|r| r["action"] == "health")
3982                .collect();
3983            assert_eq!(admin.len(), 1);
3984            assert_eq!(admin[0]["actor"], operator[4..20]);
3985        }
3986
3987        #[tokio::test]
3988        async fn health_is_unavailable_when_the_catalog_does_not_answer() {
3989            let s = server().await;
3990            let reader = key_in(&s, s.tenant, &[Scope::Read]);
3991            s.tenants
3992                .conn_for_test()
3993                .execute_batch("ALTER TABLE tenant RENAME TO gone")
3994                .expect("lose the table");
3995            let answer = health(&s, Some(&reader)).await;
3996            assert_eq!(answer.status(), 503);
3997            assert_eq!(problem(answer).await, "unavailable");
3998        }
3999
4000        #[tokio::test]
4001        async fn health_whose_count_fails_is_unavailable() {
4002            let s = server().await;
4003            let operator = s
4004                .tenants
4005                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4006                .expect("made")
4007                .token;
4008            s.tenants.break_audit();
4009            let answer = health(&s, Some(&operator)).await;
4010            assert_eq!(answer.status(), 503);
4011            assert_eq!(problem(answer).await, "audit-failed");
4012        }
4013    }
4014
4015    /// `GET /openapi.json`: the description, to any valid key.
4016    mod openapi_route {
4017        use super::pipeline::{Server, problem, server};
4018        use crate::access::{Scope, Scopes};
4019        use crate::audit::Actor;
4020
4021        async fn get(s: &Server, token: Option<&str>) -> reqwest::Response {
4022            let mut request = s.client().get(s.url("/openapi.json"));
4023            if let Some(token) = token {
4024                request = request.bearer_auth(token);
4025            }
4026            request.send().await.expect("an answer")
4027        }
4028
4029        fn admin_openapi_records(s: &Server) -> usize {
4030            let mut out = Vec::new();
4031            s.tenants.export_admin(None, &mut out).expect("export");
4032            String::from_utf8(out)
4033                .expect("UTF-8")
4034                .lines()
4035                .filter(|l| l.contains("\"action\":\"openapi\""))
4036                .count()
4037        }
4038
4039        #[tokio::test]
4040        async fn any_valid_key_gets_the_description_as_compiled_and_it_is_recorded() {
4041            let s = server().await;
4042            let operator = s
4043                .tenants
4044                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4045                .expect("an operator key")
4046                .token;
4047            for token in [s.key(&[Scope::Read]), s.key(&[Scope::Write]), operator] {
4048                let answer = get(&s, Some(&token)).await;
4049                assert_eq!(answer.status(), 200);
4050                assert_eq!(answer.headers()["content-type"], "application/json");
4051                assert_eq!(answer.text().await.expect("text"), super::super::OPENAPI);
4052            }
4053            let recorded = s.tenant_log("openapi");
4054            assert_eq!(recorded.len(), 2, "the two tenant keys");
4055            assert!(recorded.iter().all(|r| r["status"] == 200));
4056            assert_eq!(admin_openapi_records(&s), 1, "the operator key");
4057        }
4058
4059        #[tokio::test]
4060        async fn no_key_is_401_and_a_read_that_cannot_be_recorded_503() {
4061            let s = server().await;
4062            let answer = get(&s, None).await;
4063            assert_eq!(answer.status(), 401);
4064            assert_eq!(problem(answer).await, "unauthorized");
4065            let reader = s.key(&[Scope::Read]);
4066            s.tenants.store(s.tenant).expect("its store").break_audit();
4067            let answer = get(&s, Some(&reader)).await;
4068            assert_eq!(answer.status(), 503);
4069            assert_eq!(problem(answer).await, "audit-failed");
4070        }
4071
4072        #[tokio::test]
4073        async fn a_key_of_a_closing_tenant_is_503() {
4074            let s = server().await;
4075            let reader = s.key(&[Scope::Read]);
4076            s.tenants.close(s.tenant);
4077            let answer = get(&s, Some(&reader)).await;
4078            assert_eq!(answer.status(), 503);
4079            assert_eq!(problem(answer).await, "unavailable");
4080        }
4081    }
4082
4083    /// `GET /audit` and `GET /admin/audit`: a log a page at a time, as the
4084    /// command line exports it.
4085    mod audit_routes {
4086        use super::pipeline::{Server, problem, server};
4087        use crate::access::{Scope, Scopes};
4088        use crate::audit::Actor;
4089        use crate::store::testing::vcon;
4090
4091        fn operator(s: &Server) -> String {
4092            s.tenants
4093                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4094                .expect("an operator key")
4095                .token
4096        }
4097
4098        async fn get(s: &Server, path: &str, token: Option<&str>) -> reqwest::Response {
4099            let mut request = s.client().get(s.url(path));
4100            if let Some(token) = token {
4101                request = request.bearer_auth(token);
4102            }
4103            request.send().await.expect("an answer")
4104        }
4105
4106        async fn body(answer: reqwest::Response) -> Vec<u8> {
4107            assert_eq!(answer.status(), 200);
4108            assert_eq!(answer.headers()["content-type"], "application/jsonl");
4109            answer.bytes().await.expect("body").to_vec()
4110        }
4111
4112        /// The tenant's log as `audit export --tenant` writes it.
4113        fn tenant_export(s: &Server, since: Option<i64>) -> Vec<u8> {
4114            let mut out = Vec::new();
4115            s.tenants
4116                .store(s.tenant)
4117                .expect("its store")
4118                .export(since, &mut out)
4119                .expect("export");
4120            out
4121        }
4122
4123        fn admin_export(s: &Server) -> Vec<u8> {
4124            let mut out = Vec::new();
4125            s.tenants.export_admin(None, &mut out).expect("export");
4126            out
4127        }
4128
4129        async fn three_stored(s: &Server) {
4130            let writer = s.key(&[Scope::Write]);
4131            for n in 1..=3 {
4132                let answer = s
4133                    .client()
4134                    .post(s.url("/vcons"))
4135                    .bearer_auth(&writer)
4136                    .header("content-type", "application/vcon")
4137                    .body(vcon(&format!("01a07da8-c2bb-83e5-b9a2-{n:012x}"), "x").1)
4138                    .send()
4139                    .await
4140                    .expect("an answer");
4141                assert_eq!(answer.status(), 201);
4142            }
4143        }
4144
4145        #[tokio::test]
4146        async fn the_tenant_log_is_its_export_byte_for_byte_a_page_at_a_time() {
4147            let s = server().await;
4148            three_stored(&s).await;
4149            let auditor = s.key(&[Scope::Audit]);
4150            let before = tenant_export(&s, None);
4151            assert_eq!(body(get(&s, "/audit", Some(&auditor)).await).await, before);
4152            let lines: Vec<&[u8]> = before.split_inclusive(|b| *b == b'\n').collect();
4153            assert_eq!(
4154                body(get(&s, "/audit?after=1&limit=2", Some(&auditor)).await).await,
4155                lines[1..3].concat()
4156            );
4157            // The reads are recorded, each after its own page.
4158            let reads = s.tenant_log("audit_read");
4159            assert_eq!(reads.len(), 2);
4160            assert!(reads.iter().all(|r| r["status"] == 200));
4161            let after = 3;
4162            let page = body(get(&s, &format!("/audit?after={after}"), Some(&auditor)).await).await;
4163            assert_eq!(page, tenant_export(&s, Some(after))[..page.len()]);
4164        }
4165
4166        #[tokio::test]
4167        async fn the_admin_log_is_its_export_byte_for_byte() {
4168            let s = server().await;
4169            let op = operator(&s);
4170            let before = admin_export(&s);
4171            assert!(!before.is_empty(), "the tenant's creation, at least");
4172            assert_eq!(body(get(&s, "/admin/audit", Some(&op)).await).await, before);
4173            let lines: Vec<&[u8]> = before.split_inclusive(|b| *b == b'\n').collect();
4174            assert_eq!(
4175                body(get(&s, "/admin/audit?limit=1", Some(&op)).await).await,
4176                lines[0]
4177            );
4178            let mut recorded = Vec::new();
4179            s.tenants.export_admin(None, &mut recorded).expect("export");
4180            let reads = String::from_utf8(recorded)
4181                .expect("UTF-8")
4182                .lines()
4183                .filter(|l| l.contains("\"action\":\"audit_read\""))
4184                .count();
4185            assert_eq!(reads, 2);
4186        }
4187
4188        #[tokio::test]
4189        async fn the_tenant_log_needs_a_tenant_key_with_audit() {
4190            let s = server().await;
4191            assert_eq!(get(&s, "/audit", None).await.status(), 401);
4192            for scopes in [
4193                &[Scope::Read][..],
4194                &[Scope::Legal],
4195                &[Scope::Write, Scope::Delete],
4196            ] {
4197                let answer = get(&s, "/audit", Some(&s.key(scopes))).await;
4198                assert_eq!(answer.status(), 403, "{scopes:?}");
4199                assert_eq!(problem(answer).await, "forbidden");
4200            }
4201            let answer = get(&s, "/audit", Some(&operator(&s))).await;
4202            assert_eq!(answer.status(), 403, "an operator key");
4203            let refused = s.tenant_log("audit_read");
4204            assert_eq!(refused.len(), 3, "the tenant keys' refusals");
4205            assert!(refused.iter().all(|r| r["status"] == 403));
4206        }
4207
4208        #[tokio::test]
4209        async fn the_admin_log_needs_an_operator_key() {
4210            let s = server().await;
4211            assert_eq!(get(&s, "/admin/audit", None).await.status(), 401);
4212            let every = s.key(&[
4213                Scope::Write,
4214                Scope::Read,
4215                Scope::Delete,
4216                Scope::Legal,
4217                Scope::Audit,
4218            ]);
4219            let answer = get(&s, "/admin/audit", Some(&every)).await;
4220            assert_eq!(answer.status(), 403);
4221            assert_eq!(problem(answer).await, "forbidden");
4222            assert_eq!(s.tenant_log("audit_read").len(), 1, "recorded");
4223        }
4224
4225        #[tokio::test]
4226        async fn a_query_the_routes_do_not_take_is_a_bad_query_and_recorded() {
4227            let s = server().await;
4228            let auditor = s.key(&[Scope::Audit]);
4229            let op = operator(&s);
4230            let bad = [
4231                "?since=3",
4232                "?after=-1",
4233                "?limit=0",
4234                "?limit=10001",
4235                "?after=1&after=2",
4236            ];
4237            for query in bad {
4238                for (path, token) in [("/audit", &auditor), ("/admin/audit", &op)] {
4239                    let answer = get(&s, &format!("{path}{query}"), Some(token)).await;
4240                    assert_eq!(answer.status(), 400, "{path}{query}");
4241                    assert_eq!(problem(answer).await, "bad-query");
4242                }
4243            }
4244            let refused = s.tenant_log("audit_read");
4245            assert_eq!(refused.len(), bad.len());
4246            assert!(refused.iter().all(|r| r["status"] == 400));
4247            let mut admin = Vec::new();
4248            s.tenants.export_admin(None, &mut admin).expect("export");
4249            let admin_refused = String::from_utf8(admin)
4250                .expect("UTF-8")
4251                .lines()
4252                .filter(|l| l.contains("\"action\":\"audit_read\"") && l.contains("\"status\":400"))
4253                .count();
4254            assert_eq!(admin_refused, bad.len());
4255        }
4256
4257        #[tokio::test]
4258        async fn a_read_that_cannot_be_recorded_is_503() {
4259            let s = server().await;
4260            let auditor = s.key(&[Scope::Audit]);
4261            let op = operator(&s);
4262            s.tenants.store(s.tenant).expect("its store").break_audit();
4263            let answer = get(&s, "/audit", Some(&auditor)).await;
4264            assert_eq!(answer.status(), 503);
4265            assert_eq!(problem(answer).await, "audit-failed");
4266            s.tenants.break_audit();
4267            let answer = get(&s, "/admin/audit", Some(&op)).await;
4268            assert_eq!(answer.status(), 503);
4269            assert_eq!(problem(answer).await, "audit-failed");
4270        }
4271    }
4272
4273    /// `GET /vcons`: a tenant's live vCons, a page at a time.
4274    mod list {
4275        use std::collections::BTreeSet;
4276
4277        use serde_json::Value;
4278
4279        use super::pipeline::{Server, problem, server};
4280        use crate::access::{Scope, Scopes};
4281        use crate::audit::Actor;
4282        use crate::ids::TenantId;
4283        use crate::store::testing::vcon;
4284
4285        fn uuid(n: u32) -> String {
4286            format!("01a07da8-c2bb-83e5-b9a2-{n:012x}")
4287        }
4288
4289        fn key_of(s: &Server, tenant: TenantId, scopes: &[Scope]) -> String {
4290            s.tenants
4291                .create_key(Actor::Cli, Some(tenant), Scopes::of(scopes), "test")
4292                .expect("a key")
4293                .token
4294        }
4295
4296        /// Stores vCon `n` with `writer` and returns its tag, unquoted.
4297        async fn stored(s: &Server, writer: &str, n: u32) -> String {
4298            let answer = s
4299                .client()
4300                .post(s.url("/vcons"))
4301                .bearer_auth(writer)
4302                .header("content-type", "application/vcon")
4303                .body(vcon(&uuid(n), "listed").1)
4304                .send()
4305                .await
4306                .expect("an answer");
4307            assert_eq!(answer.status(), 201);
4308            answer.headers()["etag"]
4309                .to_str()
4310                .expect("text")
4311                .trim_matches('"')
4312                .to_owned()
4313        }
4314
4315        async fn list(s: &Server, token: Option<&str>, query: &str) -> reqwest::Response {
4316            let mut request = s.client().get(s.url(&format!("/vcons{query}")));
4317            if let Some(token) = token {
4318                request = request.bearer_auth(token);
4319            }
4320            request.send().await.expect("an answer")
4321        }
4322
4323        async fn page(s: &Server, reader: &str, query: &str) -> Value {
4324            let answer = list(s, Some(reader), query).await;
4325            assert_eq!(answer.status(), 200, "{query}");
4326            assert_eq!(answer.headers()["content-type"], "application/json");
4327            serde_json::from_str(&answer.text().await.expect("text")).expect("JSON")
4328        }
4329
4330        fn uuids(page: &Value) -> Vec<String> {
4331            page["vcons"]
4332                .as_array()
4333                .expect("a list")
4334                .iter()
4335                .map(|v| v["uuid"].as_str().expect("a uuid").to_owned())
4336                .collect()
4337        }
4338
4339        /// Every vCon the reader's pages list, following `next`.
4340        async fn every_page(s: &Server, reader: &str, limit: u32) -> Vec<String> {
4341            let mut seen = Vec::new();
4342            let mut query = format!("?limit={limit}");
4343            for _ in 0..100 {
4344                let page = page(s, reader, &query).await;
4345                seen.extend(uuids(&page));
4346                match page["next"].as_str() {
4347                    Some(cursor) => query = format!("?limit={limit}&cursor={cursor}"),
4348                    None => return seen,
4349                }
4350            }
4351            panic!("the cursors end")
4352        }
4353
4354        #[tokio::test]
4355        async fn the_list_is_newest_first_with_each_tag_and_time() {
4356            let s = server().await;
4357            let writer = s.key(&[Scope::Write]);
4358            let mut tags = Vec::new();
4359            for n in 1..=3 {
4360                tags.push(stored(&s, &writer, n).await);
4361            }
4362            let reader = s.key(&[Scope::Read]);
4363            let page = page(&s, &reader, "").await;
4364            assert_eq!(uuids(&page), [3, 2, 1].map(uuid));
4365            assert_eq!(page["next"], Value::Null);
4366            for (listed, tag) in page["vcons"]
4367                .as_array()
4368                .expect("a list")
4369                .iter()
4370                .zip(tags.iter().rev())
4371            {
4372                assert_eq!(listed["version"], tag.as_str());
4373                assert_eq!(listed["stored_at"], listed["updated_at"]);
4374                let at = listed["stored_at"].as_str().expect("a time");
4375                assert!(at.ends_with('Z'), "{at}");
4376            }
4377            let recorded = s.tenant_log("list");
4378            assert_eq!(recorded.len(), 1);
4379            assert_eq!(recorded[0]["status"], 200);
4380        }
4381
4382        #[tokio::test]
4383        async fn paging_returns_each_vcon_once_and_a_deleted_one_never() {
4384            let s = server().await;
4385            let writer = s.key(&[Scope::Write]);
4386            for n in 1..=7 {
4387                stored(&s, &writer, n).await;
4388            }
4389            let deleted = s
4390                .client()
4391                .delete(s.url(&format!("/vcons/{}", uuid(4))))
4392                .bearer_auth(s.key(&[Scope::Delete]))
4393                .send()
4394                .await
4395                .expect("an answer");
4396            assert_eq!(deleted.status(), 204);
4397            let reader = s.key(&[Scope::Read]);
4398            for limit in [1, 2, 3, 6, 7, 100] {
4399                assert_eq!(
4400                    every_page(&s, &reader, limit).await,
4401                    [7, 6, 5, 3, 2, 1].map(uuid),
4402                    "limit {limit}"
4403                );
4404            }
4405        }
4406
4407        #[tokio::test]
4408        async fn a_tenant_lists_its_own_vcons_never_another_tenants() {
4409            let s = server().await;
4410            let beta = s.tenants.create(Actor::Cli, "beta", None).expect("made");
4411            let acme_writer = s.key(&[Scope::Write]);
4412            let beta_writer = key_of(&s, beta, &[Scope::Write]);
4413            for n in [1, 3, 5] {
4414                stored(&s, &acme_writer, n).await;
4415            }
4416            // vCon 3 is held by both tenants, each its own.
4417            for n in [2, 3, 4] {
4418                stored(&s, &beta_writer, n).await;
4419            }
4420            let acme = s.key(&[Scope::Read]);
4421            let beta_reader = key_of(&s, beta, &[Scope::Read]);
4422            assert_eq!(every_page(&s, &acme, 1).await, [5, 3, 1].map(uuid));
4423            assert_eq!(every_page(&s, &beta_reader, 1).await, [4, 3, 2].map(uuid));
4424            // A cursor from the other tenant's list only sets a place.
4425            let beta_page = page(&s, &beta_reader, "?limit=1").await;
4426            let cursor = beta_page["next"].as_str().expect("a cursor");
4427            let mixed = page(&s, &acme, &format!("?cursor={cursor}")).await;
4428            let acme_uuids: BTreeSet<String> = [5, 3, 1].map(uuid).into_iter().collect();
4429            assert!(uuids(&mixed).iter().all(|u| acme_uuids.contains(u)));
4430        }
4431
4432        #[tokio::test]
4433        async fn since_and_until_bound_the_first_store_time() {
4434            let s = server().await;
4435            let writer = s.key(&[Scope::Write]);
4436            stored(&s, &writer, 1).await;
4437            let reader = s.key(&[Scope::Read]);
4438            let first = page(&s, &reader, "").await;
4439            let at = first["vcons"][0]["stored_at"]
4440                .as_str()
4441                .expect("a time")
4442                .to_owned();
4443            let at = at.replace(':', "%3A");
4444            assert_eq!(
4445                uuids(&page(&s, &reader, &format!("?since={at}")).await),
4446                vec![uuid(1)]
4447            );
4448            assert_eq!(
4449                uuids(&page(&s, &reader, &format!("?until={at}")).await),
4450                Vec::<String>::new()
4451            );
4452        }
4453
4454        #[tokio::test]
4455        async fn a_query_the_route_does_not_take_is_a_bad_query_and_recorded() {
4456            let s = server().await;
4457            let reader = s.key(&[Scope::Read]);
4458            let bad = [
4459                "?offset=2",
4460                "?limit=0",
4461                "?limit=1001",
4462                "?limit=ten",
4463                "?limit=1&limit=2",
4464                "?cursor=nope",
4465                "?since=yesterday",
4466                "?until=2026-10-25",
4467            ];
4468            for query in bad {
4469                let answer = list(&s, Some(&reader), query).await;
4470                assert_eq!(answer.status(), 400, "{query}");
4471                assert_eq!(problem(answer).await, "bad-query", "{query}");
4472            }
4473            let recorded = s.tenant_log("list");
4474            assert_eq!(recorded.len(), bad.len());
4475            assert!(recorded.iter().all(|r| r["status"] == 400));
4476        }
4477
4478        #[tokio::test]
4479        async fn the_list_needs_a_tenant_key_with_read() {
4480            let s = server().await;
4481            let answer = list(&s, None, "").await;
4482            assert_eq!(answer.status(), 401);
4483            for scopes in [&[Scope::Write][..], &[Scope::Delete], &[Scope::Legal]] {
4484                let answer = list(&s, Some(&s.key(scopes)), "").await;
4485                assert_eq!(answer.status(), 403, "{scopes:?}");
4486                assert_eq!(problem(answer).await, "forbidden");
4487            }
4488            let operator = s
4489                .tenants
4490                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4491                .expect("an operator key")
4492                .token;
4493            let answer = list(&s, Some(&operator), "").await;
4494            assert_eq!(answer.status(), 403);
4495            let recorded = s.tenant_log("list");
4496            assert_eq!(recorded.len(), 3, "the tenant keys' refusals");
4497            assert!(recorded.iter().all(|r| r["status"] == 403));
4498        }
4499
4500        #[tokio::test]
4501        async fn damage_is_500_and_a_list_that_cannot_be_recorded_503() {
4502            let s = server().await;
4503            let writer = s.key(&[Scope::Write]);
4504            stored(&s, &writer, 1).await;
4505            let reader = s.key(&[Scope::Read]);
4506            let store = s.tenants.store(s.tenant).expect("its store");
4507            store
4508                .conn_for_test()
4509                .execute("DELETE FROM version", [])
4510                .expect("damaged");
4511            let answer = list(&s, Some(&reader), "").await;
4512            assert_eq!(answer.status(), 500);
4513            assert_eq!(problem(answer).await, "damaged");
4514            store.break_audit();
4515            let answer = list(&s, Some(&reader), "").await;
4516            assert_eq!(answer.status(), 503);
4517            assert_eq!(problem(answer).await, "audit-failed");
4518        }
4519    }
4520
4521    /// `GET /key`: a key learns its own id, tenant and scopes, and the
4522    /// request is recorded before the answer goes out.
4523    mod key_info {
4524        use serde_json::Value;
4525
4526        use super::pipeline::{Server, problem, server};
4527        use crate::access::{self, Scope, Scopes};
4528        use crate::audit::Actor;
4529
4530        async fn key(s: &Server, token: Option<&str>) -> reqwest::Response {
4531            let mut request = s.client().get(s.url("/key"));
4532            if let Some(token) = token {
4533                request = request.bearer_auth(token);
4534            }
4535            request.send().await.expect("an answer")
4536        }
4537
4538        /// What the key may do, and nothing else: its id, tenant and scopes.
4539        fn own(token: &str, tenant: Option<String>, scopes: &[&str]) -> Value {
4540            let id = access::parse(token).expect("a token").to_string();
4541            serde_json::json!({ "id": id, "tenant": tenant, "scopes": scopes })
4542        }
4543
4544        #[tokio::test]
4545        async fn a_tenant_key_reads_its_own_record_and_the_read_is_recorded() {
4546            let s = server().await;
4547            let token = s.key(&[Scope::Read, Scope::Delete]);
4548            let answer = key(&s, Some(&token)).await;
4549            assert_eq!(answer.status(), 200);
4550            assert_eq!(answer.headers()["content-type"], "application/json");
4551            assert_eq!(answer.headers()["cache-control"], "no-store");
4552            let body: Value =
4553                serde_json::from_slice(&answer.bytes().await.expect("body")).expect("JSON");
4554            let tenant = Some(s.tenant.to_string());
4555            assert_eq!(body, own(&token, tenant, &["read", "delete"]));
4556            let records = s.tenant_log("key_info");
4557            assert_eq!(records.len(), 1, "one record per request");
4558            assert_eq!(records[0]["status"], 200);
4559            assert_eq!(records[0]["actor"], token[4..20]);
4560            assert_eq!(records[0]["target"], token[4..20], "the key read");
4561        }
4562
4563        #[tokio::test]
4564        async fn an_operator_key_reads_its_record_in_the_admin_log() {
4565            let s = server().await;
4566            let token = s
4567                .tenants
4568                .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
4569                .expect("made")
4570                .token;
4571            let answer = key(&s, Some(&token)).await;
4572            assert_eq!(answer.status(), 200);
4573            let body: Value =
4574                serde_json::from_slice(&answer.bytes().await.expect("body")).expect("JSON");
4575            assert_eq!(body, own(&token, None, &["admin"]));
4576            let records: Vec<_> = s
4577                .tenants
4578                .admin_records()
4579                .into_iter()
4580                .filter(|r| r["action"] == "key_info")
4581                .collect();
4582            assert_eq!(records.len(), 1);
4583            assert_eq!(records[0]["status"], 200);
4584            assert_eq!(records[0]["target"], token[4..20]);
4585            assert!(s.tenant_log("key_info").is_empty(), "no tenant's log");
4586        }
4587
4588        #[tokio::test]
4589        async fn no_key_or_a_bad_one_is_401() {
4590            let s = server().await;
4591            for token in [None, Some("vcs_nonsense")] {
4592                let answer = key(&s, token).await;
4593                assert_eq!(answer.status(), 401, "{token:?}");
4594                assert_eq!(problem(answer).await, "unauthorized");
4595            }
4596        }
4597
4598        #[tokio::test]
4599        async fn a_read_that_cannot_be_recorded_shows_nothing() {
4600            let s = server().await;
4601            let token = s.key(&[Scope::Read]);
4602            s.tenants.store(s.tenant).expect("its store").break_audit();
4603            let answer = key(&s, Some(&token)).await;
4604            assert_eq!(answer.status(), 503);
4605            assert_eq!(problem(answer).await, "audit-failed");
4606        }
4607
4608        #[tokio::test]
4609        async fn a_key_whose_tenant_is_closing_is_503() {
4610            let s = server().await;
4611            let token = s.key(&[Scope::Read]);
4612            s.tenants.close(s.tenant);
4613            let answer = key(&s, Some(&token)).await;
4614            assert_eq!(answer.status(), 503);
4615            assert_eq!(problem(answer).await, "unavailable");
4616            let refused: Vec<_> = s
4617                .tenants
4618                .admin_records()
4619                .into_iter()
4620                .filter(|r| r["action"] == "key_info")
4621                .collect();
4622            assert_eq!(refused.len(), 1, "the refusal is recorded");
4623            assert_eq!(refused[0]["status"], 503);
4624        }
4625    }
4626
4627    /// The console's files are served through the API's own router, with
4628    /// its headers, and the API itself never asks a browser to keep or
4629    /// share anything.
4630    mod console {
4631        use super::pipeline::server;
4632        use crate::access::Scope;
4633
4634        #[tokio::test]
4635        async fn the_router_serves_the_console_without_a_key() {
4636            let s = server().await;
4637            let files = [
4638                ("/console/", "index.html", "text/html; charset=utf-8"),
4639                (
4640                    "/console/console.js",
4641                    "console.js",
4642                    "text/javascript; charset=utf-8",
4643                ),
4644                (
4645                    "/console/console.css",
4646                    "console.css",
4647                    "text/css; charset=utf-8",
4648                ),
4649            ];
4650            for (path, file, media_type) in files {
4651                let answer = s.client().get(s.url(path)).send().await.expect("an answer");
4652                assert_eq!(answer.status(), 200, "{path}");
4653                assert_eq!(answer.headers()["content-type"], media_type, "{path}");
4654                for (name, value) in crate::console::HEADERS {
4655                    assert_eq!(answer.headers()[name], value, "{path} {name}");
4656                }
4657                let source = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4658                    .join("console")
4659                    .join(file);
4660                let source = std::fs::read(source).expect("the source file");
4661                assert_eq!(answer.bytes().await.expect("body"), source, "{path}");
4662            }
4663        }
4664
4665        /// Every answer under /console carries the headers, a refusal too.
4666        #[tokio::test]
4667        async fn every_other_console_answer_carries_the_headers() {
4668            let s = server().await;
4669            let client = s.client_not_following();
4670            let answers = [
4671                (client.get(s.url("/console/other.js")), 404),
4672                (client.get(s.url("/console/a/b")), 404),
4673                (client.post(s.url("/console/")), 405),
4674                (client.get(s.url("/console")), 308),
4675            ];
4676            for (request, status) in answers {
4677                let answer = request.send().await.expect("an answer");
4678                assert_eq!(answer.status(), status, "{answer:?}");
4679                for (name, value) in crate::console::HEADERS {
4680                    assert_eq!(answer.headers()[name], value, "{status} {name}");
4681                }
4682                if status == 308 {
4683                    assert_eq!(answer.headers()["location"], "/console/");
4684                }
4685            }
4686            let elsewhere = client
4687                .get(s.url("/consoles"))
4688                .send()
4689                .await
4690                .expect("an answer");
4691            assert_eq!(elsewhere.status(), 404);
4692            assert!(
4693                !elsewhere.headers().contains_key("content-security-policy"),
4694                "only console answers carry its headers"
4695            );
4696        }
4697
4698        #[tokio::test]
4699        async fn no_answer_sets_a_cookie_or_allows_another_origin() {
4700            let s = server().await;
4701            let token = s.key(&[Scope::Read]);
4702            for path in ["/console/", "/health", "/key", "/vcons/nonsense"] {
4703                let answer = s
4704                    .client()
4705                    .get(s.url(path))
4706                    .bearer_auth(&token)
4707                    .header("origin", "https://elsewhere.example.com")
4708                    .send()
4709                    .await
4710                    .expect("an answer");
4711                let headers = answer.headers();
4712                assert!(!headers.contains_key("set-cookie"), "{path}");
4713                assert!(
4714                    !headers.contains_key("access-control-allow-origin"),
4715                    "{path}"
4716                );
4717            }
4718        }
4719    }
4720}
4721
4722/// The website documents every problem type at the URL the type names, so
4723/// a client following a `type` finds what it means. These tests hold the
4724/// pages to the code.
4725#[cfg(test)]
4726mod site_pages {
4727    use std::collections::BTreeSet;
4728    use std::path::PathBuf;
4729
4730    use super::*;
4731
4732    /// One of every problem the API can answer. Each list sits behind an
4733    /// exhaustive match, so a new kind of problem fails to compile here
4734    /// until it is listed, and then fails the test until it has its page.
4735    pub(super) fn every_problem() -> Vec<Problem> {
4736        let mut all = vec![
4737            Problem::Unauthorized,
4738            Problem::Forbidden,
4739            Problem::TooLarge,
4740            Problem::Timeout,
4741            Problem::BadBody,
4742            Problem::Unavailable,
4743            Problem::AuditFailed,
4744            Problem::UnsupportedMediaType,
4745            Problem::BadPathUuid,
4746            Problem::BadPathVersion,
4747            Problem::PreconditionRequired,
4748            Problem::PathUuidMismatch,
4749            Problem::InvalidBody,
4750            Problem::BadQuery,
4751            Problem::UnknownKey,
4752        ];
4753        for problem in &all {
4754            match problem {
4755                Problem::Unauthorized
4756                | Problem::Forbidden
4757                | Problem::TooLarge
4758                | Problem::Timeout
4759                | Problem::BadBody
4760                | Problem::Unavailable
4761                | Problem::AuditFailed
4762                | Problem::UnsupportedMediaType
4763                | Problem::BadPathUuid
4764                | Problem::BadPathVersion
4765                | Problem::PreconditionRequired
4766                | Problem::PathUuidMismatch
4767                | Problem::InvalidBody
4768                | Problem::BadQuery
4769                | Problem::UnknownKey
4770                | Problem::Invalid(_)
4771                | Problem::Store(_)
4772                | Problem::Tenant(_) => {}
4773            }
4774        }
4775        let invalid = [
4776            Invalid::BadGzip,
4777            Invalid::NotJson,
4778            Invalid::DuplicateMember,
4779            Invalid::AmbiguousForm,
4780            Invalid::NoForm,
4781            Invalid::MissingUuid,
4782            Invalid::BadUuid,
4783            Invalid::MissingCreatedAt,
4784            Invalid::BadPayload,
4785            Invalid::UuidMismatch,
4786            Invalid::BadCritical,
4787            Invalid::UnsupportedExtension,
4788            Invalid::TooDeep,
4789            Invalid::Other,
4790        ];
4791        for kind in invalid {
4792            match kind {
4793                Invalid::BadGzip
4794                | Invalid::NotJson
4795                | Invalid::DuplicateMember
4796                | Invalid::AmbiguousForm
4797                | Invalid::NoForm
4798                | Invalid::MissingUuid
4799                | Invalid::BadUuid
4800                | Invalid::MissingCreatedAt
4801                | Invalid::BadPayload
4802                | Invalid::UuidMismatch
4803                | Invalid::BadCritical
4804                | Invalid::UnsupportedExtension
4805                | Invalid::TooDeep
4806                | Invalid::Other => all.push(Problem::Invalid(kind)),
4807            }
4808        }
4809        let store = [
4810            StoreError::Conflict,
4811            StoreError::Gone,
4812            StoreError::NotFound,
4813            StoreError::NotDeleted,
4814            StoreError::PreconditionFailed,
4815            StoreError::Immutable,
4816            StoreError::LimitReached,
4817            StoreError::Damaged,
4818            StoreError::AuditFailed,
4819            StoreError::Io,
4820        ];
4821        for error in store {
4822            match error {
4823                StoreError::Conflict
4824                | StoreError::Gone
4825                | StoreError::NotFound
4826                | StoreError::NotDeleted
4827                | StoreError::PreconditionFailed
4828                | StoreError::Immutable
4829                | StoreError::LimitReached
4830                | StoreError::Damaged
4831                | StoreError::AuditFailed
4832                | StoreError::Io => all.push(Problem::Store(error)),
4833            }
4834        }
4835        let tenant = [
4836            TenantError::NotFound,
4837            TenantError::NameTaken,
4838            TenantError::BadName,
4839            TenantError::HoldsVcons,
4840            TenantError::UnderHold,
4841            TenantError::LimitReached,
4842            TenantError::AuditFailed,
4843            TenantError::Closing,
4844            TenantError::CliOnly,
4845            TenantError::BadScopes,
4846            TenantError::WrongKey,
4847            TenantError::Io,
4848        ];
4849        for error in tenant {
4850            match error {
4851                TenantError::NotFound
4852                | TenantError::NameTaken
4853                | TenantError::BadName
4854                | TenantError::HoldsVcons
4855                | TenantError::UnderHold
4856                | TenantError::LimitReached
4857                | TenantError::AuditFailed
4858                | TenantError::Closing
4859                | TenantError::CliOnly
4860                | TenantError::BadScopes
4861                | TenantError::WrongKey
4862                | TenantError::Io => all.push(Problem::Tenant(error)),
4863            }
4864        }
4865        all
4866    }
4867
4868    fn pages() -> PathBuf {
4869        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../website/content/problems")
4870    }
4871
4872    #[test]
4873    fn every_problem_type_has_its_page_with_its_status_and_title() {
4874        for problem in every_problem() {
4875            let (name, title) = problem.name();
4876            let page = pages().join(format!("{name}.md"));
4877            let text = std::fs::read_to_string(&page)
4878                .unwrap_or_else(|e| panic!("{problem:?}: {}: {e}", page.display()));
4879            let status = problem.status().as_u16();
4880            for line in [
4881                format!("title = \"{name}\""),
4882                format!("status = {status}"),
4883                format!("problem_title = \"{title}\""),
4884            ] {
4885                assert!(
4886                    text.lines().any(|l| l == line),
4887                    "{}: no line `{line}`",
4888                    page.display()
4889                );
4890            }
4891        }
4892    }
4893
4894    /// No page outlives its problem type.
4895    #[test]
4896    fn every_problem_page_is_a_problem_type() {
4897        let names: BTreeSet<String> = every_problem()
4898            .into_iter()
4899            .map(|p| p.name().0.to_owned())
4900            .collect();
4901        let pages: BTreeSet<String> = std::fs::read_dir(pages())
4902            .expect("the problem pages")
4903            .map(|e| e.expect("entry").file_name().to_string_lossy().into_owned())
4904            .filter(|f| f != "_index.md")
4905            .map(|f| f.strip_suffix(".md").unwrap_or(&f).to_owned())
4906            .collect();
4907        assert_eq!(pages, names);
4908    }
4909}
4910
4911/// Reading the description, for the tests that hold it to the code.
4912#[cfg(test)]
4913mod description {
4914    use std::sync::OnceLock;
4915
4916    use serde_json::Value;
4917
4918    fn parsed() -> &'static Value {
4919        static PARSED: OnceLock<Value> = OnceLock::new();
4920        PARSED.get_or_init(|| serde_json::from_str(super::OPENAPI).expect("openapi.json is JSON"))
4921    }
4922
4923    /// Whether the description lists `status` for `method` on `route`, a
4924    /// route template as the router writes it. The router's `{*name}`, the
4925    /// rest of a path, is OpenAPI's `{name}`.
4926    pub(super) fn lists(method: &str, route: &str, status: u16) -> bool {
4927        let path = route.replace("{*", "{");
4928        parsed()["paths"][path.as_str()][method.to_ascii_lowercase()]["responses"]
4929            .get(status.to_string())
4930            .is_some()
4931    }
4932
4933    /// Whether the operation needs a key: it does unless it says
4934    /// `security: []`.
4935    pub(super) fn keyed(method: &str, path: &str) -> bool {
4936        parsed()["paths"][path][method.to_ascii_lowercase()]["security"]
4937            .as_array()
4938            .is_none_or(|s| !s.is_empty())
4939    }
4940
4941    /// Every path and its methods, as the description names them.
4942    pub(super) fn operations() -> Vec<(String, String)> {
4943        let mut all = Vec::new();
4944        for (path, item) in parsed()["paths"].as_object().expect("paths") {
4945            for method in item.as_object().expect("a path item").keys() {
4946                if ["get", "put", "post", "delete", "patch"].contains(&method.as_str()) {
4947                    all.push((method.to_ascii_uppercase(), path.clone()));
4948                }
4949            }
4950        }
4951        all
4952    }
4953
4954    pub(super) fn document() -> &'static Value {
4955        parsed()
4956    }
4957}
4958
4959/// The description against the code: the published copy, the version, the
4960/// problem types and every route and method. Statuses are checked on every
4961/// answer the unit tests produce, in `log_request`.
4962#[cfg(test)]
4963mod openapi_description {
4964    use std::collections::BTreeSet;
4965
4966    use reqwest::Method;
4967
4968    use super::description::{document, keyed, lists, operations};
4969    use super::site_pages::every_problem;
4970    use super::tests::pipeline::server;
4971    use super::*;
4972
4973    #[test]
4974    fn the_website_publishes_the_compiled_description() {
4975        let published = std::fs::read_to_string(format!(
4976            "{}/../../website/static/openapi.json",
4977            env!("CARGO_MANIFEST_DIR")
4978        ))
4979        .expect("the website's copy");
4980        assert!(published == OPENAPI, "website/static/openapi.json differs");
4981    }
4982
4983    #[test]
4984    fn it_is_openapi_3_1_for_this_version() {
4985        let doc = document();
4986        assert!(
4987            doc["openapi"]
4988                .as_str()
4989                .is_some_and(|v| v.starts_with("3.1.")),
4990            "{}",
4991            doc["openapi"]
4992        );
4993        assert_eq!(doc["info"]["version"], env!("CARGO_PKG_VERSION"));
4994        assert!(doc["info"]["title"].as_str().is_some_and(|t| !t.is_empty()));
4995    }
4996
4997    #[test]
4998    fn its_problem_types_are_the_ones_the_api_answers() {
4999        let described: BTreeSet<String> =
5000            document()["components"]["schemas"]["Problem"]["properties"]["type"]["enum"]
5001                .as_array()
5002                .expect("the Problem type list")
5003                .iter()
5004                .map(|t| t.as_str().expect("a URL").to_owned())
5005                .collect();
5006        let answered: BTreeSet<String> = every_problem()
5007            .into_iter()
5008            .map(|p| format!("https://vcon-server.com/problems/{}", p.name().0))
5009            .collect();
5010        assert_eq!(described, answered);
5011    }
5012
5013    /// A path template with sample values, so it reaches its route.
5014    fn concrete(path: &str) -> String {
5015        path.replace("{uuid}", "01a07da8-c2bb-83e5-b9a2-279e0d16bc46")
5016            .replace("{version}", "00112233445566778899aabbccddeeff")
5017            .replace("{id}", "00112233445566778899aabbccddeeff")
5018            .replace("{other}", "no-such-file")
5019    }
5020
5021    /// Every operation the description names is routed (one that needs a
5022    /// key asks for it; one that needs none answers a status it lists), and
5023    /// every other method on its paths is not (405).
5024    #[tokio::test]
5025    async fn every_described_route_is_routed_and_no_other_method() {
5026        let s = server().await;
5027        let described: BTreeSet<(String, String)> = operations().into_iter().collect();
5028        assert!(!described.is_empty());
5029        let paths: BTreeSet<&String> = described.iter().map(|(_, p)| p).collect();
5030        for path in paths {
5031            for method in ["GET", "POST", "PUT", "PATCH", "DELETE"] {
5032                // Not following redirects, so each answer is the route's own.
5033                let answer = s
5034                    .client_not_following()
5035                    .request(
5036                        Method::from_bytes(method.as_bytes()).expect("a method"),
5037                        s.url(&concrete(path)),
5038                    )
5039                    .send()
5040                    .await
5041                    .expect("an answer");
5042                let status = answer.status().as_u16();
5043                if !described.contains(&(method.to_owned(), path.clone())) {
5044                    assert_eq!(status, 405, "{method} {path}");
5045                } else if keyed(method, path) {
5046                    assert_eq!(status, 401, "{method} {path}");
5047                } else {
5048                    assert!(lists(method, path, status), "{method} {path}: {status}");
5049                }
5050            }
5051        }
5052    }
5053}