feat(media): write per-upload-event records for moderation (#1551)

Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1ty04d6th3jzvggd25za6xmqrh99g42kvkvqsmf7ydv793fgyxa3s3t2lw7 <591f56e9778c84c421aaa0bba36c03b94a8aaaccb3010da7c46b3c58a5043763@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Bradley Axen
2026-07-09 19:59:38 -07:00
committed by GitHub
parent 19919e1810
commit 83fc30b14d
13 changed files with 809 additions and 44 deletions

View File

@@ -18,7 +18,7 @@ use axum::{
use base64::Engine;
use buzz_audit::{AuditAction, NewAuditEntry};
use buzz_core::tenant::TenantContext;
use buzz_media::{BlobDescriptor, MediaError};
use buzz_media::{BlobDescriptor, MediaError, UploadAttribution, UploadNetworkInfo};
use crate::state::AppState;
@@ -209,6 +209,52 @@ impl FromRequestParts<Arc<AppState>> for AuthenticatedUpload {
}
}
/// Build per-event upload attribution when upload records are enabled
/// (`BUZZ_MEDIA_UPLOAD_RECORDS`). Returns `None` when the feature is off —
/// the upload pipeline then writes no `_uploads/` record at all.
///
/// - `uploader_name` is the uploader's current display name in the bound
/// community (best-effort label; lookup failure degrades to absent).
/// - `net.ip` is read from the operator-configured trusted edge header
/// (`BUZZ_MEDIA_UPLOAD_IP_HEADER`) and validated as a public IP —
/// fail-empty: missing/malformed/non-public values record nothing. The
/// socket address is never used; behind a sidecar it is meaningless, and a
/// wrong address is worse than none.
/// - `net.port` (optional companion header) is only kept alongside a valid IP.
async fn upload_attribution(
state: &AppState,
auth: &AuthenticatedUpload,
headers: &HeaderMap,
) -> Option<UploadAttribution> {
let cfg = &state.config.media;
if !cfg.upload_records_enabled {
return None;
}
let uploader_name = state
.db
.get_user(auth.tenant.community(), &auth.auth_event.pubkey.to_bytes())
.await
.ok()
.flatten()
.and_then(|profile| profile.display_name)
.map(|name| name.trim().to_string())
.filter(|name| !name.is_empty());
let header_value = |name: &Option<String>| {
name.as_deref()
.and_then(|h| headers.get(h))
.and_then(|v| v.to_str().ok())
};
let ip = header_value(&cfg.upload_ip_header).and_then(buzz_media::parse_public_ip);
let port = ip.and(header_value(&cfg.upload_port_header).and_then(buzz_media::parse_port));
Some(UploadAttribution {
uploader_name,
net: UploadNetworkInfo { ip, port },
})
}
/// PUT /media/upload — Blossom BUD-02 upload.
///
/// Auth is validated via the [`AuthenticatedUpload`] extractor BEFORE the body
@@ -238,6 +284,8 @@ pub async fn upload_blob(
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let attribution = upload_attribution(&state, &auth, &headers).await;
let mut descriptor = if content_type.starts_with("video/") {
// Video path: stream body directly to disk — never fully buffered in RAM.
let content_length = headers
@@ -251,6 +299,7 @@ pub async fn upload_blob(
&auth.auth_event,
body.into_data_stream(),
content_length,
attribution,
)
.await?
} else {
@@ -280,6 +329,7 @@ pub async fn upload_blob(
&auth.tenant,
&auth.auth_event,
bytes,
attribution,
)
.await?
} else {
@@ -289,6 +339,7 @@ pub async fn upload_blob(
&auth.tenant,
&auth.auth_event,
bytes,
attribution,
)
.await?
}
@@ -914,6 +965,20 @@ mod tests {
assert!(validate_media_path("").is_err());
}
#[test]
fn test_validate_media_path_rejects_upload_record_keys() {
// The `_uploads/` per-event records (and `_meta/` sidecars) must be
// unreachable through the serve path. Axum's single path segment
// can't even contain `/`, but assert the validator rejects these
// shapes outright so the property survives any routing change.
assert!(validate_media_path("_uploads").is_err());
assert!(validate_media_path(&format!("_uploads/c/{VALID_HASH}/01J.json")).is_err());
assert!(validate_media_path("_meta").is_err());
assert!(validate_media_path(&format!("_meta/c/{VALID_HASH}.json")).is_err());
// Suffix-style metadata keys are also non-servable (>3 segments / bad ext).
assert!(validate_media_path(&format!("{VALID_HASH}.png.metadata")).is_err());
}
#[test]
fn media_base_url_for_tenant_uses_tenant_host_and_http_scheme() {
assert_eq!(