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