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}