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