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