oxedyne/ore/cli/tests/guard.rs
15.9 KiB, 1 run
created by r2848102244:740, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! The guard that will not let a credential into a history nothing leaves. |
| 2 | //! |
| 3 | //! Every key here is spelled in two pieces and joined at run time, so that the scanners which read |
| 4 | //! this very file -- the git pre-commit hook, and the guard under test -- find nothing in it. |
| 5 | //! |
| 6 | //! The refusal is only worth having if the same fixture would otherwise be recorded, so each test |
| 7 | //! that asserts a refusal is paired with one asserting that the store is clean afterwards, and the |
| 8 | //! marker test asserts the opposite: that the excused line does reach a segment. Two states the |
| 9 | //! tests can tell apart is the whole point of them. |
| 10 | |
| 11 | mod support; |
| 12 | |
| 13 | use support::{ |
| 14 | ore, |
| 15 | write, |
| 16 | Scratch, |
| 17 | }; |
| 18 | |
| 19 | use oxedyne_fe2o3_core::prelude::*; |
| 20 | |
| 21 | use std::fs; |
| 22 | use std::os::unix::fs::MetadataExt; |
| 23 | use std::path::Path; |
| 24 | use std::process::Command; |
| 25 | |
| 26 | |
| 27 | // A key of a shape that is a credential and nothing else, and a second one so that a test can add |
| 28 | // a credential to a file already holding one. Both in two pieces. |
| 29 | const KEY: (&str, &str) = ("fw", "_3ZjKq81mAbCdEfGhIjKlMnOpQrSt"); |
| 30 | const OTHER: (&str, &str) = ("sk-ant", "-api03-AbCdEfGhIjKlMnOpQrStUvWx"); |
| 31 | |
| 32 | |
| 33 | /// The whole of a key, as it stands in a file. |
| 34 | fn key(pieces: (&str, &str)) -> String { |
| 35 | fmt!("{}{}", pieces.0, pieces.1) |
| 36 | } |
| 37 | |
| 38 | /// A line of source assigning one. |
| 39 | fn line(pieces: (&str, &str)) -> String { |
| 40 | fmt!("let key = \"{}\";\n", key(pieces)) |
| 41 | } |
| 42 | |
| 43 | /// Does the store hold these bytes anywhere at all? |
| 44 | /// |
| 45 | /// The question the whole guard exists to answer no to. Asked of the bytes on disk rather than |
| 46 | /// through a verb, because there is no verb that does not capture: asking one would write. |
| 47 | fn in_store(root: &Path, needle: &[u8]) |
| 48 | -> Outcome<bool> |
| 49 | { |
| 50 | found(&root.join(".ore"), needle) |
| 51 | } |
| 52 | |
| 53 | /// Reads a directory, recursing, looking for the bytes. |
| 54 | fn found(dir: &Path, needle: &[u8]) |
| 55 | -> Outcome<bool> |
| 56 | { |
| 57 | for entry in res!(fs::read_dir(dir)) { |
| 58 | let entry = res!(entry); |
| 59 | let kind = res!(entry.file_type()); |
| 60 | if kind.is_dir() { |
| 61 | if res!(found(&entry.path(), needle)) { |
| 62 | return Ok(true); |
| 63 | } |
| 64 | continue; |
| 65 | } |
| 66 | if !kind.is_file() { |
| 67 | continue; |
| 68 | } |
| 69 | let bytes = res!(fs::read(entry.path())); |
| 70 | if bytes.windows(needle.len()).any(|w| w == needle) { |
| 71 | return Ok(true); |
| 72 | } |
| 73 | } |
| 74 | Ok(false) |
| 75 | } |
| 76 | |
| 77 | /// Prepares a repository holding one ordinary file, with everything captured. |
| 78 | fn started(scratch: &Scratch, name: &str) |
| 79 | -> Outcome<std::path::PathBuf> |
| 80 | { |
| 81 | let root = res!(scratch.sub(name)); |
| 82 | res!(write(&root, "main.rs", b"fn main() {\n\tprintln!(\"hello\");\n}\n")); |
| 83 | res!(res!(ore(&root, &["init"])).good("init")); |
| 84 | Ok(root) |
| 85 | } |
| 86 | |
| 87 | |
| 88 | /// A key in the working copy refuses the next verb, and reaches no segment. |
| 89 | #[test] |
| 90 | fn a_credential_refuses_the_verb_and_is_not_recorded() -> Outcome<()> { |
| 91 | let scratch = res!(Scratch::new("guard_refuse")); |
| 92 | let root = res!(started(&scratch, "repo")); |
| 93 | res!(write(&root, "main.rs", |
| 94 | fmt!("fn main() {{}}\n{}", line(KEY)).as_bytes())); |
| 95 | |
| 96 | let ran = res!(ore(&root, &["log"])); |
| 97 | assert!(!ran.ok, "the verb was refused: {}{}", ran.out, ran.err); |
| 98 | assert!(ran.err.contains("main.rs:2"), |
| 99 | "and it names the file and the line: {}", ran.err); |
| 100 | assert!(ran.err.contains("Fireworks key"), |
| 101 | "and what it found: {}", ran.err); |
| 102 | assert!(!ran.err.contains(&key(KEY)), |
| 103 | "and never the value itself: {}", ran.err); |
| 104 | assert!(!res!(in_store(&root, key(KEY).as_bytes())), |
| 105 | "and nothing of it reached the store"); |
| 106 | Ok(()) |
| 107 | } |
| 108 | |
| 109 | /// Every verb refuses, because every verb captures. |
| 110 | #[test] |
| 111 | fn no_verb_is_a_way_round_it() -> Outcome<()> { |
| 112 | let scratch = res!(Scratch::new("guard_verbs")); |
| 113 | let root = res!(started(&scratch, "repo")); |
| 114 | res!(write(&root, "main.rs", fmt!("fn main() {{}}\n{}", line(KEY)).as_bytes())); |
| 115 | |
| 116 | for verb in [ |
| 117 | vec!["log"], |
| 118 | vec!["flags"], |
| 119 | vec!["who", "main.rs"], |
| 120 | vec!["mark", "here"], |
| 121 | vec!["undo"], |
| 122 | ] { |
| 123 | let ran = res!(ore(&root, &verb)); |
| 124 | assert!(!ran.ok, "`ore {}` was refused: {}{}", verb.join(" "), ran.out, ran.err); |
| 125 | } |
| 126 | assert!(!res!(in_store(&root, key(KEY).as_bytes())), |
| 127 | "and none of them recorded it"); |
| 128 | Ok(()) |
| 129 | } |
| 130 | |
| 131 | /// The marker excuses the line, and taking it away un-excuses it. |
| 132 | /// |
| 133 | /// The half that proves the rest: the excused line is captured, so the fixture really is one the |
| 134 | /// guard would otherwise have stopped. |
| 135 | #[test] |
| 136 | fn the_marker_excuses_a_line_and_only_while_it_is_there() -> Outcome<()> { |
| 137 | let scratch = res!(Scratch::new("guard_marker")); |
| 138 | let root = res!(started(&scratch, "repo")); |
| 139 | let marked = fmt!("fn main() {{}}\nlet key = \"{}\"; // allowlist secret\n", key(KEY)); |
| 140 | res!(write(&root, "main.rs", marked.as_bytes())); |
| 141 | |
| 142 | let ran = res!(ore(&root, &["log"])); |
| 143 | assert!(ran.ok, "a fixture the author marked is captured: {}{}", ran.out, ran.err); |
| 144 | assert!(res!(in_store(&root, key(KEY).as_bytes())), |
| 145 | "and it is in the store, which is what makes the refusals elsewhere mean something"); |
| 146 | |
| 147 | // The same bytes without the marker are a line the history has never seen. |
| 148 | res!(write(&root, "main.rs", fmt!("fn main() {{}}\n{}", line(KEY)).as_bytes())); |
| 149 | let ran = res!(ore(&root, &["log"])); |
| 150 | assert!(!ran.ok, "and without it the line is refused again: {}{}", ran.out, ran.err); |
| 151 | Ok(()) |
| 152 | } |
| 153 | |
| 154 | /// A placeholder is not a credential. |
| 155 | /// |
| 156 | /// The rule that decides whether the guard survives the week. A documentation example refused is a |
| 157 | /// guard somebody switches off, and a guard switched off protects nothing. |
| 158 | #[test] |
| 159 | fn a_placeholder_is_not_refused() -> Outcome<()> { |
| 160 | let scratch = res!(Scratch::new("guard_placeholder")); |
| 161 | let root = res!(started(&scratch, "repo")); |
| 162 | res!(write(&root, "README.md", b"Set it yourself:\n\n api_key = \"your-key-here\"\n")); |
| 163 | res!(write(&root, "cfg.toml", b"token = \"PLACEHOLDER_VALUE_HERE_OK\"\n")); |
| 164 | |
| 165 | let ran = res!(ore(&root, &["log"])); |
| 166 | assert!(ran.ok, "an example nobody has filled in is captured: {}{}", ran.out, ran.err); |
| 167 | Ok(()) |
| 168 | } |
| 169 | |
| 170 | /// A lockfile is not scanned, because its hashes read like keys. |
| 171 | #[test] |
| 172 | fn a_lockfile_is_left_alone() -> Outcome<()> { |
| 173 | let scratch = res!(Scratch::new("guard_lock")); |
| 174 | let root = res!(started(&scratch, "repo")); |
| 175 | res!(write(&root, "Cargo.lock", fmt!("checksum = \"{}\"\n", key(KEY)).as_bytes())); |
| 176 | |
| 177 | let ran = res!(ore(&root, &["log"])); |
| 178 | assert!(ran.ok, "the lockfile is captured: {}{}", ran.out, ran.err); |
| 179 | Ok(()) |
| 180 | } |
| 181 | |
| 182 | /// A marker on the line above, taken away, leaves the line itself unchanged -- and unchanged is |
| 183 | /// what the history already holds. |
| 184 | /// |
| 185 | /// The other half of the marker's story, and deliberately not the same answer as |
| 186 | /// [`the_marker_excuses_a_line_and_only_while_it_is_there`]. Taking a *trailing* marker off a line |
| 187 | /// rewrites that line, so it is a line the history has never seen and it is refused. Taking away a |
| 188 | /// marker that sat on the line *above* leaves the credential line byte for byte as the history |
| 189 | /// already carries it, so it is not what this command would be introducing. |
| 190 | /// |
| 191 | /// The asymmetry is the price of matching on the exact line, and the exact line is worth the |
| 192 | /// price: anything looser -- the same file, the same shape of credential -- would let the value |
| 193 | /// itself be edited freely, which is a new key every time. |
| 194 | #[test] |
| 195 | fn dropping_a_marker_from_the_line_above_is_not_introducing_the_line() -> Outcome<()> { |
| 196 | let scratch = res!(Scratch::new("guard_above")); |
| 197 | let root = res!(started(&scratch, "repo")); |
| 198 | res!(write(&root, "lib.rs", |
| 199 | fmt!("one\ntwo\n// allowlist secret\n{}", line(KEY)).as_bytes())); |
| 200 | let ran = res!(ore(&root, &["log"])); |
| 201 | assert!(ran.ok, "the marked fixture is captured: {}{}", ran.out, ran.err); |
| 202 | assert!(res!(in_store(&root, key(KEY).as_bytes())), |
| 203 | "and it is in the log, which is what the rest of this rests on"); |
| 204 | |
| 205 | res!(write(&root, "lib.rs", fmt!("one\ntwo\n{}", line(KEY)).as_bytes())); |
| 206 | let ran = res!(ore(&root, &["log"])); |
| 207 | assert!(ran.ok, "and dropping the marker line is captured: {}{}", ran.out, ran.err); |
| 208 | Ok(()) |
| 209 | } |
| 210 | |
| 211 | /// A credential a peer sent does not brick the file it landed in, and a new one still refuses. |
| 212 | /// |
| 213 | /// This is the route the carve-out exists for. A capture is guarded and an import is guarded, so |
| 214 | /// the one way a credential still reaches a log its owner did not write is `ore sync`: a peer's |
| 215 | /// operations were authored and signed on their machine and arrive whole, and refusing one would |
| 216 | /// be refusing their history. Nothing at this end can prevent it. |
| 217 | /// |
| 218 | /// What this end can decide is what happens next. While the file is untouched nothing scans it at |
| 219 | /// all -- the capture skips a file whose bytes are what the history says -- so the cost falls |
| 220 | /// exactly where the carve-out is: on the owner's next edit to that file. Without it a peer could |
| 221 | /// forbid, permanently and from across a relay, any further edit to any file they put a credential |
| 222 | /// in. |
| 223 | #[test] |
| 224 | fn a_line_a_peer_sent_does_not_brick_the_file() -> Outcome<()> { |
| 225 | let scratch = res!(Scratch::new("guard_peer")); |
| 226 | |
| 227 | // The peer arrives at an unmarked credential the only way anybody can: marked, captured, and |
| 228 | // then the marker taken off the line above it. |
| 229 | let peer = res!(scratch.sub("peer")); |
| 230 | res!(write(&peer, "lib.rs", |
| 231 | fmt!("one\ntwo\n// allowlist secret\n{}", line(KEY)).as_bytes())); |
| 232 | res!(res!(ore(&peer, &["init"])).good("init")); |
| 233 | res!(res!(ore(&peer, &["log"])).good("log")); |
| 234 | res!(write(&peer, "lib.rs", fmt!("one\ntwo\n{}", line(KEY)).as_bytes())); |
| 235 | res!(res!(ore(&peer, &["log"])).good("log")); |
| 236 | |
| 237 | let mine = res!(started(&scratch, "mine")); |
| 238 | res!(res!(ore(&mine, &["sync", &fmt!("{}", peer.display())])).good("sync")); |
| 239 | assert!(res!(in_store(&mine, key(KEY).as_bytes())), |
| 240 | "the sync put the peer's credential in my log, which I had no way to refuse"); |
| 241 | let landed = res!(fs::read(mine.join("lib.rs"))); |
| 242 | assert!(landed.windows(key(KEY).len()).any(|w| w == key(KEY).as_bytes()), |
| 243 | "and in my working copy"); |
| 244 | |
| 245 | // Untouched, the file is never scanned, so this says nothing about the carve-out. |
| 246 | let ran = res!(ore(&mine, &["log"])); |
| 247 | assert!(ran.ok, "a verb with nothing changed runs: {}{}", ran.out, ran.err); |
| 248 | |
| 249 | // Editing it is the moment the carve-out is the only thing standing between a peer and a file |
| 250 | // I can no longer touch. |
| 251 | res!(write(&mine, "lib.rs", fmt!("one\ntwo\nthree\n{}", line(KEY)).as_bytes())); |
| 252 | let ran = res!(ore(&mine, &["log"])); |
| 253 | assert!(ran.ok, "and my own edit to that file is captured: {}{}", ran.out, ran.err); |
| 254 | |
| 255 | // And the carve-out is not a hole: a line the file did not already carry is refused. |
| 256 | res!(write(&mine, "lib.rs", |
| 257 | fmt!("one\ntwo\nthree\n{}{}", line(KEY), line(OTHER)).as_bytes())); |
| 258 | let ran = res!(ore(&mine, &["log"])); |
| 259 | assert!(!ran.ok, "a new credential in that same file is refused: {}{}", ran.out, ran.err); |
| 260 | assert!(ran.err.contains("Anthropic key"), "and named: {}", ran.err); |
| 261 | assert!(!res!(in_store(&mine, key(OTHER).as_bytes())), |
| 262 | "and it reached no segment"); |
| 263 | Ok(()) |
| 264 | } |
| 265 | |
| 266 | /// Runs `touch` with the arguments given, which is how a test forges a timestamp. |
| 267 | /// |
| 268 | /// There is no way to set a modification time through `std`, and this file will not take a |
| 269 | /// dependency for one. `touch` is the same tool somebody would reach for by hand, which is the |
| 270 | /// point: the case being built is what a restore, a build system or a person with `touch -r` does. |
| 271 | fn touch(dir: &Path, args: &[&str]) |
| 272 | -> Outcome<()> |
| 273 | { |
| 274 | let out = match Command::new("touch").current_dir(dir).args(args).output() { |
| 275 | Ok(o) => o, |
| 276 | Err(e) => return Err(err!(e, "`touch {}` could not be run.", args.join(" "); Test, IO)), |
| 277 | }; |
| 278 | match out.status.success() { |
| 279 | true => Ok(()), |
| 280 | false => Err(err!("`touch {}` failed: {}", args.join(" "), |
| 281 | String::from_utf8_lossy(&out.stderr); Test, IO)), |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | /// Runs two verbs over an unchanged working copy, which is what leaves an index behind. |
| 286 | /// |
| 287 | /// The first capture records what is there and writes no index -- a capture that appended |
| 288 | /// something has moved the history and cannot say the disk matches it. The second appends nothing, |
| 289 | /// and that is the one moment the index may be written. |
| 290 | fn arm_the_index(root: &Path) |
| 291 | -> Outcome<()> |
| 292 | { |
| 293 | res!(res!(ore(root, &["log"])).good("log")); |
| 294 | res!(res!(ore(root, &["log"])).good("log")); |
| 295 | assert!(root.join(".ore").join("stat").is_file(), |
| 296 | "the working copy index was not written, so this test is not exercising the fast path \ |
| 297 | it exists for; if the index moved or was renamed, this precondition is what needs \ |
| 298 | changing"); |
| 299 | Ok(()) |
| 300 | } |
| 301 | |
| 302 | /// An index saying the working copy is clean does not carry a credential past the guard. |
| 303 | /// |
| 304 | /// `capture` skips the render entirely where a stat index says every file is where it was, and the |
| 305 | /// guard runs after the render. So the guard's whole contract now rests on a claim it does not |
| 306 | /// make itself: that a file which arrived, or changed, is never one the index calls clean. |
| 307 | /// |
| 308 | /// Three ways of arriving, and the third is the one worth the test. A same-size write with the |
| 309 | /// modification time put back leaves the two fields a conventional index compares -- size and |
| 310 | /// mtime -- exactly as they were recorded, which the assertions below check rather than assume. If |
| 311 | /// that file is ever believed, a credential reaches a log that cannot forget it and no test but |
| 312 | /// this one would notice. |
| 313 | #[test] |
| 314 | fn an_index_that_says_clean_does_not_carry_a_credential_past() -> Outcome<()> { |
| 315 | let scratch = res!(Scratch::new("guard_index")); |
| 316 | let root = res!(scratch.sub("repo")); |
| 317 | // Long enough to hold the credential later without changing size, and shaped like nothing. |
| 318 | let decoy = fmt!("let key = \"qq_{}\";\n", &key(KEY)[3..]); |
| 319 | res!(write(&root, "lib.rs", b"one\ntwo\n")); |
| 320 | res!(write(&root, "swap.rs", decoy.as_bytes())); |
| 321 | // Older than the index's own reading of the clock, so that the entries are believed rather |
| 322 | // than re-read for being too recent to mean anything. |
| 323 | res!(touch(&root, &["-d", "1 hour ago", "lib.rs", "swap.rs"])); |
| 324 | res!(res!(ore(&root, &["init"])).good("init")); |
| 325 | res!(arm_the_index(&root)); |
| 326 | |
| 327 | // A path the index has never heard of. |
| 328 | res!(write(&root, "arrived.rs", line(KEY).as_bytes())); |
| 329 | let ran = res!(ore(&root, &["log"])); |
| 330 | assert!(!ran.ok, "a file that arrived is refused: {}{}", ran.out, ran.err); |
| 331 | assert!(ran.err.contains("arrived.rs:1"), "and named: {}", ran.err); |
| 332 | res!(fs::remove_file(root.join("arrived.rs"))); |
| 333 | res!(touch(&root, &["-d", "1 hour ago", "lib.rs", "swap.rs"])); |
| 334 | res!(arm_the_index(&root)); |
| 335 | |
| 336 | // A path it knows, taken away and put back under the same name. |
| 337 | res!(fs::remove_file(root.join("swap.rs"))); |
| 338 | res!(write(&root, "swap.rs", line(KEY).as_bytes())); |
| 339 | let ran = res!(ore(&root, &["log"])); |
| 340 | assert!(!ran.ok, "a path removed and re-created is refused: {}{}", ran.out, ran.err); |
| 341 | assert!(ran.err.contains("swap.rs:1"), "and named: {}", ran.err); |
| 342 | res!(write(&root, "swap.rs", decoy.as_bytes())); |
| 343 | res!(touch(&root, &["-d", "1 hour ago", "lib.rs", "swap.rs"])); |
| 344 | res!(arm_the_index(&root)); |
| 345 | |
| 346 | // And the one a size and a time cannot tell apart: the same number of bytes, written in |
| 347 | // place so the inode is kept, with the modification time put back afterwards. |
| 348 | let before = res!(fs::metadata(root.join("swap.rs"))); |
| 349 | let planted = line(KEY); |
| 350 | req!(planted.len(), decoy.len(), "the fixture only tests what it means to if the two lines \ |
| 351 | are the same length"); |
| 352 | // The reference the time is copied from lives OUTSIDE the working copy. Put it inside and it |
| 353 | // is a file that arrived, the index does not know it, and the capture takes the slow path for |
| 354 | // that reason alone -- which would leave this case proving nothing about the swap. |
| 355 | let stamp = scratch.path.join("stamp.ref"); |
| 356 | res!(fs::write(&stamp, b"")); |
| 357 | res!(touch(&root, &["-r", "swap.rs", &fmt!("{}", stamp.display())])); |
| 358 | res!(write(&root, "swap.rs", planted.as_bytes())); |
| 359 | res!(touch(&root, &["-r", &fmt!("{}", stamp.display()), "swap.rs"])); |
| 360 | let after = res!(fs::metadata(root.join("swap.rs"))); |
| 361 | req!(after.len(), before.len(), "the size is what it was"); |
| 362 | req!(after.mtime(), before.mtime(), "and so is the modification time, to the second"); |
| 363 | req!(after.mtime_nsec(), before.mtime_nsec(), "and to the nanosecond"); |
| 364 | req!(after.ino(), before.ino(), "and it is the same file, not a replacement"); |
| 365 | |
| 366 | let ran = res!(ore(&root, &["log"])); |
| 367 | assert!(!ran.ok, |
| 368 | "a credential written over the same number of bytes, with the time put back, is still \ |
| 369 | refused: {}{}", ran.out, ran.err); |
| 370 | assert!(ran.err.contains("swap.rs:1"), "and named: {}", ran.err); |
| 371 | assert!(!res!(in_store(&root, key(KEY).as_bytes())), |
| 372 | "and none of the three reached a segment"); |
| 373 | Ok(()) |
| 374 | } |