cratefield_module_privacy/
provider.rs1use cratefield_core::{Config, HttpClient, HttpError, HttpPolicy};
18use hmac::{Hmac, KeyInit, Mac};
19use http::header::CONTENT_TYPE;
20use serde_json::{Value, json};
21use sha2::{Digest, Sha256};
22use std::sync::Arc;
23use std::time::Duration;
24
25use crate::handlers::unix_now;
26
27pub const SIGNATURE_HEADER: &str = "Cratefield-Signature";
31
32const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
35
36const DEFAULT_MAX_RESPONSE_BYTES: usize = 1024 * 1024;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub(crate) enum ProviderError {
43 Unavailable,
45 Rejected,
47 InvalidResponse,
49 NotConfigured,
51}
52
53impl ProviderError {
54 pub(crate) const fn as_str(self) -> &'static str {
55 match self {
56 Self::Unavailable => "unavailable",
57 Self::Rejected => "rejected",
58 Self::InvalidResponse => "invalid_response",
59 Self::NotConfigured => "not_configured",
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
67pub struct HttpProvider {
68 name: String,
69 url: String,
70 secret_env: Option<String>,
71 timeout: Duration,
72 max_response_bytes: usize,
73 account: bool,
76}
77
78impl HttpProvider {
79 #[must_use]
82 pub fn new(name: impl Into<String>, url: impl Into<String>) -> Self {
83 Self {
84 name: name.into(),
85 url: url.into(),
86 secret_env: None,
87 timeout: DEFAULT_TIMEOUT,
88 max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
89 account: false,
90 }
91 }
92
93 #[must_use]
97 pub fn secret_env(mut self, name: impl Into<String>) -> Self {
98 self.secret_env = Some(name.into());
99 self
100 }
101
102 #[must_use]
104 pub fn timeout(mut self, timeout: Duration) -> Self {
105 self.timeout = timeout;
106 self
107 }
108
109 #[must_use]
112 pub fn max_response_bytes(mut self, max_response_bytes: usize) -> Self {
113 self.max_response_bytes = max_response_bytes;
114 self
115 }
116
117 #[must_use]
140 pub fn account(mut self) -> Self {
141 self.account = true;
142 self
143 }
144
145 #[must_use]
147 pub fn name(&self) -> &str {
148 &self.name
149 }
150
151 pub(crate) fn secret(&self, config: &dyn Config) -> Result<String, ProviderError> {
153 let name = self
154 .secret_env
155 .as_deref()
156 .ok_or(ProviderError::NotConfigured)?;
157 config
158 .get(name)
159 .filter(|secret| !secret.trim().is_empty())
160 .ok_or(ProviderError::NotConfigured)
161 }
162
163 pub(crate) async fn export(
165 &self,
166 http: &Arc<dyn HttpClient>,
167 config: &dyn Config,
168 subject: &str,
169 request_id: &str,
170 ) -> Result<Vec<Value>, ProviderError> {
171 let secret = self.secret(config)?;
172 let body = request_body(subject, request_id);
173 let response = self.post(http, "export", &secret, &body).await?;
174 provider_sections(response.body())
175 }
176
177 pub(crate) async fn erase_plan(
179 &self,
180 http: &Arc<dyn HttpClient>,
181 config: &dyn Config,
182 subject: &str,
183 request_id: &str,
184 ) -> Result<Vec<Value>, ProviderError> {
185 let secret = self.secret(config)?;
186 let body = request_body(subject, request_id);
187 let response = self.post(http, "erase/plan", &secret, &body).await?;
188 plan_sections(response.body())
189 }
190
191 pub(crate) async fn apply_signed(
194 &self,
195 http: &Arc<dyn HttpClient>,
196 secret: &str,
197 body: &bytes::Bytes,
198 ) -> Result<(), ProviderError> {
199 self.post(http, "erase/apply", secret, body).await?;
200 Ok(())
201 }
202
203 async fn post(
204 &self,
205 http: &Arc<dyn HttpClient>,
206 path: &str,
207 secret: &str,
208 body: &bytes::Bytes,
209 ) -> Result<http::Response<bytes::Bytes>, ProviderError> {
210 let policy = HttpPolicy {
211 max_response_bytes: self.max_response_bytes,
212 timeout: self.timeout,
213 }
214 .clamped();
215 let url = format!("{}/{path}", self.url.trim_end_matches('/'));
216 let mut request = http::Request::builder()
217 .method(http::Method::POST)
218 .uri(&url)
219 .header(CONTENT_TYPE, "application/json")
220 .header(SIGNATURE_HEADER, signature_header(secret, unix_now(), body))
221 .body(body.clone())
222 .map_err(|_| ProviderError::Unavailable)?;
223 request.extensions_mut().insert(policy);
224 let response = http.send(request).await.map_err(|err| match err {
225 HttpError::ResponseTooLarge { .. } => ProviderError::InvalidResponse,
227 _ => ProviderError::Unavailable,
228 })?;
229 check_status(&response)?;
230 if response.body().len() > policy.max_response_bytes {
234 return Err(ProviderError::InvalidResponse);
235 }
236 Ok(response)
237 }
238}
239
240pub(crate) fn in_erasure_order(providers: &[HttpProvider]) -> Vec<&HttpProvider> {
251 let (mut rest, account): (Vec<_>, Vec<_>) =
252 providers.iter().partition(|provider| !provider.account);
253 rest.extend(account);
254 rest
255}
256
257pub(crate) fn request_body(subject: &str, request_id: &str) -> bytes::Bytes {
259 bytes::Bytes::from(
260 serde_json::to_vec(&json!({ "subject": subject, "request_id": request_id }))
261 .expect("a JSON object serialises"),
262 )
263}
264
265fn signature_header(secret: &str, signed_at: u64, body: &[u8]) -> String {
268 let mut mac =
269 Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("HMAC accepts any key length");
270 mac.update(signed_at.to_string().as_bytes());
271 mac.update(b".");
272 mac.update(body);
273 format!(
274 "t={signed_at},v1={}",
275 hex::encode(mac.finalize().into_bytes())
276 )
277}
278
279fn check_status(response: &http::Response<bytes::Bytes>) -> Result<(), ProviderError> {
282 let status = response.status();
283 if status.is_success() {
284 Ok(())
285 } else if status.is_client_error() {
286 Err(ProviderError::Rejected)
287 } else {
288 Err(ProviderError::Unavailable)
289 }
290}
291
292fn sections(
294 body: &[u8],
295 render: impl Fn(&Value) -> Result<Value, ProviderError>,
296) -> Result<Vec<Value>, ProviderError> {
297 let value: Value = serde_json::from_slice(body).map_err(|_| ProviderError::InvalidResponse)?;
298 let Value::Object(mut object) = value else {
299 return Err(ProviderError::InvalidResponse);
300 };
301 let Some(Value::Array(sections)) = object.remove("sections") else {
302 return Err(ProviderError::InvalidResponse);
303 };
304 sections.iter().map(render).collect()
305}
306
307fn provider_sections(body: &[u8]) -> Result<Vec<Value>, ProviderError> {
310 sections(body, |section| {
311 let name = section
312 .get("name")
313 .and_then(Value::as_str)
314 .ok_or(ProviderError::InvalidResponse)?;
315 let data = section.get("data").ok_or(ProviderError::InvalidResponse)?;
316 let mut rendered = json!({ "name": name, "data": data });
317 match section.get("description") {
318 None => {}
319 Some(Value::String(description)) => rendered["description"] = json!(description),
320 Some(_) => return Err(ProviderError::InvalidResponse),
321 }
322 Ok(rendered)
323 })
324}
325
326fn plan_sections(body: &[u8]) -> Result<Vec<Value>, ProviderError> {
330 sections(body, |section| {
331 let name = section
332 .get("name")
333 .and_then(Value::as_str)
334 .ok_or(ProviderError::InvalidResponse)?;
335 match section.get("action").and_then(Value::as_str) {
336 Some(action @ ("delete" | "anonymise")) => {
337 Ok(json!({ "name": name, "action": action }))
338 }
339 Some("retain") => {
340 let reason = section
341 .get("reason")
342 .and_then(Value::as_str)
343 .filter(|reason| !reason.is_empty())
344 .ok_or(ProviderError::InvalidResponse)?;
345 Ok(json!({ "name": name, "action": "retain", "reason": reason }))
346 }
347 _ => Err(ProviderError::InvalidResponse),
348 }
349 })
350}
351
352pub(crate) fn erase_request_id(token: &str) -> String {
355 format!("erase_{}", &digest_hex(token.as_bytes())[..32])
356}
357
358pub(crate) fn export_request_id(subject: &str, now: u64) -> String {
361 let seed = format!("{subject}:{now}");
362 format!("export_{}", &digest_hex(seed.as_bytes())[..32])
363}
364
365fn digest_hex(bytes: &[u8]) -> String {
367 hex::encode(Sha256::digest(bytes))
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn a_signature_is_t_and_lowercase_hex_v1() {
376 let header = signature_header("secret", 1_800_000_000, b"{}");
377 let (t, sig) = header.split_once(",v1=").expect("t=…,v1=…");
378 assert_eq!(t, "t=1800000000");
379 assert_eq!(sig.len(), 64);
380 assert!(
381 sig.bytes()
382 .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
383 );
384 }
385
386 #[test]
387 fn response_sections_are_validated_and_rebuilt() {
388 for body in [
391 b"not json".as_slice(),
392 b"{}".as_slice(),
393 br#"{"sections":[{"name":"a"}]}"#.as_slice(),
394 br#"{"sections":[{"name":"a","data":1,"description":2}]}"#.as_slice(),
395 ] {
396 assert_eq!(provider_sections(body), Err(ProviderError::InvalidResponse));
397 }
398 for body in [
400 br#"{"sections":[{"name":"x","action":"shred"}]}"#.as_slice(),
401 br#"{"sections":[{"name":"invoices","action":"retain"}]}"#.as_slice(),
402 br#"{"sections":[{"name":"invoices","action":"retain","reason":""}]}"#.as_slice(),
403 ] {
404 assert_eq!(plan_sections(body), Err(ProviderError::InvalidResponse));
405 }
406
407 let body = br#"{"sections":[
409 {"name":"profile","action":"delete"},
410 {"name":"invoices","action":"retain","reason":"Tax law."}
411 ],"extra":"dropped"}"#;
412 let sections = plan_sections(body).expect("valid");
413 assert_eq!(sections[0], json!({"name":"profile","action":"delete"}));
414 assert_eq!(
415 sections[1],
416 json!({"name":"invoices","action":"retain","reason":"Tax law."})
417 );
418 }
419}