Skip to main content

max / makenotwork

5.8 KB · 134 lines History Blame Raw
1 //! Two grep-based lints over `src/routes/`, not unit tests: one proves every
2 //! bucket delete goes through the authority, the other that no served key is
3 //! built by hand. Both resolve their target from `CARGO_MANIFEST_DIR`, so
4 //! moving this file does not move what they scan.
5
6 use std::path::Path;
7
8 /// Hand every `.rs` file under `dir` to `f`, with its path and its contents.
9 /// Both seals below are a grep over the same tree, so they share the walk.
10 fn walk(dir: &Path, f: &mut impl FnMut(&Path, &str)) {
11 let Ok(entries) = std::fs::read_dir(dir) else {
12 return;
13 };
14 for entry in entries.flatten() {
15 let path = entry.path();
16 if path.is_dir() {
17 walk(&path, f);
18 } else if path.extension().is_some_and(|e| e == "rs")
19 && let Ok(contents) = std::fs::read_to_string(&path)
20 {
21 f(&path, &contents);
22 }
23 }
24 }
25
26 /// Build-time enforcement: route handlers must never
27 /// delete S3 objects directly, nor mint an [`S3DeleteAuthority`]. Direct
28 /// deletion is for the sanctioned durable-deletion paths (`scheduler/cleanup.rs`,
29 /// `scanning/worker.rs`) only; handlers enqueue through `pending_s3_deletions`.
30 ///
31 /// The type system already makes the accidental `s3.delete_object(key)`
32 /// uncompilable (the delete methods require an authority handlers can't reach).
33 /// This test closes the deliberate-circumvention gap: it fails the build if any
34 /// file under `src/routes/` names a delete method or the authority type, so the
35 /// seal cannot silently erode in a future handler.
36 #[cfg(test)]
37 mod delete_seal_guard {
38 use super::walk;
39 use std::path::Path;
40
41 #[test]
42 fn routes_never_delete_s3_directly() {
43 let routes_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/routes");
44 let mut offenders = Vec::new();
45 walk(&routes_dir, &mut |path, contents| {
46 for (i, line) in contents.lines().enumerate() {
47 // Skip comment/doc lines (they legitimately mention the API).
48 if line.trim_start().starts_with("//") {
49 continue;
50 }
51 if line.contains(".delete_object(")
52 || line.contains(".delete_objects(")
53 || line.contains(".delete_prefix(")
54 || line.contains("S3DeleteAuthority")
55 {
56 offenders.push(format!("{}:{}: {}", path.display(), i + 1, line.trim()));
57 }
58 }
59 });
60 assert!(
61 offenders.is_empty(),
62 "CHRONIC B' seal violated, route code must enqueue via \
63 routes::storage::enqueue_s3_orphan, never delete S3 directly or mint an \
64 S3DeleteAuthority. Offending lines:\n{}",
65 offenders.join("\n")
66 );
67 }
68 }
69
70 /// C1 scan-then-promote seal: route handlers must never mint a *served* S3 key.
71 ///
72 /// A presigned client upload can only ever land at a `staging/{uuid}` key
73 /// ([`S3Client::generate_staging_key`]); the served, content-addressed key
74 /// ([`S3Client::content_key`]) is created in exactly one place, the scan
75 /// worker's promote step, after a Clean verdict, so the bytes a buyer is served
76 /// are provably the bytes that were scanned. The mutable-served-key class (a
77 /// presign minting `{user}/{item}/type/filename`, then the owner re-PUTting to it
78 /// after it goes Clean) is what this closes.
79 ///
80 /// This guard fails the build if any file under `src/routes/` names a served-key
81 /// generator or `content_key`. It is stronger than `pub(crate)` visibility,
82 /// route code lives in the same crate, so `pub(crate)` would not stop it from
83 /// calling these, and it is the same grep-proof discipline as the delete seal
84 /// above. (The build runner uploads OTA artifacts server-side to a deterministic
85 /// key via `generate_ota_artifact_key`; it lives outside `src/routes/`, so it is
86 /// legitimately unaffected.)
87 #[cfg(test)]
88 mod served_key_seal_guard {
89 use super::walk;
90 use std::path::Path;
91
92 #[test]
93 fn routes_never_mint_served_keys() {
94 let routes_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/routes");
95 // Every served-key generator, plus the content-key minter. `staging`
96 // keys are the ONLY key a route may mint, so `generate_staging_key` is
97 // deliberately absent from this list.
98 // Anchored to the `S3Client::` call prefix so an unrelated `generate_key`
99 // (e.g. `license_keys::generate_key`, `helpers::generate_key_code`) is not
100 // a false positive, only the storage generators are S3Client methods.
101 const FORBIDDEN: &[&str] = &[
102 "S3Client::generate_key(",
103 "S3Client::generate_version_key(",
104 "S3Client::generate_insertion_key(",
105 "S3Client::generate_media_key(",
106 "S3Client::generate_project_image_key(",
107 "S3Client::generate_ota_artifact_key(",
108 "S3Client::generate_item_gallery_key(",
109 "S3Client::generate_project_gallery_key(",
110 "S3Client::content_key(",
111 ];
112 let mut offenders = Vec::new();
113 walk(&routes_dir, &mut |path, contents| {
114 for (i, line) in contents.lines().enumerate() {
115 if line.trim_start().starts_with("//") {
116 continue;
117 }
118 for needle in FORBIDDEN {
119 if line.contains(needle) {
120 offenders.push(format!("{}:{}: {}", path.display(), i + 1, line.trim()));
121 }
122 }
123 }
124 });
125 assert!(
126 offenders.is_empty(),
127 "C1 seal violated, route handlers must presign only `generate_staging_key`; \
128 the served/content key is minted solely by the scan worker's promote step. \
129 Offending lines:\n{}",
130 offenders.join("\n")
131 );
132 }
133 }
134