code wiki / _hdl_build / nx_connect_album_gate.nx
nx_connect_album_gate.nx source
↩ module page · 258 lines · 14745 B
1// nx_connect_album_gate.nx -- the API's regression proof. The dispatcher is the seam where a permission
2// rule most easily gets lost: it is the layer that turns a request into a plane call, so a bug here looks
3// like a routing detail and behaves like an access-control failure.
4// The teeth that matter are the refusals, and the one that matters most is T3: a signed-out caller must
5// not be able to mutate anything, because that is the failure that leaves no trace in the data.
6// license_tier: ORIGINAL expect_exit: 0
7import "nx_connect_album.nx"
8
9const AG_ALICE: i64 = 1001
10const AG_BOB: i64 = 1002
11const AG_CARA: i64 = 1003
12const AG_ALBUM: i64 = 7001
13const AG_OUT: i64 = 8192
14const AG_KIND: *u8 = "album\x00"
15// "no item" cannot be an empty literal: nx_cc returns the NEXT literal in the pool for a zero-length
16// string (seq907), so absence is carried as a one-byte sentinel.
17const AG_NOITEM: *u8 = "-\x00"
18const AG_ITEM1: *u8 = "cid_aaa\x00"
19const AG_ITEM2: *u8 = "cid_bbb\x00"
20
21func a_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
22func a_n(v: i64) -> i64 {
23 let bb: *u8=sys_mmap(28); var m: i64=v
24 if m<0 { m=0-m; sys_write(1,"-" as *u8,1) }
25 let t: *u8=sys_mmap(28); var k: i64=0
26 if m==0 { t[0]=48 as u8; k=1 }
27 while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
28 var i: i64=0
29 while i<k { bb[i]=t[k-1-i]; i=i+1 }
30 sys_write(1,bb,k); return 0
31}
32func a_t(pass: i64, label: *u8, fails: *i64) -> i64 {
33 a_w(" " as *u8); a_w(label); a_w(": " as *u8)
34 if pass==1 { a_w("PASS\n" as *u8) } else { a_w("FAIL\n" as *u8); fails[0]=fails[0]+1 }
35 return 0
36}
37// does the JSON response contain this literal? (the wire contract IS the thing under test)
38func a_has(buf: *u8, needle: *u8) -> i64 {
39 let hl: i64 = rb_slen(buf); let nl: i64 = rb_slen(needle)
40 if nl==0 { return 0 }
41 var i: i64=0
42 while i+nl<=hl {
43 var j: i64=0; var hit: i64=1
44 while j<nl { if buf[i+j]!=needle[j] { hit=0; j=nl } else { j=j+1 } }
45 if hit==1 { return 1 }
46 i=i+1
47 }
48 return 0
49}
50func px(tag: i64) -> *u8 {
51 let p: *u8 = sys_mmap(128); var o: i64 = 0
52 o = rb_cat(p, o, "/tmp/algate_" as *u8); o = rb_catn(p, o, sys_now_us())
53 o = rb_cat(p, o, "_" as *u8); o = rb_catn(p, o, tag); o = rb_cat(p, o, "_" as *u8)
54 p[o] = 0 as u8
55 return p
56}
57
58// The containment teeth live in their own function: main() had grown large enough that adding them
59// segfaulted the GATE while nx_album_probe ran the identical calls clean. A test harness that falls
60// over under its own weight will be misread as a failure of the thing it tests.
61func containment_teeth(out: *u8, fails: *i64) -> i64 {
62 // ---- CONTAINMENT: an album is only a product once it holds something ----
63
64 // T11 the owner puts items in and reads them back; a plain VIEWER can read them but must NOT add.
65 // "May look, may not change what is in it" is the whole difference between a viewer and a curator.
66 let p11: *u8 = px(11)
67 al_do(p11, "claim" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
68 al_do(p11, "add" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_ITEM1, out, AG_OUT)
69 let a1: i64 = a_has(out, "\x22added\x22:1" as *u8)
70 al_do(p11, "add" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_ITEM2, out, AG_OUT)
71 al_do(p11, "share" as *u8, AG_ALICE, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
72 al_do(p11, "items" as *u8, AG_BOB, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
73 a_w(" -> " as *u8); a_w(out); a_w("\n" as *u8)
74 let sees: i64 = a_has(out, "item:cid_aaa" as *u8)
75 let sees2: i64 = a_has(out, "item:cid_bbb" as *u8)
76 al_do(p11, "add" as *u8, AG_BOB, 0, AG_KIND, AG_ALBUM, "cid_evil\x00" as *u8, out, AG_OUT)
77 let blocked: i64 = a_has(out, "not_permitted" as *u8)
78 var t11: i64=0
79 if a1==1 { if sees==1 { if sees2==1 { if blocked==1 { t11=1 } } } }
80 a_t(t11, "T11 a shared viewer SEES the items and CANNOT add one" as *u8, fails)
81
82 // T12 THE PRIVACY TOOTH: a stranger asking for an album's contents gets the SAME refusal whether the
83 // album is not theirs or does not exist. If those two answers differ, the API is an enumeration oracle
84 // and anyone can map the family's albums by probing ids.
85 let p12: *u8 = px(12)
86 al_do(p12, "claim" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
87 al_do(p12, "add" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_ITEM1, out, AG_OUT)
88 al_do(p12, "items" as *u8, AG_CARA, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
89 let denied_real: i64 = a_has(out, "not_permitted" as *u8)
90 let leaked: i64 = a_has(out, "cid_aaa" as *u8)
91 al_do(p12, "items" as *u8, AG_CARA, 0, AG_KIND, 999999, AG_NOITEM, out, AG_OUT)
92 let denied_ghost: i64 = a_has(out, "not_permitted" as *u8)
93 var t12: i64=0
94 if denied_real==1 { if denied_ghost==1 { if leaked==0 { t12=1 } } }
95 a_t(t12, "T12 PRIVACY an existing-but-unshared album and a nonexistent one are INDISTINGUISHABLE, and no item leaks" as *u8, fails)
96
97 // T13 a curator may add (that is what curator means), and removal tombstones rather than erases
98 let p13: *u8 = px(13)
99 al_do(p13, "claim" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
100 al_do(p13, "curate" as *u8, AG_ALICE, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
101 al_do(p13, "add" as *u8, AG_BOB, 0, AG_KIND, AG_ALBUM, AG_ITEM1, out, AG_OUT)
102 let cadd: i64 = a_has(out, "\x22added\x22:1" as *u8)
103 al_do(p13, "add" as *u8, AG_BOB, 0, AG_KIND, AG_ALBUM, AG_ITEM2, out, AG_OUT)
104 al_do(p13, "remove" as *u8, AG_BOB, 0, AG_KIND, AG_ALBUM, AG_ITEM1, out, AG_OUT)
105 al_do(p13, "items" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
106 a_w(" -> " as *u8); a_w(out); a_w("\n" as *u8)
107 var t13: i64=0
108 if cadd==1 { if a_has(out, "cid_bbb" as *u8)==1 { if a_has(out, "cid_aaa" as *u8)==0 { t13=1 } } }
109 a_t(t13, "T13 a curator can add, and a removed item stops being listed" as *u8, fails)
110
111 // T14 an add with NO item is refused rather than writing a blank membership row
112 let p14: *u8 = px(14)
113 al_do(p14, "claim" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
114 al_do(p14, "add" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
115 let badarg: i64 = a_has(out, "bad_arg" as *u8)
116 al_do(p14, "items" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
117 var t14: i64=0
118 if badarg==1 { if a_has(out, "\x22items\x22:[]" as *u8)==1 { t14=1 } }
119 a_t(t14, "T14 an add with no item is refused and writes NOTHING" as *u8, fails)
120
121 return 0
122}
123
124func main() -> i64 {
125 let fails: *i64 = sys_mmap(16) as *i64
126 fails[0]=0
127 let out: *u8 = sys_mmap(AG_OUT)
128 a_w("=== nx_connect_album_gate -- the sharing API is judged by what it refuses ===\n" as *u8)
129
130 // T1 an unknown verb is refused by name, not silently ignored
131 let p1: *u8 = px(1)
132 al_do(p1, "teleport" as *u8, AG_ALICE, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
133 a_w(" -> " as *u8); a_w(out); a_w("\n" as *u8)
134 a_t(a_has(out, "unknown_verb" as *u8), "T1 an unknown verb is refused BY NAME" as *u8, fails)
135
136 // T2 claim then share then may -- the happy path exists at all
137 let p2: *u8 = px(2)
138 al_do(p2, "claim" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
139 let c_ok: i64 = a_has(out, "\x22claimed\x22:1" as *u8)
140 al_do(p2, "share" as *u8, AG_ALICE, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
141 let s_ok: i64 = a_has(out, "\x22shared\x22:1" as *u8)
142 al_do(p2, "may" as *u8, AG_BOB, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
143 let m_ok: i64 = a_has(out, "\x22may\x22:1" as *u8)
144 var t2: i64=0
145 if c_ok==1 { if s_ok==1 { if m_ok==1 { t2=1 } } }
146 a_t(t2, "T2 claim -> share -> may is a working loop over the canonical plane" as *u8, fails)
147
148 // T3 THE ONE THAT MATTERS: a signed-out caller (actor 0) may not mutate. Every mutating verb, not a
149 // sample of them -- a gate that checks one of five refusals proves one of five refusals.
150 let p3: *u8 = px(3)
151 al_do(p3, "claim" as *u8, 0, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
152 let r1: i64 = a_has(out, "sign_in_required" as *u8)
153 al_do(p3, "share" as *u8, 0, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
154 let r2: i64 = a_has(out, "sign_in_required" as *u8)
155 al_do(p3, "curate" as *u8, 0, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
156 let r3: i64 = a_has(out, "sign_in_required" as *u8)
157 al_do(p3, "unshare" as *u8, 0, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
158 let r4: i64 = a_has(out, "sign_in_required" as *u8)
159 al_do(p3, "gift" as *u8, 0, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
160 let r5: i64 = a_has(out, "sign_in_required" as *u8)
161 var t3: i64=0
162 if r1==1 { if r2==1 { if r3==1 { if r4==1 { if r5==1 { t3=1 } } } } }
163 a_t(t3, "T3 a signed-out caller is refused by ALL FIVE mutating verbs" as *u8, fails)
164
165 // T3b NON-VACUITY: prove the signed-out refusal actually prevented the mutation, rather than merely
166 // printing a refusal while the write went through anyway.
167 al_do(p3, "may" as *u8, AG_BOB, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
168 a_t(a_has(out, "\x22may\x22:0" as *u8),
169 "T3b NON-VACUOUS the refused signed-out writes left NOTHING behind" as *u8, fails)
170
171 // T4 sharing an unclaimed album is refused, and the reason is nameable
172 let p4: *u8 = px(4)
173 al_do(p4, "share" as *u8, AG_ALICE, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
174 a_t(a_has(out, "unclaimed" as *u8), "T4 sharing an UNCLAIMED album is refused as 'unclaimed'" as *u8, fails)
175
176 // T5 ATTACK a plain viewer re-shares -> not_owner, and the third party still cannot see it
177 let p5: *u8 = px(5)
178 al_do(p5, "claim" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
179 al_do(p5, "share" as *u8, AG_ALICE, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
180 al_do(p5, "share" as *u8, AG_BOB, AG_CARA, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
181 let esc: i64 = a_has(out, "not_owner" as *u8)
182 al_do(p5, "may" as *u8, AG_CARA, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
183 let leak: i64 = a_has(out, "\x22may\x22:0" as *u8)
184 var t5: i64=0
185 if esc==1 { if leak==1 { t5=1 } }
186 a_t(t5, "T5 ATTACK a viewer cannot re-share AND the third party still sees nothing" as *u8, fails)
187
188 // T6 a curator CAN re-share -- the delegated capability that survived the plane retirement
189 let p6: *u8 = px(6)
190 al_do(p6, "claim" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
191 al_do(p6, "curate" as *u8, AG_ALICE, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
192 al_do(p6, "share" as *u8, AG_BOB, AG_CARA, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
193 let cok: i64 = a_has(out, "\x22shared\x22:1" as *u8)
194 al_do(p6, "may" as *u8, AG_CARA, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
195 var t6: i64=0
196 if cok==1 { if a_has(out, "\x22may\x22:1" as *u8)==1 { t6=1 } }
197 a_t(t6, "T6 a CURATOR can re-share (the delegated capability survived the retirement)" as *u8, fails)
198
199 // T7 unshare is immediate and the viewer panel forgets them
200 let p7: *u8 = px(7)
201 al_do(p7, "claim" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
202 al_do(p7, "share" as *u8, AG_ALICE, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
203 al_do(p7, "share" as *u8, AG_ALICE, AG_CARA, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
204 al_do(p7, "unshare" as *u8, AG_ALICE, AG_CARA, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
205 al_do(p7, "may" as *u8, AG_CARA, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
206 let gone: i64 = a_has(out, "\x22may\x22:0" as *u8)
207 al_do(p7, "viewers" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
208 a_w(" -> " as *u8); a_w(out); a_w("\n" as *u8)
209 var t7: i64=0
210 if gone==1 { if a_has(out, "user:1002" as *u8)==1 { if a_has(out, "user:1003" as *u8)==0 { t7=1 } } }
211 a_t(t7, "T7 unshare is immediate and the viewer list forgets the revoked person" as *u8, fails)
212
213 // T8 a gift moves ownership and the chain is readable over the wire
214 let p8: *u8 = px(8)
215 al_do(p8, "claim" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
216 al_do(p8, "gift" as *u8, AG_ALICE, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
217 let g1: i64 = a_has(out, "\x22gifted\x22:1" as *u8)
218 al_do(p8, "gift" as *u8, AG_BOB, AG_CARA, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
219 al_do(p8, "may" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
220 let giver_lost: i64 = a_has(out, "\x22may\x22:0" as *u8)
221 al_do(p8, "provenance" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
222 a_w(" -> " as *u8); a_w(out); a_w("\n" as *u8)
223 var t8: i64=0
224 if g1==1 { if giver_lost==1 {
225 if a_has(out, "user:1001" as *u8)==1 { if a_has(out, "user:1002" as *u8)==1 {
226 if a_has(out, "user:1003" as *u8)==1 { t8=1 } } }
227 } }
228 a_t(t8, "T8 a gift moves ownership and the whole chain is readable over the wire" as *u8, fails)
229
230 // T9 ATTACK a former owner re-gifts what they gave away
231 let p9: *u8 = px(9)
232 al_do(p9, "claim" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
233 al_do(p9, "gift" as *u8, AG_ALICE, AG_BOB, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
234 al_do(p9, "gift" as *u8, AG_ALICE, AG_CARA, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
235 a_t(a_has(out, "not_owner" as *u8), "T9 ATTACK a former owner cannot re-gift what they gave away" as *u8, fails)
236
237 // T10 every response is versioned -- an unversioned API contract cannot be evolved without breaking
238 // somebody silently (rule 19)
239 let p10: *u8 = px(10)
240 al_do(p10, "may" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
241 let v1: i64 = a_has(out, "\x22v\x22:1" as *u8)
242 al_do(p10, "nope" as *u8, AG_ALICE, 0, AG_KIND, AG_ALBUM, AG_NOITEM, out, AG_OUT)
243 let v2: i64 = a_has(out, "\x22v\x22:1" as *u8)
244 var t10: i64=0
245 if v1==1 { if v2==1 { t10=1 } }
246 a_t(t10, "T10 success AND failure responses both carry the contract version" as *u8, fails)
247
248 containment_teeth(out, fails)
249
250 a_w(" fails=" as *u8); a_n(fails[0]); a_w("\n" as *u8)
251 if fails[0]==0 {
252 a_w("VERDICT: verdict=GREEN (14/14 -- signed-out cannot mutate, viewers cannot widen, gifts move and stay traceable)\n" as *u8)
253 sys_exit(0)
254 }
255 a_w("VERDICT: RED\n" as *u8)
256 sys_exit(1)
257 return 1
258}