code wiki / (root) / nx_authz_svc.nx

nx_authz_svc.nx source

↩ module page · 254 lines · 15426 B

1// nx_authz_svc.nx -- the SOVEREIGN AUTHORIZATION SERVICE core (the SpiceDB/OpenFGA/Ory-Keto architecture: ONE 2// daemon owns ONE tuple store and is the SINGLE WRITER; every surface CHECKs and WRITEs through it over a loopback 3// protocol). This is the architecturally complete endpoint of the ReBAC plane: because all surfaces share one 4// store, a group defined anywhere grants access everywhere -- BIDIRECTIONALLY (vs the read-only secondary hack). 5// PURE CORE (no main): az_handle (request -> response) + az_selftest (the offline gate body). Consumers: 6// _hdl_build/nx_authz_svc_daemon (loopback :8029 accept loop) + nx_authz_client (surfaces call in). Reuses nx_rebac 7// for the actual engine (rb_check/rb_put/rb_may_grant/rb_list_*). TRUST MODEL: binds 127.0.0.1 ONLY -> the callers 8// are our own authenticated daemons (rule 12: trust internal service-to-service; the public boundary already 9// validated the user), so the grantor identity in a /write is trusted. license_tier: ORIGINAL 10import "nx_rebac.nx" 11const K_MAGIC_2048: i64 = 2048 12const K_MAGIC_8192: i64 = 8192 13const K_MAGIC_65536: i64 = 65536 14 15func az_p(s: *u8) -> i64 { sys_write(1, s, rb_slen(s)); return 0 } 16func az_pn(v: i64) -> i64 { rb_pn(v); return 0 } 17// substring search: is `needle` present in buf[0..n)? 1/0. 18func az_has(buf: *u8, n: i64, needle: *u8) -> i64 { 19 var i: i64 = 0 20 while i < n { 21 var k: i64 = 0; var hit: i64 = 1 22 while needle[k] != (0 as u8) { if i + k >= n { hit = 0; k = 0 - 1; break } if buf[i+k] != needle[k] { hit = 0; break } k = k + 1 } 23 if hit == 1 { if k > 0 { return 1 } } 24 i = i + 1 25 } 26 return 0 27} 28 29// urldecode one param `key` from a region buf[0..blen) ("a=b&c=d"): + -> space, %XX -> byte. returns value len. 30func az_hexv(c: i64) -> i64 { 31 if c >= 48 { if c <= 57 { return c - 48 } } 32 if c >= 65 { if c <= 70 { return c - 55 } } 33 if c >= 97 { if c <= 102 { return c - 87 } } 34 return 0 35} 36func az_param(buf: *u8, blen: i64, key: *u8, out: *u8, cap: i64) -> i64 { 37 let kl: i64 = rb_slen(key) 38 var i: i64 = 0 39 while i < blen { 40 var atk: i64 = 0 41 if i == 0 { atk = 1 } else { if buf[i-1] == (38 as u8) { atk = 1 } } 42 if atk == 1 { 43 var m: i64 = 1; var j: i64 = 0 44 while j < kl { if i + j >= blen { m = 0; j = kl } else { if buf[i+j] != key[j] { m = 0; j = kl } else { j = j + 1 } } } 45 if m == 1 { if i + kl < blen { if buf[i+kl] == (61 as u8) { 46 var q: i64 = i + kl + 1; var t: i64 = 0 47 var go: i64 = 1 48 while go == 1 { 49 if q >= blen { go = 0 } else { 50 let c: i64 = buf[q] as i64 51 if c == 38 { go = 0 } else { 52 var ch: i64 = c 53 if c == 43 { ch = 32 } 54 if c == 37 { if q + 2 < blen { ch = az_hexv(buf[q+1] as i64) * 16 + az_hexv(buf[q+2] as i64); q = q + 2 } } 55 if t < cap - 1 { out[t] = ch as u8; t = t + 1 } 56 q = q + 1 57 } 58 } 59 } 60 out[t] = 0 as u8; return t 61 } } } 62 } 63 i = i + 1 64 } 65 out[0] = 0 as u8; return 0 66} 67// emit a full HTTP response (status line + json body + Content-Length). status e.g. "200 OK". 68func az_emit(out: *u8, status: *u8, body: *u8) -> i64 { 69 var o: i64 = 0 70 o = rb_cat(out, o, "HTTP/1.1 " as *u8) 71 o = rb_cat(out, o, status) 72 o = rb_cat(out, o, "\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: " as *u8) 73 o = rb_catn(out, o, rb_slen(body)) 74 o = rb_cat(out, o, "\r\n\r\n" as *u8) 75 o = rb_cat(out, o, body) 76 return o 77} 78// find the request-target token (between the first two spaces); split query (after '?') off into qbuf. 79func az_target(req: *u8, reqlen: i64, path: *u8, pcap: i64, qbuf: *u8, qcap: i64) -> i64 { 80 var i: i64 = 0 81 while i < reqlen { if req[i] == (32 as u8) { i = i + 1; break } i = i + 1 } 82 var t: i64 = 0; var inq: i64 = 0; var qn: i64 = 0 83 while i < reqlen { 84 let c: i64 = req[i] as i64 85 if c == 32 { break } 86 if c == 63 { inq = 1; i = i + 1 } else { 87 if inq == 0 { if t < pcap - 1 { path[t] = c as u8; t = t + 1 } } else { if qn < qcap - 1 { qbuf[qn] = c as u8; qn = qn + 1 } } 88 i = i + 1 89 } 90 } 91 path[t] = 0 as u8; qbuf[qn] = 0 as u8 92 return qn 93} 94func az_body(req: *u8, reqlen: i64, szbox: *i64) -> *u8 { 95 var i: i64 = 0 96 while i + 3 < reqlen { 97 if req[i] == (13 as u8) { if req[i+1] == (10 as u8) { if req[i+2] == (13 as u8) { if req[i+3] == (10 as u8) { 98 szbox[0] = reqlen - (i + 4); return (req as i64 + i + 4) as *u8 99 } } } } 100 i = i + 1 101 } 102 szbox[0] = 0; return (req as i64 + reqlen) as *u8 103} 104// emit a JSON array of the ref strings in arr[0..n) into out at off; returns new off. 105func az_json_arr(out: *u8, off: i64, arr: *i64, n: i64) -> i64 { 106 var o: i64 = rb_cat(out, off, "[" as *u8) 107 var i: i64 = 0 108 while i < n { 109 if i > 0 { o = rb_cat(out, o, "," as *u8) } 110 o = rb_cat(out, o, "\"" as *u8); o = rb_cat(out, o, arr[i] as *u8); o = rb_cat(out, o, "\"" as *u8) 111 i = i + 1 112 } 113 o = rb_cat(out, o, "]" as *u8) 114 return o 115} 116 117// ---- the service handler: request bytes -> response bytes. ONE store = `prefix`. ---- 118func az_handle(prefix: *u8, req: *u8, reqlen: i64, out: *u8, cap: i64) -> i64 { 119 var is_post: i64 = 0 120 if req[0] == (80 as u8) { if req[1] == (79 as u8) { is_post = 1 } } 121 let path: *u8 = sys_mmap(256); let qbuf: *u8 = sys_mmap(K_MAGIC_2048) 122 az_target(req, reqlen, path, 256, qbuf, K_MAGIC_2048) 123 // params come from the query string (GET) or the body (POST) 124 var region: *u8 = qbuf; var rlen: i64 = rb_slen(qbuf) 125 if is_post == 1 { let szb: *i64 = sys_mmap(16) as *i64; region = az_body(req, reqlen, szb); rlen = szb[0] } 126 127 // GET /check?s=&p=&o= -> {"allow":0|1} 128 if rb_seq(path, "/check" as *u8) == 1 { 129 let s: *u8 = sys_mmap(256); let p: *u8 = sys_mmap(64); let ob: *u8 = sys_mmap(256) 130 az_param(region, rlen, "s" as *u8, s, 256); az_param(region, rlen, "p" as *u8, p, 64); az_param(region, rlen, "o" as *u8, ob, 256) 131 let allow: i64 = rb_check(prefix, s, p, ob, 0) 132 let body: *u8 = sys_mmap(64); var bo: i64 = rb_cat(body, 0, "{\"allow\":" as *u8); bo = rb_catn(body, bo, allow); bo = rb_cat(body, bo, "}" as *u8); body[bo] = 0 as u8 133 return az_emit(out, "200 OK" as *u8, body) 134 } 135 // POST /write o=&r=&s=&g=&c= -> authorize (rb_may_grant) -> rb_put -> {"ok":1} | 403 136 if rb_seq(path, "/write" as *u8) == 1 { 137 if is_post == 0 { return az_emit(out, "405 Method Not Allowed" as *u8, "{\"error\":\"POST only\"}" as *u8) } 138 let ob: *u8 = sys_mmap(256); let r: *u8 = sys_mmap(64); let s: *u8 = sys_mmap(256); let g: *u8 = sys_mmap(256); let c: *u8 = sys_mmap(8) 139 az_param(region, rlen, "o" as *u8, ob, 256); az_param(region, rlen, "r" as *u8, r, 64) 140 az_param(region, rlen, "s" as *u8, s, 256); az_param(region, rlen, "g" as *u8, g, 256); az_param(region, rlen, "c" as *u8, c, 8) 141 if ob[0] == (0 as u8) { return az_emit(out, "400 Bad Request" as *u8, "{\"error\":\"missing o\"}" as *u8) } 142 if r[0] == (0 as u8) { return az_emit(out, "400 Bad Request" as *u8, "{\"error\":\"missing r\"}" as *u8) } 143 if s[0] == (0 as u8) { return az_emit(out, "400 Bad Request" as *u8, "{\"error\":\"missing s\"}" as *u8) } 144 // WRITE AUTHORITY: the grantor must be able to MANAGE the object (owner/admin), or bootstrap it unclaimed. 145 if rb_may_grant(prefix, g, ob) == 0 { return az_emit(out, "403 Forbidden" as *u8, "{\"error\":\"grantor may not write this object\"}" as *u8) } 146 var cur: i64 = 1 147 if c[0] == (48 as u8) { cur = 0 } 148 rb_put(prefix, ob, r, s, g, cur) 149 return az_emit(out, "200 OK" as *u8, "{\"ok\":1}" as *u8) 150 } 151 // GET /subjects?o=&r= -> {"n":N,"subs":[...]} 152 if rb_seq(path, "/subjects" as *u8) == 1 { 153 let ob: *u8 = sys_mmap(256); let r: *u8 = sys_mmap(64) 154 az_param(region, rlen, "o" as *u8, ob, 256); az_param(region, rlen, "r" as *u8, r, 64) 155 let arr: *i64 = sys_mmap(8 * 128) as *i64 156 let n: i64 = rb_list_subjects(prefix, ob, r, arr, 128) 157 let body: *u8 = sys_mmap(K_MAGIC_8192); var bo: i64 = rb_cat(body, 0, "{\"n\":" as *u8); bo = rb_catn(body, bo, n); bo = rb_cat(body, bo, ",\"subs\":" as *u8); bo = az_json_arr(body, bo, arr, n); bo = rb_cat(body, bo, "}" as *u8); body[bo] = 0 as u8 158 return az_emit(out, "200 OK" as *u8, body) 159 } 160 // GET /objects?s=&r= -> {"n":N,"objs":[...]} 161 if rb_seq(path, "/objects" as *u8) == 1 { 162 let s: *u8 = sys_mmap(256); let r: *u8 = sys_mmap(64) 163 az_param(region, rlen, "s" as *u8, s, 256); az_param(region, rlen, "r" as *u8, r, 64) 164 let arr: *i64 = sys_mmap(8 * 128) as *i64 165 let n: i64 = rb_list_objects_for_sub(prefix, s, r, arr, 128) 166 let body: *u8 = sys_mmap(K_MAGIC_8192); var bo: i64 = rb_cat(body, 0, "{\"n\":" as *u8); bo = rb_catn(body, bo, n); bo = rb_cat(body, bo, ",\"objs\":" as *u8); bo = az_json_arr(body, bo, arr, n); bo = rb_cat(body, bo, "}" as *u8); body[bo] = 0 as u8 167 return az_emit(out, "200 OK" as *u8, body) 168 } 169 // GET / -> health 170 if rb_seq(path, "/" as *u8) == 1 { return az_emit(out, "200 OK" as *u8, "{\"service\":\"nx_authz_svc\",\"model\":\"rebac\"}" as *u8) } 171 return az_emit(out, "404 Not Found" as *u8, "{\"error\":\"no such route\"}" as *u8) 172} 173// fd shells (accept-loop reuse; same idiom as the other daemons) 174func az_read_req(fd: i64, buf: *u8, cap: i64) -> i64 { 175 var n: i64 = 0; var hdr_end: i64 = 0 - 1; var want: i64 = 0 - 1; var go: i64 = 1 176 while go == 1 { 177 if n >= cap - 1 { go = 0 } else { 178 let r: i64 = sys_read(fd, (buf as i64 + n) as *u8, cap - 1 - n) 179 if r <= 0 { go = 0 } else { 180 n = n + r 181 if hdr_end < 0 { 182 var i: i64 = 0 183 while i + 3 < n { if buf[i] == (13 as u8) { if buf[i+1] == (10 as u8) { if buf[i+2] == (13 as u8) { if buf[i+3] == (10 as u8) { hdr_end = i + 4; i = n } } } } i = i + 1 } 184 if hdr_end >= 0 { 185 var cl: i64 = 0 - 1; let key: *u8 = "Content-Length:" as *u8; let kl: i64 = 15; var j: i64 = 0 186 while j + kl < hdr_end { 187 var m: i64 = 1; var q: i64 = 0 188 while q < kl { if buf[j+q] != key[q] { m = 0; q = kl } else { q = q + 1 } } 189 if m == 1 { var v: i64 = 0; var t: i64 = j + kl 190 while t < hdr_end { let c: i64 = buf[t] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } if c == 13 { t = hdr_end } t = t + 1 } 191 cl = v; j = hdr_end } 192 j = j + 1 193 } 194 if cl < 0 { want = hdr_end } else { want = hdr_end + cl } 195 } 196 } 197 if want >= 0 { if n >= want { go = 0 } } 198 } 199 } 200 } 201 return n 202} 203func az_write_all(fd: i64, buf: *u8, n: i64) -> i64 { var w: i64 = 0; while w < n { let k: i64 = sys_write(fd, (buf as i64 + w) as *u8, n - w); if k <= 0 { return 0 - 1 } w = w + k } return 0 } 204 205// ---- offline gate: the service semantics over constructed requests ---- 206func az_ck(label: *u8, got: i64, want: i64, okp: *i64) -> i64 { 207 az_p(" AZSVC " as *u8); az_p(label); az_p(" got=" as *u8); az_pn(got) 208 if got == want { az_p(" ok\n" as *u8); return 0 } 209 az_p(" *** WRONG (want " as *u8); az_pn(want); az_p(") ***\n" as *u8); okp[0] = 0; return 0 210} 211func az_selftest() -> i64 { 212 az_p("=== NX-AUTHZ-SVC GATE (single-store service: check/write/subjects/objects; write-authority enforced) ===\n" as *u8) 213 let okp: *i64 = sys_mmap(8) as *i64; okp[0] = 1 214 let prefix: *u8 = sys_mmap(128); var po: i64 = rb_cat(prefix, 0, "/tmp/azsvc_" as *u8); po = rb_catn(prefix, po, sys_now_us()); po = rb_cat(prefix, po, "_" as *u8); prefix[po] = 0 as u8 215 let out: *u8 = sys_mmap(K_MAGIC_65536) 216 // 1. bootstrap WRITE: lead claims group:choir (unclaimed -> allowed) as owner 217 let w1: *u8 = "POST /write HTTP/1.1\r\nContent-Length: 62\r\n\r\no=group:choir&r=owner&s=user:lead&g=user:lead&c=1" as *u8 218 let n1: i64 = az_handle(prefix, w1, rb_slen(w1), out, K_MAGIC_65536) 219 az_ck("W1 bootstrap owner write ok" as *u8, az_has(out,n1, "\"ok\":1" as *u8), 1, okp) 220 // 2. lead (owner) adds member zoe 221 let w2: *u8 = "POST /write HTTP/1.1\r\nContent-Length: 60\r\n\r\no=group:choir&r=member&s=user:zoe&g=user:lead&c=1" as *u8 222 az_handle(prefix, w2, rb_slen(w2), out, K_MAGIC_65536) 223 // 3. share a doc with the group userset (lead owns the doc first) 224 let w3: *u8 = "POST /write HTTP/1.1\r\nContent-Length: 58\r\n\r\no=doc:hymnal&r=owner&s=user:lead&g=user:lead&c=1" as *u8 225 az_handle(prefix, w3, rb_slen(w3), out, K_MAGIC_65536) 226 let w4: *u8 = "POST /write HTTP/1.1\r\nContent-Length: 74\r\n\r\no=doc:hymnal&r=viewer&s=group:choir%23member&g=user:lead&c=1" as *u8 227 az_handle(prefix, w4, rb_slen(w4), out, K_MAGIC_65536) 228 // 4. CHECK: zoe reads the doc via the group userset 229 let c1: *u8 = "GET /check?s=user:zoe&p=read&o=doc:hymnal HTTP/1.1\r\n\r\n" as *u8 230 let nc1: i64 = az_handle(prefix, c1, rb_slen(c1), out, K_MAGIC_65536) 231 az_ck("C1 zoe reads doc via group userset (allow:1)" as *u8, az_has(out,nc1, "\"allow\":1" as *u8), 1, okp) 232 // 5. non-member frank denied 233 let c2: *u8 = "GET /check?s=user:frank&p=read&o=doc:hymnal HTTP/1.1\r\n\r\n" as *u8 234 let nc2: i64 = az_handle(prefix, c2, rb_slen(c2), out, K_MAGIC_65536) 235 az_ck("C2 frank denied (allow:0)" as *u8, az_has(out,nc2, "\"allow\":0" as *u8), 1, okp) 236 // 6. WRITE AUTHORITY: mallory (not owner/admin of doc) cannot share it -> 403 237 let w5: *u8 = "POST /write HTTP/1.1\r\nContent-Length: 60\r\n\r\no=doc:hymnal&r=viewer&s=user:eve&g=user:mallory&c=1" as *u8 238 let n5: i64 = az_handle(prefix, w5, rb_slen(w5), out, K_MAGIC_65536) 239 az_ck("W5 non-owner write REFUSED (403)" as *u8, az_has(out,n5, "403 Forbidden" as *u8), 1, okp) 240 // 7. subjects list: the doc's viewers include the group userset 241 let s1: *u8 = "GET /subjects?o=doc:hymnal&r=viewer HTTP/1.1\r\n\r\n" as *u8 242 let ns1: i64 = az_handle(prefix, s1, rb_slen(s1), out, K_MAGIC_65536) 243 az_ck("S1 /subjects lists the group userset" as *u8, az_has(out,ns1, "group:choir#member" as *u8), 1, okp) 244 // 8. revoke membership -> zoe loses the doc (single store, immediate) 245 let w6: *u8 = "POST /write HTTP/1.1\r\nContent-Length: 60\r\n\r\no=group:choir&r=member&s=user:zoe&g=user:lead&c=0" as *u8 246 az_handle(prefix, w6, rb_slen(w6), out, K_MAGIC_65536) 247 let c3: *u8 = "GET /check?s=user:zoe&p=read&o=doc:hymnal HTTP/1.1\r\n\r\n" as *u8 248 let nc3: i64 = az_handle(prefix, c3, rb_slen(c3), out, K_MAGIC_65536) 249 az_ck("C3 after revoke zoe denied (allow:0)" as *u8, az_has(out,nc3, "\"allow\":0" as *u8), 1, okp) 250 az_p("NX-AUTHZ-SVC " as *u8) 251 if okp[0] == 1 { az_p("verdict=GREEN (the single-writer service: check + write-authority + subjects + userset + revoke, all over the HTTP protocol)\n" as *u8); return 0 } 252 az_p("verdict=RED\n" as *u8) 253 return 1 254}