use trust_tasks_rs::{ErrorPayload, Payload, RejectReason};
use super::helpers::{TrustTaskOutcome, unrouted_error_response};
use crate::server::AppState;
pub(crate) const DEFAULT_MAX_DOCUMENT_BYTES: usize = 64 * 1024;
pub(crate) const POLICY_UPSERT_MAX_DOCUMENT_BYTES: usize =
2 * crate::policy::POLICY_SOURCE_MAX_BYTES + 64 * 1024;
pub(crate) const DID_REGISTER_MAX_DOCUMENT_BYTES: usize = 1024 * 1024;
pub(crate) const AWAITING_DECLARATION: &[(&str, usize)] = &[
(
<trust_tasks_rs::specs::policy::upsert::v0_2::Payload as Payload>::TYPE_URI,
POLICY_UPSERT_MAX_DOCUMENT_BYTES,
),
(
<trust_tasks_rs::specs::did_management::did::register::v0_1::Payload as Payload>::TYPE_URI,
DID_REGISTER_MAX_DOCUMENT_BYTES,
),
];
fn declared(type_uri: &str) -> Option<usize> {
trust_tasks_rs::schema_index::max_document_bytes_for(type_uri).or_else(|| {
AWAITING_DECLARATION
.iter()
.find(|(uri, _)| *uri == type_uri)
.map(|(_, max)| *max)
})
}
fn served_uris() -> Vec<&'static str> {
let mut all: Vec<&'static str> = super::DISPATCHED_URIS.to_vec();
all.extend(crate::rooms::handlers::served_uris());
all
}
pub(crate) fn largest_max_document_bytes() -> usize {
largest_in(&served_uris(), declared)
}
fn largest_in(served: &[&str], declared: impl Fn(&str) -> Option<usize>) -> usize {
served
.iter()
.filter_map(|uri| declared(uri))
.fold(DEFAULT_MAX_DOCUMENT_BYTES, usize::max)
}
pub(crate) const DETAILS_MAX_BYTES: &str = "maxBytes";
pub(crate) fn max_document_bytes(type_uri: &str) -> usize {
max_in(type_uri, &served_uris(), declared)
}
fn max_in(type_uri: &str, served: &[&str], declared: impl Fn(&str) -> Option<usize>) -> usize {
if served.contains(&type_uri) {
declared(type_uri).unwrap_or(DEFAULT_MAX_DOCUMENT_BYTES)
} else {
DEFAULT_MAX_DOCUMENT_BYTES
}
}
pub(crate) const LARGE_DOCUMENT_BUDGET_PER_WINDOW: u64 = 5;
pub(crate) const LARGE_DOCUMENT_BUDGET_WINDOW_SECS: u64 = 60;
pub(crate) const LARGE_DOCUMENT_GLOBAL_BUDGET_PER_WINDOW: u64 = 60;
const MAX_TRACKED_ADDRESSES: usize = 10_000;
#[derive(Debug, Clone, Copy)]
struct Bucket {
spent: u64,
window_start: u64,
cost: u64,
}
#[derive(Debug, Default)]
pub struct LargeDocumentBudget {
inner: std::sync::Mutex<BudgetState>,
}
#[derive(Debug, Default)]
struct BudgetState {
buckets: std::collections::HashMap<String, Bucket>,
global_spent: u64,
global_window_start: u64,
}
impl LargeDocumentBudget {
pub fn new() -> Self {
Self::default()
}
fn try_charge(&self, address: &str, now: u64) -> Result<(), u64> {
let mut state = self
.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let state = &mut *state;
if now.saturating_sub(state.global_window_start) >= LARGE_DOCUMENT_BUDGET_WINDOW_SECS {
state.global_spent = 0;
state.global_window_start = now;
}
let global_retry =
(state.global_window_start + LARGE_DOCUMENT_BUDGET_WINDOW_SECS).saturating_sub(now);
if state.global_spent >= LARGE_DOCUMENT_GLOBAL_BUDGET_PER_WINDOW {
return Err(global_retry);
}
let buckets = &mut state.buckets;
if buckets.len() >= MAX_TRACKED_ADDRESSES && !buckets.contains_key(address) {
buckets.retain(|_, b| {
b.cost > 1 || now.saturating_sub(b.window_start) < LARGE_DOCUMENT_BUDGET_WINDOW_SECS
});
if buckets.len() >= MAX_TRACKED_ADDRESSES {
return Err(global_retry);
}
}
let entry = buckets.entry(address.to_string()).or_insert(Bucket {
spent: 0,
window_start: now,
cost: 1,
});
if now.saturating_sub(entry.window_start) >= LARGE_DOCUMENT_BUDGET_WINDOW_SECS {
entry.spent = 0;
entry.window_start = now;
}
if entry.spent.saturating_add(entry.cost) > LARGE_DOCUMENT_BUDGET_PER_WINDOW {
let retry_after_secs =
(entry.window_start + LARGE_DOCUMENT_BUDGET_WINDOW_SECS).saturating_sub(now);
return Err(retry_after_secs);
}
entry.spent += entry.cost;
state.global_spent += 1;
Ok(())
}
fn penalize(&self, address: &str) {
let mut state = self
.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let entry = state.buckets.entry(address.to_string()).or_insert(Bucket {
spent: 0,
window_start: 0,
cost: 1,
});
entry.cost = entry.cost.saturating_mul(2).min(1 << 20);
}
#[cfg(test)]
fn cost(&self, address: &str) -> u64 {
self.inner
.lock()
.unwrap()
.buckets
.get(address)
.map(|b| b.cost)
.unwrap_or(1)
}
}
#[derive(Debug)]
pub(crate) struct LargeDocumentCharge {
address: String,
claimed_issuer: String,
}
#[cfg(test)]
pub(crate) async fn check_for_known_issuer(
state: &AppState,
body: &[u8],
address: &str,
now: u64,
) -> Result<Option<LargeDocumentCharge>, TrustTaskOutcome> {
check_for_known_issuer_with(state, body, address, now, false).await
}
pub(crate) async fn check_for_known_issuer_with(
state: &AppState,
body: &[u8],
address: &str,
now: u64,
room_precharged: bool,
) -> Result<Option<LargeDocumentCharge>, TrustTaskOutcome> {
if body.len() <= DEFAULT_MAX_DOCUMENT_BYTES {
return Ok(None);
}
if room_precharged
&& let Some(issuer) = peek_issuer(body)
&& holds_upload_for(state, peek_type(body).as_deref(), &issuer).await
{
check_with(body, max_document_bytes)?;
return Ok(None);
}
let claimed_issuer = match peek_issuer(body) {
Some(issuer) if has_standing(state, body, &issuer).await => Some(issuer),
_ => None,
};
let Some(claimed_issuer) = claimed_issuer else {
check_with(body, |_| DEFAULT_MAX_DOCUMENT_BYTES)?;
return Ok(None);
};
if let Err(retry_after_secs) = state.large_document_budget.try_charge(address, now) {
let payload: ErrorPayload = RejectReason::Unavailable {
retry_after: Some(
chrono::Utc::now() + chrono::Duration::seconds(retry_after_secs as i64),
),
}
.into();
return Err(unrouted_error_response(payload));
}
check_with(body, max_document_bytes)?;
Ok(Some(LargeDocumentCharge {
address: address.to_string(),
claimed_issuer,
}))
}
pub(crate) fn settle_large_document_charge(
budget: &LargeDocumentBudget,
charge: Option<&LargeDocumentCharge>,
verified_issuer: Option<&str>,
) {
let Some(charge) = charge else {
return;
};
if verified_issuer != Some(charge.claimed_issuer.as_str()) {
budget.penalize(&charge.address);
}
}
async fn has_standing(state: &AppState, body: &[u8], issuer: &str) -> bool {
if is_known_issuer(state, issuer).await {
return true;
}
let files = &state.room_files;
match peek_type(body).as_deref() {
t @ Some(
vti_rooms::blob_tasks::UPLOAD_CHUNK_TYPE | vti_rooms::blob_tasks::UPLOAD_CHUNK_V2_TYPE,
) => holds_upload_for(state, t, issuer).await,
Some(
vti_rooms::blob_tasks::UPLOAD_BEGIN_TYPE | vti_rooms::blob_tasks::UPLOAD_BEGIN_V2_TYPE,
) if files.files_enabled() => match peek_room_id(body) {
Some(room_id) => vti_rooms::storage::get_room(&state.rooms_ks, &room_id)
.await
.is_ok(),
None => false,
},
_ => false,
}
}
async fn holds_upload_for(state: &AppState, type_uri: Option<&str>, issuer: &str) -> bool {
let did = issuer.split('#').next().unwrap_or(issuer);
match type_uri {
Some(vti_rooms::blob_tasks::UPLOAD_CHUNK_TYPE) => {
state.room_files.holds_open_upload(did).await
}
Some(vti_rooms::blob_tasks::UPLOAD_CHUNK_V2_TYPE) => {
state.room_files.holds_open_upload_key(did).await
}
_ => false,
}
}
fn peek_room_id(body: &[u8]) -> Option<String> {
#[derive(serde::Deserialize)]
struct RoomOnly {
#[serde(rename = "roomId")]
room_id: String,
}
#[derive(serde::Deserialize)]
struct PayloadOnly {
payload: RoomOnly,
}
serde_json::from_slice::<PayloadOnly>(body)
.ok()
.map(|p| p.payload.room_id)
}
async fn is_known_issuer(state: &AppState, issuer: &str) -> bool {
let now = crate::auth::session::now_epoch();
let live = |entry: Option<crate::acl::VtcAclEntry>| entry.is_some_and(|e| !e.is_expired(now));
let did = issuer.split('#').next().unwrap_or(issuer);
if live(
crate::acl::get_acl_entry(&state.acl_ks, did)
.await
.ok()
.flatten(),
) {
return true;
}
match crate::acl::console_key::resolve_delegated_admin(&state.console_keys_ks, did).await {
Ok(Some(delegation)) => live(
crate::acl::get_acl_entry(&state.acl_ks, &delegation.admin_did)
.await
.ok()
.flatten(),
),
_ => false,
}
}
const MAX_NAMED_TYPE_CHARS: usize = 256;
fn check_with(body: &[u8], limit: impl Fn(&str) -> usize) -> Result<(), TrustTaskOutcome> {
if body.len() <= DEFAULT_MAX_DOCUMENT_BYTES {
return Ok(());
}
let type_uri = peek_type(body);
let max = type_uri
.as_deref()
.map_or(DEFAULT_MAX_DOCUMENT_BYTES, &limit);
if body.len() <= max {
return Ok(());
}
let named = match type_uri.as_deref() {
Some(t) if t.chars().count() <= MAX_NAMED_TYPE_CHARS => t,
Some(_) => "its type",
None => "a document of no readable type",
};
let payload: ErrorPayload = RejectReason::MalformedRequest {
reason: format!(
"the document is {} bytes; {named} accepts at most {max}",
body.len()
),
}
.into();
Err(unrouted_error_response(payload.with_details(
serde_json::json!({ DETAILS_MAX_BYTES: max }),
)))
}
fn peek_type(body: &[u8]) -> Option<String> {
#[derive(serde::Deserialize)]
struct TypeOnly {
#[serde(rename = "type")]
type_uri: String,
}
serde_json::from_slice::<TypeOnly>(body)
.ok()
.map(|t| t.type_uri)
}
fn peek_issuer(body: &[u8]) -> Option<String> {
#[derive(serde::Deserialize)]
struct IssuerOnly {
issuer: String,
}
serde_json::from_slice::<IssuerOnly>(body)
.ok()
.map(|t| t.issuer)
}
#[cfg(test)]
mod tests {
use super::*;
const POLICY_UPSERT: &str =
<trust_tasks_rs::specs::policy::upsert::v0_2::Payload as Payload>::TYPE_URI;
const MEMBERS_UPDATE: &str =
<trust_tasks_rs::specs::vtc::members::update::v0_1::Payload as Payload>::TYPE_URI;
const WEBSITE_CHUNK: &str =
<trust_tasks_rs::specs::vtc::website::upload::chunk::v0_1::Payload as Payload>::TYPE_URI;
fn served_upsert(type_uri: &str) -> usize {
max_in(type_uri, &[POLICY_UPSERT], declared)
}
fn document_from(issuer: &str, type_uri: &str, len: usize) -> Vec<u8> {
let head = format!(r#"{{"type":"{type_uri}","issuer":"{issuer}","payload":{{"pad":""#);
let tail = r#""}}"#;
let pad = len - head.len() - tail.len();
let body = format!("{head}{}{tail}", "x".repeat(pad));
assert_eq!(body.len(), len);
body.into_bytes()
}
fn document_of(type_uri: &str, len: usize) -> Vec<u8> {
document_from("did:key:z6MkSizeSender", type_uri, len)
}
fn refusal(outcome: TrustTaskOutcome) -> serde_json::Value {
assert_eq!(outcome.status, axum::http::StatusCode::BAD_REQUEST);
payload_of(outcome)
}
fn payload_of(outcome: TrustTaskOutcome) -> serde_json::Value {
let doc: serde_json::Value = serde_json::from_slice(&outcome.body).unwrap();
doc["payload"].clone()
}
#[test]
fn every_awaiting_entry_is_still_undeclared_upstream() {
for (uri, max) in AWAITING_DECLARATION {
assert!(
trust_tasks_rs::schema_index::schema_for(uri).is_some(),
"{uri} names no published specification"
);
assert!(
trust_tasks_rs::schema_index::max_document_bytes_for(uri).is_none(),
"{uri} now declares maxDocumentBytes; drop its AWAITING_DECLARATION entry"
);
assert!(
*max > DEFAULT_MAX_DOCUMENT_BYTES,
"{uri} awaits {max}, which is not above the default; drop it"
);
}
}
#[test]
fn a_declared_limit_is_the_specifications_own() {
let chunk = trust_tasks_rs::schema_index::max_document_bytes_for(WEBSITE_CHUNK)
.expect("the chunk declares a limit");
assert_eq!(max_in(WEBSITE_CHUNK, &[WEBSITE_CHUNK], declared), chunk);
assert_eq!(served_upsert(MEMBERS_UPDATE), DEFAULT_MAX_DOCUMENT_BYTES);
assert_eq!(
served_upsert(POLICY_UPSERT),
POLICY_UPSERT_MAX_DOCUMENT_BYTES
);
}
#[test]
fn a_declared_type_that_is_not_served_takes_the_default() {
assert_eq!(
max_in(POLICY_UPSERT, &[], declared),
DEFAULT_MAX_DOCUMENT_BYTES
);
assert_eq!(largest_in(&[], declared), DEFAULT_MAX_DOCUMENT_BYTES);
assert_eq!(
largest_in(&[POLICY_UPSERT, MEMBERS_UPDATE], declared),
POLICY_UPSERT_MAX_DOCUMENT_BYTES
);
for uri in super::super::DISPATCHED_URIS {
assert!(
max_document_bytes(uri) <= largest_max_document_bytes(),
"{uri}"
);
}
}
#[test]
fn a_document_at_its_types_limit_is_admitted() {
assert!(
check_with(
&document_of(MEMBERS_UPDATE, DEFAULT_MAX_DOCUMENT_BYTES),
max_document_bytes
)
.is_ok()
);
assert!(
check_with(
&document_of(POLICY_UPSERT, POLICY_UPSERT_MAX_DOCUMENT_BYTES),
served_upsert
)
.is_ok()
);
}
#[test]
fn a_document_over_the_default_is_refused_for_a_type_that_declares_nothing() {
let payload = refusal(
check_with(
&document_of(MEMBERS_UPDATE, DEFAULT_MAX_DOCUMENT_BYTES + 1),
max_document_bytes,
)
.unwrap_err(),
);
assert_eq!(payload["code"], "malformedRequest");
assert_eq!(
payload["details"][DETAILS_MAX_BYTES],
DEFAULT_MAX_DOCUMENT_BYTES
);
}
#[test]
fn a_document_over_its_raised_limit_is_refused() {
let payload = refusal(
check_with(
&document_of(POLICY_UPSERT, POLICY_UPSERT_MAX_DOCUMENT_BYTES + 1),
served_upsert,
)
.unwrap_err(),
);
assert_eq!(payload["code"], "malformedRequest");
assert_eq!(
payload["details"][DETAILS_MAX_BYTES],
POLICY_UPSERT_MAX_DOCUMENT_BYTES
);
}
#[test]
fn the_type_is_the_top_level_one_wherever_it_sits() {
let pad = "x".repeat(DEFAULT_MAX_DOCUMENT_BYTES);
let late = format!(
r#"{{"payload":{{"type":"{MEMBERS_UPDATE}","pad":"{pad}"}},"type":"{POLICY_UPSERT}"}}"#
);
assert!(check_with(late.as_bytes(), served_upsert).is_ok());
let nested_only = format!(r#"{{"payload":{{"type":"{POLICY_UPSERT}","pad":"{pad}"}}}}"#);
assert_eq!(
refusal(check_with(nested_only.as_bytes(), served_upsert).unwrap_err())["details"]
[DETAILS_MAX_BYTES],
DEFAULT_MAX_DOCUMENT_BYTES
);
}
#[test]
fn a_duplicated_type_takes_the_default() {
let pad = "x".repeat(DEFAULT_MAX_DOCUMENT_BYTES);
let body =
format!(r#"{{"type":"{MEMBERS_UPDATE}","type":"{POLICY_UPSERT}","pad":"{pad}"}}"#);
assert_eq!(
refusal(check_with(body.as_bytes(), served_upsert).unwrap_err())["details"]
[DETAILS_MAX_BYTES],
DEFAULT_MAX_DOCUMENT_BYTES
);
}
#[test]
fn a_refusal_does_not_echo_an_oversized_type() {
let huge = "t".repeat(DEFAULT_MAX_DOCUMENT_BYTES + 1);
let body = format!(r#"{{"type":"{huge}"}}"#);
let payload = refusal(check_with(body.as_bytes(), max_document_bytes).unwrap_err());
let text = payload.to_string();
assert!(
text.len() < 1024,
"the refusal echoes the body: {} bytes",
text.len()
);
assert_eq!(
payload["details"],
serde_json::json!({ DETAILS_MAX_BYTES: DEFAULT_MAX_DOCUMENT_BYTES })
);
}
#[test]
fn an_oversized_body_with_no_readable_type_takes_the_default() {
let junk = vec![b'['; DEFAULT_MAX_DOCUMENT_BYTES + 1];
let payload = refusal(check_with(&junk, max_document_bytes).unwrap_err());
assert_eq!(payload["code"], "malformedRequest");
assert_eq!(
payload["details"][DETAILS_MAX_BYTES],
DEFAULT_MAX_DOCUMENT_BYTES
);
}
async fn vtc_with_known_admin() -> (crate::test_support::TestVtc, &'static str, &'static str) {
use crate::acl::{VtcAclEntry, VtcRole, store_acl_entry};
let vtc = crate::test_support::TestVtc::builder().build().await;
let admin = "did:key:z6MkSizeKnownAdmin";
store_acl_entry(
&vtc.state.acl_ks,
&VtcAclEntry {
did: admin.into(),
role: VtcRole::Admin,
label: None,
admin: VtcRole::Admin.implied_authority(),
delegated_by: None,
created_at: 0,
created_by: "did:key:vtc-install".into(),
updated_at: None,
updated_by: None,
expires_at: None,
resource_grants: Vec::new(),
label_set_by_subject: false,
suspension: None,
},
)
.await
.unwrap();
let key = "did:key:z6MkSizeDelegatedKey";
crate::acl::console_key::enrol_delegation(
&vtc.state.console_keys_ks,
&vtc.state.acl_ks,
key,
admin,
None,
None,
)
.await
.unwrap();
(vtc, admin, key)
}
fn room_document(issuer: &str, type_uri: &str, room_id: &str, len: usize) -> Vec<u8> {
let head = format!(
r#"{{"type":"{type_uri}","issuer":"{issuer}","payload":{{"roomId":"{room_id}","pad":""#
);
let tail = r#""}}"#;
let body = format!("{head}{}{tail}", "x".repeat(len - head.len() - tail.len()));
body.into_bytes()
}
#[tokio::test]
async fn a_room_file_document_takes_its_raised_limit_from_room_standing() {
use vti_rooms::wire::{ROOMS_BLOBS_UPLOAD_BEGIN_TYPE, ROOMS_BLOBS_UPLOAD_CHUNK_TYPE};
let vtc = crate::test_support::build_test_vtc().await;
let state = &vtc.state;
let f = vti_rooms_dtg::test_support::RoomFixture::new(vti_rooms::Visibility::Open).await;
vti_rooms::storage::create_room(&state.rooms_ks, &f.room)
.await
.unwrap();
let len = DEFAULT_MAX_DOCUMENT_BYTES + 1;
assert!(max_document_bytes(ROOMS_BLOBS_UPLOAD_CHUNK_TYPE) >= len);
assert!(max_document_bytes(ROOMS_BLOBS_UPLOAD_BEGIN_TYPE) >= len);
let uploader = f.owner.did.as_str();
let chunk = |issuer: &str| room_document(issuer, ROOMS_BLOBS_UPLOAD_CHUNK_TYPE, "-", len);
let early = refusal(
check_for_known_issuer(state, &chunk(uploader), "ip:early", 0)
.await
.unwrap_err(),
);
assert_eq!(
early["details"][DETAILS_MAX_BYTES],
DEFAULT_MAX_DOCUMENT_BYTES
);
let mut builder = vti_rooms::files::ManifestBuilder::new(16_384);
builder.push(&[7u8; 16_384]);
let manifest = serde_json::to_value(builder.finish().unwrap()).unwrap();
state
.room_files
.begin(
&f.room,
uploader,
None,
vti_rooms::blobs::Manifest::parse(&manifest).unwrap(),
)
.await
.expect("open an upload");
assert!(
check_for_known_issuer(state, &chunk(uploader), "ip:uploader", 0)
.await
.unwrap()
.is_some(),
"the uploader's chunk is charged and admitted"
);
let other = refusal(
check_for_known_issuer(state, &chunk("did:key:z6MkSizeStranger"), "ip:other", 0)
.await
.unwrap_err(),
);
assert_eq!(
other["details"][DETAILS_MAX_BYTES],
DEFAULT_MAX_DOCUMENT_BYTES
);
assert_eq!(state.large_document_budget.cost("ip:other"), 1);
let begin = |room: &str| {
room_document(
"did:key:z6MkSizeMember",
ROOMS_BLOBS_UPLOAD_BEGIN_TYPE,
room,
len,
)
};
assert!(
check_for_known_issuer(state, &begin(&f.room.room_id), "ip:member", 0)
.await
.unwrap()
.is_some()
);
let unhosted = refusal(
check_for_known_issuer(state, &begin("did:key:z6MkNoSuchRoom"), "ip:nobody", 0)
.await
.unwrap_err(),
);
assert_eq!(
unhosted["details"][DETAILS_MAX_BYTES],
DEFAULT_MAX_DOCUMENT_BYTES
);
}
#[tokio::test]
async fn a_raised_limit_needs_an_issuer_with_standing() {
let (vtc, admin, key) = vtc_with_known_admin().await;
let len = DEFAULT_MAX_DOCUMENT_BYTES + 1;
assert!(served_upsert(POLICY_UPSERT) >= len);
let limit_for = |issuer: &str| document_from(issuer, POLICY_UPSERT, len);
assert!(super::super::DISPATCHED_URIS.contains(&POLICY_UPSERT));
for (i, issuer) in [admin, key].into_iter().enumerate() {
assert!(
check_for_known_issuer(&vtc.state, &limit_for(issuer), &format!("ip:{i}"), 0)
.await
.is_ok(),
"{issuer}"
);
}
let stranger = refusal(
check_for_known_issuer(
&vtc.state,
&limit_for("did:key:z6MkSizeStranger"),
"ip:stranger",
0,
)
.await
.unwrap_err(),
);
assert_eq!(
stranger["details"][DETAILS_MAX_BYTES],
DEFAULT_MAX_DOCUMENT_BYTES
);
assert_eq!(
vtc.state.large_document_budget.cost("ip:stranger"),
1,
"an unknown issuer never charges the budget"
);
let pad = "x".repeat(DEFAULT_MAX_DOCUMENT_BYTES);
let anonymous = format!(r#"{{"type":"{POLICY_UPSERT}","payload":{{"pad":"{pad}"}}}}"#);
assert!(
check_for_known_issuer(&vtc.state, anonymous.as_bytes(), "ip:anon", 0)
.await
.is_err(),
"a document naming no issuer at all is held to the default"
);
}
#[tokio::test]
async fn small_documents_are_never_charged() {
let (vtc, admin, _) = vtc_with_known_admin().await;
let small = document_from(admin, POLICY_UPSERT, DEFAULT_MAX_DOCUMENT_BYTES);
for _ in 0..(LARGE_DOCUMENT_BUDGET_PER_WINDOW * 3) {
let charge = check_for_known_issuer(&vtc.state, &small, "ip:small", 0)
.await
.expect("small document admitted");
assert!(
charge.is_none(),
"a small document never charges the budget"
);
}
}
#[tokio::test]
async fn repeated_large_documents_from_one_address_are_refused_past_the_budget() {
let (vtc, admin, _) = vtc_with_known_admin().await;
let len = DEFAULT_MAX_DOCUMENT_BYTES + 1;
let doc = document_from(admin, POLICY_UPSERT, len);
for _ in 0..LARGE_DOCUMENT_BUDGET_PER_WINDOW {
assert!(
check_for_known_issuer(&vtc.state, &doc, "ip:9.9.9.9", 0)
.await
.is_ok()
);
}
let err = check_for_known_issuer(&vtc.state, &doc, "ip:9.9.9.9", 0)
.await
.unwrap_err();
assert_eq!(err.status, axum::http::StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(payload_of(err)["code"], "unavailable");
assert!(
check_for_known_issuer(&vtc.state, &doc, "ip:1.1.1.1", 0)
.await
.is_ok()
);
}
#[tokio::test]
async fn a_verified_known_issuer_within_its_budget_settles_for_free() {
let (vtc, admin, _) = vtc_with_known_admin().await;
let len = DEFAULT_MAX_DOCUMENT_BYTES + 1;
let doc = document_from(admin, POLICY_UPSERT, len);
let charge = check_for_known_issuer(&vtc.state, &doc, "ip:1.2.3.4", 0)
.await
.expect("within budget")
.expect("a large document charges the budget");
settle_large_document_charge(&vtc.state.large_document_budget, Some(&charge), Some(admin));
assert_eq!(
vtc.state.large_document_budget.cost("ip:1.2.3.4"),
1,
"a verified claim is not penalised"
);
}
#[test]
fn an_unverified_or_mismatched_issuer_penalises_the_address() {
let budget = LargeDocumentBudget::new();
let charge = LargeDocumentCharge {
address: "ip:1.2.3.4".to_string(),
claimed_issuer: "did:key:z6MkSizeKnownAdmin".to_string(),
};
settle_large_document_charge(&budget, Some(&charge), None);
assert_eq!(budget.cost("ip:1.2.3.4"), 2);
settle_large_document_charge(&budget, Some(&charge), Some("did:key:z6MkSizeSomeoneElse"));
assert_eq!(budget.cost("ip:1.2.3.4"), 4);
settle_large_document_charge(&budget, None, None);
assert_eq!(budget.cost("ip:1.2.3.4"), 4);
}
#[test]
fn many_addresses_together_are_held_to_the_global_budget() {
let budget = LargeDocumentBudget::new();
for i in 0..LARGE_DOCUMENT_GLOBAL_BUDGET_PER_WINDOW {
assert!(budget.try_charge(&format!("vid:{i}"), 1_000).is_ok());
}
assert!(
budget.try_charge("vid:fresh", 1_000).is_err(),
"a fresh address does not escape the service-wide cap"
);
assert!(
budget
.try_charge("vid:fresh", 1_000 + LARGE_DOCUMENT_BUDGET_WINDOW_SECS)
.is_ok(),
"the next window has room again"
);
}
#[test]
fn a_full_map_keeps_its_penalties() {
let budget = LargeDocumentBudget::new();
budget.penalize("ip:abuser");
{
let mut state = budget.inner.lock().unwrap();
for i in 0..MAX_TRACKED_ADDRESSES {
state.buckets.insert(
format!("ip:{i}"),
Bucket {
spent: 0,
window_start: 0,
cost: 1,
},
);
}
}
assert!(budget.try_charge("ip:new", 10_000).is_ok());
assert_eq!(budget.cost("ip:abuser"), 2);
}
#[test]
fn a_full_map_of_live_addresses_refuses_a_new_one() {
let budget = LargeDocumentBudget::new();
{
let mut state = budget.inner.lock().unwrap();
for i in 0..MAX_TRACKED_ADDRESSES {
state.buckets.insert(
format!("ip:{i}"),
Bucket {
spent: 1,
window_start: 10_000,
cost: 1,
},
);
}
}
assert!(budget.try_charge("ip:new", 10_000).is_err());
assert!(
budget.try_charge("ip:0", 10_000).is_ok(),
"a tracked address still charges"
);
}
#[tokio::test]
async fn every_transport_meets_the_same_refusal() {
use crate::join::JoinTransport;
let vtc = crate::test_support::TestVtc::builder().build().await;
let body = document_of(MEMBERS_UPDATE, DEFAULT_MAX_DOCUMENT_BYTES + 1);
for transport in [
JoinTransport::Rest,
JoinTransport::DIDComm,
JoinTransport::Tsp,
] {
let ctx = match transport {
JoinTransport::Rest => super::super::JoinAuthCtx::rest(),
_ => super::super::JoinAuthCtx {
transport,
sender_did: Some("did:key:z6MkSizeSender".into()),
verified_signer: None,
},
};
let out = super::super::dispatch_trust_task_core(&vtc.state, &ctx, &body).await;
let payload = refusal(out);
assert_eq!(payload["code"], "malformedRequest", "{transport:?}");
assert_eq!(
payload["details"][DETAILS_MAX_BYTES], DEFAULT_MAX_DOCUMENT_BYTES,
"{transport:?}"
);
}
}
}