1use 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#[derive(Debug, Clone, Copy)]
44pub struct Limits {
45 pub body: usize,
47 pub inflated: u64,
49 pub body_timeout: Duration,
51}
52
53pub struct App {
55 pub tenants: Arc<Tenants>,
56 pub limits: Limits,
57 pub supported: BTreeSet<String>,
59 pub retention_default: Retention,
61 pub mcp: crate::mcp::Limits,
63}
64
65pub 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
94async 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 #[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_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
133pub const OPENAPI: &str = include_str!("../openapi.json");
136
137async 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 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
183async 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 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
216pub(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
233async 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
259async 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 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
282const JSON_LINES: &str = "application/jsonl";
284
285async 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 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
313async 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 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
342async 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 Err(error) => failed(error).into_response(),
381 }
382}
383
384async 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 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
430fn 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
445async 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 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 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
487const VCON_STATE: HeaderName = HeaderName::from_static("vcon-state");
489
490async 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
508async 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
522async 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
545async fn legal_body(admitted: &Admitted, uuid: Uuid, version: Option<VersionId>) -> Response {
547 let (store, key) = (admitted.store.clone(), admitted.caller.key);
548 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
572async 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
601pub(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
625async 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
644async 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
654async 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 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
678async 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
699async 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
717fn 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#[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#[derive(serde::Deserialize)]
737#[serde(deny_unknown_fields)]
738struct NewRetention {
739 #[serde(deserialize_with = "period")]
740 retention: Option<Retention>,
741}
742
743#[derive(serde::Deserialize)]
745#[serde(deny_unknown_fields)]
746struct NewMetadata {
747 #[serde(deserialize_with = "level")]
748 metadata: crate::export::Metadata,
749}
750
751fn 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
757fn 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
767async 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 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
788async 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
807async 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
843async 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
876async 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 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
908fn 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#[derive(serde::Deserialize)]
921#[serde(deny_unknown_fields)]
922struct NewKeyBody {
923 scopes: Vec<String>,
924 label: String,
925}
926
927async 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 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
966async 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
999async 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 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
1031async 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 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
1075async 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
1131fn 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
1142fn entity_tag(version: VersionId) -> String {
1144 format!("\"{version}\"")
1145}
1146
1147fn 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
1161struct Admitted {
1163 caller: Caller,
1164 tenant: TenantId,
1165 store: Arc<Store>,
1166 body: Bytes,
1167}
1168
1169async 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
1202pub(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 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
1241pub(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
1266pub(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#[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 BadPathUuid,
1286 BadPathVersion,
1288 PreconditionRequired,
1290 PathUuidMismatch,
1292 Invalid(Invalid),
1294 InvalidBody,
1296 BadQuery,
1298 UnknownKey,
1300 Store(StoreError),
1302 Tenant(TenantError),
1304 RateLimited,
1306 AtCapacity,
1308 UnknownTool,
1310 InvalidArguments,
1312}
1313
1314#[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 Other,
1332}
1333
1334fn 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 _ => Invalid::Other,
1357 })
1358}
1359
1360fn 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 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 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#[derive(Debug)]
1552pub enum TlsError {
1553 Certificate,
1555 Key,
1557 Refused(rustls::Error),
1560}
1561
1562pub 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
1584pub 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 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 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 pub(crate) const START: time::OffsetDateTime =
1729 time::macros::datetime!(2026-10-23 12:00 UTC);
1730
1731 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 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 pub(crate) async fn server_with(supported: &[&str]) -> Server {
1778 server_from(supported, Retention::Years(7)).await
1779 }
1780
1781 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 #[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 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 #[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 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 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 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 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 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 let reads = s.tenant_log("audit_read");
4399 assert_eq!(reads.len(), 2);
4400 assert!(reads.iter().all(|r| r["status"] == 200));
4401 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 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 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 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 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 let beta_page = page(&s, &beta_reader, "?limit=1").await;
4678 let cursor = beta_page["next"].as_str().expect("a cursor");
4679 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 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 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 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 #[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#[cfg(test)]
4983mod site_pages {
4984 use std::collections::BTreeSet;
4985 use std::path::PathBuf;
4986
4987 use super::*;
4988
4989 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 #[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#[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 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 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 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#[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 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 #[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 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}