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