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