code wiki / (root) / nx_site_publish_gate.nx

nx_site_publish_gate.nx source

↩ module page · 215 lines · 14758 B

1// nx_site_publish_gate.nx -- gates artifact-publish placement semantics on a THROWAWAY CWD (the docroot 2// is CWD-relative sites/): fresh publish lands byte-identical; republish preserves .prev AND lands v2; 3// traversal ('..', absolute) DENIED with NOTHING written; secret-needle relpath DENIED via the IO layer; 4// absent source honest ABSENT. Runner: mkdir a scratch dir, cd into it, run. Exit 0 only on all-PASS. 5// license_tier: ORIGINAL expect_exit: 0 6import "nx_site_publish_lib.nx" 7import "nx_seg_store.nx" // ss_writefile -- fixture author 8import "nx_gate_verdict.nx" 9 10const SG_CASES: *u8 = "fresh-publish-lands\nfirst-publish-retry-does-not-create-prev\nrepublish-preserves-prev\nchanged-publish-retry-retains-rollback\nbinary-difference-after-nul-and-retry\nrelpath-traversal-denied\nabsolute-site-denied\nordinary-key-name-allowed\nsecret-relpath-denied\nabsent-source-honest\nmissing-parent-dir-fails-loud\nguarded-create-exact\nwrong-artifact-pin-retains-live\nstale-live-pin-retains-live\nguarded-change-retains-predecessor\nguarded-retry-original-precondition\nguarded-reverse-retains-replaced-version\ncontended-target-refuses-without-wait\nreleased-lock-allows-retry\noversized-path-refused-before-construction\nstage-wrong-pin-creates-no-candidate\nstage-exact-candidate\nstage-retry-preserves-candidate\nstage-replacement-refused\nlegacy-publish-cannot-overwrite-staging\nguarded-publish-cannot-overwrite-staging\nstage-refuses-live-route\nstaging-alias-overwrites-refused\nregistered-site-config-written\nregistered-site-plane-seeded\nunregistered-stage-refused-without-placement\nrelease-identity-registered\nlegacy-publish-cannot-ignore-registered-hash\nregistered-stage-places-exact-bytes\nregistered-stage-retry-keeps-predecessor-absent\nconflicting-registration-cannot-authorize-replacement\n" as *u8 11static sg_plan: *i64 12const SG_ARG_CELL_BYTES: i64 = 8 // native x86-64 argv uses i64 pointer cells 13const SG_ARGV_BYTES: i64 = (PB_GUARDED_ARGC + 1) * SG_ARG_CELL_BYTES 14 15func sg_check(name: *u8, ok: i64, pass: *i64) -> i64 { 16 return gv_plan_check(sg_plan,name,ok,pass) 17} 18func sg_run(src: *u8, site: *u8, rel: *u8) -> i64 { 19 let av: *i64 = sys_mmap(SG_ARGV_BYTES) as *i64 20 av[0] = "nx_site_publish" as *u8 as i64 21 av[PB_ARG_VERB] = "publish" as *u8 as i64 22 av[PB_ARG_SRC] = src as i64 23 av[PB_ARG_SITE] = site as i64 24 av[PB_ARG_REL] = rel as i64 25 let rc: i64 = sp_main(PB_ARGC,av) 26 sys_munmap(av as *u8,SG_ARGV_BYTES) 27 return rc 28} 29func sg_eq(path: *u8, expect: *u8) -> i64 { 30 return sg_eq_bytes(path,expect,vw_slen(expect)) 31} 32func sg_eq_bytes(path: *u8, expect: *u8, expected_bytes: i64) -> i64 { 33 // One extra byte detects unexpected trailing content instead of accepting a prefix. 34 let read_bytes: i64 = expected_bytes+1 35 let b: *u8 = sys_mmap(read_bytes) 36 let n: i64 = vw_read(path,b,read_bytes) 37 let same: i64 = pb_same_bytes(b,n,expect,expected_bytes) 38 sys_munmap(b,read_bytes) 39 return same 40} 41func sg_absent(path: *u8) -> i64 { 42 let fd: i64 = sys_openat_rd(path) 43 // Linux ENOENT establishes absence; permission and other I/O failures do not. 44 if fd == (0-2) { return 1 } 45 if fd >= 0 { sys_close(fd) } 46 return 0 47} 48 49func main(argc: i64, argv: *i64) -> i64 { 50 // An explicit scratch directory must be new; refuse to run fixtures over an existing tree. 51 if argc > 1 { 52 let scratch: *u8 = argv[1] as *u8 53 if sys_mkdir(scratch, 0x1ed) != 0 { return 2 } 54 if sys_chdir(scratch) != 0 { return 2 } 55 } 56 let pass: *i64 = gv_ctr() 57 sg_plan = gv_plan_new(SG_CASES) 58 sys_mkdir("sites" as *u8, 0x1ed) 59 sys_mkdir("sites/gatesite" as *u8, 0x1ed) 60 ss_writefile("v1.html" as *u8, "<h1>one</h1>" as *u8, vw_slen("<h1>one</h1>" as *u8)) 61 ss_writefile("v2.html" as *u8, "<h1>two</h1>" as *u8, vw_slen("<h1>two</h1>" as *u8)) 62 // T1 fresh publish lands byte-identical, no .prev 63 var ok1: i64 = 0 64 if sg_run("v1.html" as *u8, "gatesite" as *u8, "page.html" as *u8) == 0 { 65 if sg_eq("sites/gatesite/page.html" as *u8, "<h1>one</h1>" as *u8) == 1 { 66 ok1 = sg_absent("sites/gatesite/page.html.prev" as *u8) 67 } 68 } 69 sg_check("fresh-publish-lands" as *u8, ok1, pass) 70 var retry1: i64 = 0 71 if sg_run("v1.html" as *u8, "gatesite" as *u8, "page.html" as *u8) == 0 { 72 if sg_eq("sites/gatesite/page.html" as *u8, "<h1>one</h1>" as *u8) == 1 { 73 retry1 = sg_absent("sites/gatesite/page.html.prev" as *u8) 74 } 75 } 76 sg_check("first-publish-retry-does-not-create-prev" as *u8, retry1, pass) 77 // T2 republish: .prev holds v1, target holds v2 78 var ok2: i64 = 0 79 if sg_run("v2.html" as *u8, "gatesite" as *u8, "page.html" as *u8) == 0 { 80 if sg_eq("sites/gatesite/page.html" as *u8, "<h1>two</h1>" as *u8) == 1 { 81 ok2 = sg_eq("sites/gatesite/page.html.prev" as *u8, "<h1>one</h1>" as *u8) 82 } 83 } 84 sg_check("republish-preserves-prev" as *u8, ok2, pass) 85 var retry2: i64 = 0 86 if sg_run("v2.html" as *u8, "gatesite" as *u8, "page.html" as *u8) == 0 { 87 if sg_eq("sites/gatesite/page.html" as *u8, "<h1>two</h1>" as *u8) == 1 { 88 retry2 = sg_eq("sites/gatesite/page.html.prev" as *u8, "<h1>one</h1>" as *u8) 89 } 90 } 91 sg_check("changed-publish-retry-retains-rollback" as *u8, retry2, pass) 92 let binary_bytes: i64 = 3 // fixture is exactly [A, NUL, B/C] 93 let binary1: *u8 = sys_mmap(binary_bytes) 94 let binary2: *u8 = sys_mmap(binary_bytes) 95 binary1[0] = 65 as u8; binary1[1] = 0 as u8; binary1[2] = 66 as u8 96 binary2[0] = 65 as u8; binary2[1] = 0 as u8; binary2[2] = 67 as u8 97 ss_writefile("binary1" as *u8, binary1, binary_bytes) 98 ss_writefile("binary2" as *u8, binary2, binary_bytes) 99 var binary_ok: i64 = 0 100 if sg_run("binary1" as *u8, "gatesite" as *u8, "asset.wasm" as *u8) == 0 { 101 if sg_run("binary2" as *u8, "gatesite" as *u8, "asset.wasm" as *u8) == 0 { 102 if sg_run("binary2" as *u8, "gatesite" as *u8, "asset.wasm" as *u8) == 0 { 103 if sg_eq_bytes("sites/gatesite/asset.wasm" as *u8,binary2,binary_bytes) == 1 { 104 binary_ok = sg_eq_bytes("sites/gatesite/asset.wasm.prev" as *u8,binary1,binary_bytes) 105 } 106 } 107 } 108 } 109 sg_check("binary-difference-after-nul-and-retry" as *u8, binary_ok, pass) 110 sys_munmap(binary1,binary_bytes); sys_munmap(binary2,binary_bytes) 111 // T3 traversal in relpath DENIED, nothing written outside 112 var ok3: i64 = 0 113 if sg_run("v1.html" as *u8, "gatesite" as *u8, "../escape.html" as *u8) == FSX_RC_DENIED { 114 ok3 = sg_absent("sites/escape.html" as *u8) 115 } 116 sg_check("relpath-traversal-denied" as *u8, ok3, pass) 117 // T4 absolute site DENIED 118 sg_check("absolute-site-denied" as *u8, (sg_run("v1.html" as *u8, "/etc" as *u8, "x.html" as *u8) == FSX_RC_DENIED) as i64, pass) 119 // Policy distinguishes an ordinary key-containing name from a secret-bearing path. 120 sg_check("ordinary-key-name-allowed" as *u8, (sg_run("v1.html" as *u8, "gatesite" as *u8, "apikey.html" as *u8) == 0) as i64, pass) 121 var secret_ok: i64 = 0 122 if sg_run("v1.html" as *u8, "gatesite" as *u8, "api_secret.html" as *u8) == FSX_RC_DENIED { 123 secret_ok = sg_absent("sites/gatesite/api_secret.html" as *u8) 124 } 125 sg_check("secret-relpath-denied" as *u8, secret_ok, pass) 126 // T6 absent source -> honest ABSENT 127 sg_check("absent-source-honest" as *u8, (sg_run("nope.html" as *u8, "gatesite" as *u8, "y.html" as *u8) == FSX_RC_ABSENT) as i64, pass) 128 // T7 REGRESSION (debt seq910): a relpath whose PARENT DIRECTORY does not exist must fail 129 // LOUD with FSX_RC_IO and write nothing. This is the case T1-T6 never covered -- the gate 130 // was 6/6 GREEN while the only silent exit in sp_main went untested, and a real publish to 131 // "research/rt004.html" returned empty output and read as success. 132 var ok7: i64 = 0 133 if sg_run("v1.html" as *u8, "gatesite" as *u8, "nodir/page.html" as *u8) == FSX_RC_IO { 134 ok7 = sg_absent("sites/gatesite/nodir/page.html" as *u8) 135 } 136 sg_check("missing-parent-dir-fails-loud" as *u8, ok7, pass) 137 138 let av: *i64 = sys_mmap(SG_ARGV_BYTES) as *i64 139 let h1: *u8 = sys_mmap(PB_SHA_HEX+1) 140 let h2: *u8 = sys_mmap(PB_SHA_HEX+1) 141 pb_hash("<h1>one</h1>" as *u8,12,h1) 142 pb_hash("<h1>two</h1>" as *u8,12,h2) 143 av[0]="nx_site_publish" as *u8 as i64;av[1]="publish-guarded" as *u8 as i64 144 av[2]="v1.html" as *u8 as i64;av[3]="gatesite" as *u8 as i64 145 av[4]="guard.html" as *u8 as i64;av[5]=h1 as i64;av[6]="absent" as *u8 as i64 146 let fresh: i64 = sp_main(PB_GUARDED_ARGC,av) 147 sg_check("guarded-create-exact",((fresh==0)&&(sg_eq("sites/gatesite/guard.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass) 148 av[2]="v2.html" as *u8 as i64;av[5]=h1 as i64;av[6]=h1 as i64 149 let wrong: i64 = sp_main(PB_GUARDED_ARGC,av) 150 sg_check("wrong-artifact-pin-retains-live",((wrong==PB_RC_CONFLICT)&&(sg_eq("sites/gatesite/guard.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass) 151 av[5]=h2 as i64;av[6]="absent" as *u8 as i64 152 let stale: i64 = sp_main(PB_GUARDED_ARGC,av) 153 sg_check("stale-live-pin-retains-live",((stale==PB_RC_CONFLICT)&&(sg_absent("sites/gatesite/guard.html.prev" as *u8)==1)) as i64,pass) 154 av[6]=h1 as i64 155 let change: i64 = sp_main(PB_GUARDED_ARGC,av) 156 sg_check("guarded-change-retains-predecessor",((change==0)&&(sg_eq("sites/gatesite/guard.html.prev" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass) 157 let retry: i64 = sp_main(PB_GUARDED_ARGC,av) 158 sg_check("guarded-retry-original-precondition",((retry==0)&&(sg_eq("sites/gatesite/guard.html.prev" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass) 159 av[2]="v1.html" as *u8 as i64;av[5]=h1 as i64;av[6]=h2 as i64 160 let reverse: i64 = sp_main(PB_GUARDED_ARGC,av) 161 sg_check("guarded-reverse-retains-replaced-version",((reverse==0)&&(sg_eq("sites/gatesite/guard.html" as *u8,"<h1>one</h1>" as *u8)==1)&&(sg_eq("sites/gatesite/guard.html.prev" as *u8,"<h1>two</h1>" as *u8)==1)) as i64,pass) 162 let lockfd: i64 = sys_openat_wr("sites/gatesite/guard.html.publish-lock" as *u8,FSX_MODE_RW) 163 var held: i64 = 0 164 if lockfd >= 0 { 165 if sys_flock(lockfd,PB_LOCK_EX_NB)==0 { held=(sp_main(PB_GUARDED_ARGC,av)==PB_RC_BUSY) as i64 } 166 sys_close(lockfd) 167 } 168 sg_check("contended-target-refuses-without-wait",held,pass) 169 sg_check("released-lock-allows-retry",(sp_main(PB_GUARDED_ARGC,av)==0) as i64,pass) 170 let longrel: *u8 = sys_mmap(PB_PATH+1) 171 var li: i64 = 0 172 while li < PB_PATH { longrel[li]=97 as u8;li=li+1 };longrel[li]=0 as u8 173 av[4]=longrel as i64 174 sg_check("oversized-path-refused-before-construction",(sp_main(PB_GUARDED_ARGC,av)==FSX_RC_DENIED) as i64,pass) 175 sys_mkdir("sites/gatesite/releases" as *u8,0x1ed) 176 av[1]="stage" as *u8 as i64;av[2]="v1.html" as *u8 as i64 177 av[4]="releases/candidate.html" as *u8 as i64;av[5]=h2 as i64 178 sg_check("stage-wrong-pin-creates-no-candidate",((sp_main(PB_STAGE_ARGC,av)==PB_RC_CONFLICT)&&(sg_absent("sites/gatesite/releases/candidate.html" as *u8)==1)) as i64,pass) 179 av[5]=h1 as i64 180 sg_check("stage-exact-candidate",((sp_main(PB_STAGE_ARGC,av)==0)&&(sg_eq("sites/gatesite/releases/candidate.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass) 181 sg_check("stage-retry-preserves-candidate",((sp_main(PB_STAGE_ARGC,av)==0)&&(sg_absent("sites/gatesite/releases/candidate.html.prev" as *u8)==1)) as i64,pass) 182 av[2]="v2.html" as *u8 as i64;av[5]=h2 as i64 183 sg_check("stage-replacement-refused",((sp_main(PB_STAGE_ARGC,av)==PB_RC_CONFLICT)&&(sg_eq("sites/gatesite/releases/candidate.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass) 184 sg_check("legacy-publish-cannot-overwrite-staging",(sg_run("v2.html" as *u8,"gatesite" as *u8,"releases/candidate.html" as *u8)==PB_RC_CONFLICT) as i64,pass) 185 av[1]="publish-guarded" as *u8 as i64;av[6]=h1 as i64 186 sg_check("guarded-publish-cannot-overwrite-staging",(sp_main(PB_GUARDED_ARGC,av)==PB_RC_CONFLICT) as i64,pass) 187 av[1]="stage" as *u8 as i64;av[4]="page.html" as *u8 as i64 188 sg_check("stage-refuses-live-route",(sp_main(PB_STAGE_ARGC,av)==FSX_RC_DENIED) as i64,pass) 189 var aliases: i64 = 0 190 if sg_run("v2.html" as *u8,"gatesite" as *u8,"./releases/candidate.html" as *u8)==FSX_RC_DENIED { 191 if sg_run("v2.html" as *u8,"gatesite/releases" as *u8,"candidate.html" as *u8)==FSX_RC_DENIED { 192 if sg_run("v2.html" as *u8,"gatesite" as *u8,"releases//candidate.html" as *u8)==FSX_RC_DENIED { 193 aliases=sg_eq("sites/gatesite/releases/candidate.html" as *u8,"<h1>one</h1>" as *u8) 194 } 195 } 196 } 197 sg_check("staging-alias-overwrites-refused",aliases,pass) 198 sys_mkdir("knowledge" as *u8,0x1ed) 199 let rcfg: *u8 = "gatesite\tsites/gatesite\tknowledge/pub-\thttps://example.invalid\tGate\tprivate/\n" as *u8 200 sg_check("registered-site-config-written",ss_writefile("knowledge/pub_sites.conf" as *u8,rcfg,ss_len(rcfg)) == 0,pass) 201 let rw: *i64 = ss_begin_cap(64) 202 ss_add(rw,1,"q:n" as *u8,"0" as *u8,1) 203 sg_check("registered-site-plane-seeded",ss_commit("knowledge/pub-" as *u8,rw,ss_next_segid("knowledge/pub-" as *u8)) == 0,pass) 204 av[1]="stage" as *u8 as i64; av[2]="v1.html" as *u8 as i64 205 av[4]="releases/registered.html" as *u8 as i64; av[5]=h1 as i64 206 sg_check("unregistered-stage-refused-without-placement",((sp_main(PB_STAGE_ARGC,av)==PB_RC_CONFLICT)&&(sg_absent("sites/gatesite/releases/registered.html" as *u8)==1)) as i64,pass) 207 sg_check("release-identity-registered",pr_register("knowledge/pub-" as *u8,"releases/registered.html" as *u8,h1)==PR_CREATED,pass) 208 sg_check("legacy-publish-cannot-ignore-registered-hash",((sg_run("v2.html" as *u8,"gatesite" as *u8,"releases/registered.html" as *u8)==PB_RC_CONFLICT)&&(sg_absent("sites/gatesite/releases/registered.html" as *u8)==1)) as i64,pass) 209 sg_check("registered-stage-places-exact-bytes",((sp_main(PB_STAGE_ARGC,av)==0)&&(sg_eq("sites/gatesite/releases/registered.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass) 210 sg_check("registered-stage-retry-keeps-predecessor-absent",((sp_main(PB_STAGE_ARGC,av)==0)&&(sg_absent("sites/gatesite/releases/registered.html.prev" as *u8)==1)) as i64,pass) 211 sg_check("conflicting-registration-cannot-authorize-replacement",((pr_register("knowledge/pub-" as *u8,"releases/registered.html" as *u8,h2)==PR_CONFLICT)&&(sg_eq("sites/gatesite/releases/registered.html" as *u8,"<h1>one</h1>" as *u8)==1)) as i64,pass) 212 gv_plan_finish(sg_plan,pass) 213 sys_munmap(av as *u8,SG_ARGV_BYTES) 214 return gv_verdict("SITE-PUBLISH-GATE" as *u8,pass,"atomic publication, retry preservation, candidate identity and staging overwrite refusal" as *u8) 215}