code wiki / _hdl_build / nx_docportal_admin_lib.nx

nx_docportal_admin_lib.nx source

↩ module page · 233 lines · 14997 B

1// nx_docportal_admin_lib.nx -- the admin.<domain> DOCUMENT-PORTAL request handler (operator's global split: 2// CAPABILITIES live on subdomains -- admin.<d> -- while visitor content lives on <d>/paths). Pure bytes-in / 3// bytes-out per the proven nx_status_daemon sd_handle pattern: a thin socket loop wraps it at deploy time; the 4// logic + gate need no socket. The admin capability is GLOBAL (one handler serves every domain); the SHARD is the 5// domain, passed per request -> a request can only touch the domain it names, and within that domain the 6// public/private split of nx_docportal_lib holds by construction. 7// 8// AUTHZ deny-by-default (defensive at the boundary; the caller sets user_level AFTER authenticating, like the 9// legal_portal NxPortalCtx + the nx_status_daemon login->token flow): 0=anon, 1=client, 2=staff. Every /admin 10// route needs >=1; /admin/staff routes need 2. Composes dp_ingest. license_tier: ORIGINAL 11import "nx_docportal_lib.nx" 12import "nx_doc_at_rest.nx" // P1 at-rest (operator-approved): doc_put confidential encrypt-on-write + at_rest_confidential 13import "nx_doc_authz.nx" // P2 eyes-only: authz_set_owner (own:<cid>) + authz_doc_get (owner-gated private read) 14 15// Consts for the zero-alloc numeric emitter + flag buffer; MUST precede their first readers. 16const DA_ASCII_0: i64 = 48 17const DA_DEC: i64 = 10 18const DA_FLAGBUF: i64 = 8 19 20const K_MAGIC_131072: i64 = 131072 21 22// one admin session: the authenticated user's level + the session SUBJECT (the 64-hex uid hash from 23// nx_sa_validate_handle), both set by the caller post-authentication. subject==0 (unset) => no ownership recorded / 24// read denied = fail-closed, never a null-deref. 25struct DaCtx { user_level: i64, subject: *u8 } 26 27func da_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 28func da_cat(out: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { out[o] = s[i]; o = o + 1; i = i + 1 } return o } 29func da_numc(out: *u8, o: i64, v: i64) -> i64 { 30 if v == 0 { out[o] = 48 as u8; return o + 1 } 31 var m: i64 = v; if m < 0 { out[o] = 45 as u8; o = o + 1; m = 0 - m } 32 // MSB-FIRST: zero allocation (2026-07-31, debt 1785516350). The old body built digits 33 // least-significant-first, needing a sys_mmap(24) scratch to reverse through that was never freed. 34 var pw: i64 = 1 35 while m / pw >= DA_DEC { pw = pw * DA_DEC } 36 while pw > 0 { out[o] = (DA_ASCII_0 + ((m / pw) % DA_DEC)) as u8; o = o + 1; pw = pw / DA_DEC } 37 return o 38} 39func da_eq(p: *u8, pl: i64, lit: *u8) -> i64 { let ll: i64 = da_slen(lit); if pl != ll { return 0 } var i: i64 = 0; while i < pl { if p[i] != lit[i] { return 0 } i = i + 1 } return 1 } 40func da_prefix(p: *u8, pl: i64, lit: *u8) -> i64 { let ll: i64 = da_slen(lit); if pl < ll { return 0 } var i: i64 = 0; while i < ll { if p[i] != lit[i] { return 0 } i = i + 1 } return 1 } 41 42// request path into pathbuf (after the first space, up to space or '?'); returns its length 43func da_pathlen(req: *u8, n: i64, pathbuf: *u8) -> i64 { 44 var ps: i64 = 0 45 while ps < n { if req[ps] == 32 { break } ps = ps + 1 } 46 ps = ps + 1 47 var pe: i64 = ps 48 var sc: i64 = 1 49 while sc == 1 { 50 if pe >= n { sc = 0 } else { let c: i64 = req[pe] as i64; if c == 32 { sc = 0 } else { if c == 63 { sc = 0 } else { pe = pe + 1 } } } 51 } 52 let pl: i64 = pe - ps 53 var pi: i64 = 0 54 while pi < pl { pathbuf[pi] = req[ps + pi]; pi = pi + 1 } 55 pathbuf[pl] = 0 as u8 56 return pl 57} 58 59// copy the value of "<key>=..." from the REQUEST LINE ONLY (first CRLF), up to '&' or ' ' or end, into out 60// (null-terminated). Restricting to the request line keeps a POST body (which may contain "domain=") from 61// spoofing a query param. Returns value length (0 if absent). 62func da_query_str(req: *u8, n: i64, key: *u8, out: *u8, outcap: i64) -> i64 { 63 var lineend: i64 = 0 64 while lineend < n { if req[lineend] == (13 as u8) { break } if req[lineend] == (10 as u8) { break } lineend = lineend + 1 } 65 let kl: i64 = da_slen(key) 66 var i: i64 = 0 67 while i + kl + 1 <= lineend { 68 var m: i64 = 1; var j: i64 = 0 69 while j < kl { if req[i + j] != key[j] { m = 0; break } j = j + 1 } 70 if m == 1 { if req[i + kl] == (61 as u8) { 71 var k: i64 = i + kl + 1; var w: i64 = 0 72 while k < lineend { let c: i64 = req[k] as i64; if c == 38 { break } if c == 32 { break } if w < outcap - 1 { out[w] = req[k]; w = w + 1 } k = k + 1 } 73 out[w] = 0 as u8 74 return w 75 } } 76 i = i + 1 77 } 78 out[0] = 0 as u8 79 return 0 80} 81 82// offset of the body (after the CRLFCRLF header terminator), or n if there is no body 83func da_body_start(req: *u8, n: i64) -> i64 { 84 var i: i64 = 0 85 while i + 3 < n { 86 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) { return i + 4 } } } } 87 i = i + 1 88 } 89 return n 90} 91 92// text/plain HTTP response (status line + nosniff + Content-Length + body) 93func da_text_response(out: *u8, status: *u8, body: *u8, blen: i64) -> i64 { 94 var o: i64 = 0 95 o = da_cat(out, o, "HTTP/1.1 " as *u8) 96 o = da_cat(out, o, status) 97 o = da_cat(out, o, "\r\nContent-Type: text/plain; charset=utf-8\r\nX-Content-Type-Options: nosniff\r\nContent-Length: " as *u8) 98 o = da_numc(out, o, blen) 99 o = da_cat(out, o, "\r\n\r\n" as *u8) 100 var i: i64 = 0 101 while i < blen { out[o] = body[i]; o = o + 1; i = i + 1 } 102 return o 103} 104 105// is the request a POST? 106func da_is_post(req: *u8, n: i64) -> i64 { if n < 4 { return 0 } if req[0] == (80 as u8) { if req[1] == (79 as u8) { if req[2] == (83 as u8) { if req[3] == (84 as u8) { return 1 } } } } return 0 } 107 108func da_atoi(s: *u8) -> i64 { var v: i64 = 0; var i: i64 = 0; while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } return v } 109 110// parse the owner's usage flags from the request line (pub_search/ai_blog/ai_summary/share_ext, each =1 sets the 111// bit). anyout[0]=1 iff ANY of these params is present -- the upload path then uses the explicit choice, else the 112// sensible default policy. This is the surface where a lawyer/user FLAGS how their upload is managed. 113func da_parse_flags(req: *u8, n: i64, anyout: *i64) -> i64 { 114 let fb: *u8 = sys_mmap(DA_FLAGBUF) 115 var flags: i64 = 0; var any: i64 = 0 116 if da_query_str(req, n, "pub_search" as *u8, fb, 8) > 0 { any = 1; if fb[0] == (49 as u8) { flags = flags | DP_USE_PUB_SEARCH } } 117 if da_query_str(req, n, "ai_blog" as *u8, fb, 8) > 0 { any = 1; if fb[0] == (49 as u8) { flags = flags | DP_USE_AI_BLOG } } 118 if da_query_str(req, n, "ai_summary" as *u8, fb, 8) > 0 { any = 1; if fb[0] == (49 as u8) { flags = flags | DP_USE_AI_SUMMARY } } 119 if da_query_str(req, n, "share_ext" as *u8, fb, 8) > 0 { any = 1; if fb[0] == (49 as u8) { flags = flags | DP_USE_SHARE_EXT } } 120 anyout[0] = any 121 sys_munmap(fb, DA_FLAGBUF) 122 return flags 123} 124 125// ---- the pure router: request bytes in, response bytes out ---- 126func da_handle(ctx: *DaCtx, req: *u8, n: i64, out: *u8) -> i64 { 127 let pathbuf: *u8 = sys_mmap(512) 128 let pl: i64 = da_pathlen(req, n, pathbuf) 129 130 // deny-by-default: only /admin/* exists; staff routes need level 2, others need level 1 131 if da_prefix(pathbuf, pl, "/admin" as *u8) == 0 { return da_text_response(out, "404 Not Found" as *u8, "no such resource" as *u8, 16) } 132 var need: i64 = 1 133 if da_prefix(pathbuf, pl, "/admin/staff" as *u8) == 1 { need = 2 } 134 if ctx.user_level < need { return da_text_response(out, "401 Unauthorized" as *u8, "authentication required" as *u8, 23) } 135 136 // POST /admin/upload?domain=D&visibility=public|private body = the document bytes 137 if da_is_post(req, n) == 1 { if da_eq(pathbuf, pl, "/admin/upload" as *u8) == 1 { 138 let dom: *u8 = sys_mmap(256); let vis: *u8 = sys_mmap(32) 139 let dl: i64 = da_query_str(req, n, "domain" as *u8, dom, 256) 140 da_query_str(req, n, "visibility" as *u8, vis, 32) 141 if dl <= 0 { return da_text_response(out, "400 Bad Request" as *u8, "missing domain" as *u8, 14) } 142 var visnum: i64 = DP_VIS_PUBLIC 143 if da_eq(vis, da_slen(vis), "private" as *u8) == 1 { visnum = DP_VIS_PRIVATE } 144 let bstart: i64 = da_body_start(req, n) 145 let blen: i64 = n - bstart 146 if blen <= 0 { return da_text_response(out, "400 Bad Request" as *u8, "empty document body" as *u8, 19) } 147 // owner usage flags from the request (absent -> sensible default policy) 148 let anyp: *i64 = sys_mmap(16) as *i64 149 var flags: i64 = da_parse_flags(req, n, anyp) 150 if anyp[0] == 0 { flags = dp_default_policy(visnum) } 151 let cidout: *i64 = sys_mmap(16) as *i64 152 // P1 ENCRYPT-AT-REST (operator-approved, new confidential writes only): a CONFIDENTIAL upload (vis != PUBLIC) 153 // is sealed with the crypto core -- doc:<cid> = NXENC1-magic || AEAD envelope -- while cid stays content- 154 // addressed on the PLAINTEXT (idempotent re-upload; P2's private route reads it via doc_get). The pol:<cid> 155 // row is preserved (dp_set_policy). The PUBLIC corpus path is BYTE-IDENTICAL (dp_ingest_policy unchanged). 156 var rc: i64 = 0 157 if at_rest_confidential(visnum) == 1 { 158 let bptr: *u8 = (req as i64 + bstart) as *u8 159 cidout[0] = dp_cid(bptr, blen) 160 rc = doc_put(dom, visnum, cidout[0], bptr, blen) 161 if rc == 0 { 162 dp_set_policy(dom, visnum, cidout[0], flags) 163 // P2 OWNERSHIP: record own:<cid> = the authenticated session subject. Guarded -- a caller path that 164 // did not set ctx.subject records NO owner, leaving the doc unreadable (fail-closed), never a crash. 165 if (ctx.subject as i64) != 0 { authz_set_owner(dom, visnum, cidout[0], ctx.subject) } 166 } 167 } else { 168 rc = dp_ingest_policy(dom, visnum, flags, (req as i64 + bstart) as *u8, blen, cidout) 169 } 170 if rc != 0 { return da_text_response(out, "507 Insufficient Storage" as *u8, "ingest failed" as *u8, 13) } 171 let body: *u8 = sys_mmap(512); var bo: i64 = 0 172 bo = da_cat(body, bo, "UPLOADED domain=" as *u8); bo = da_cat(body, bo, dom) 173 bo = da_cat(body, bo, " visibility=" as *u8); if visnum == DP_VIS_PRIVATE { bo = da_cat(body, bo, "private" as *u8) } else { bo = da_cat(body, bo, "public" as *u8) } 174 bo = da_cat(body, bo, " cid=" as *u8); bo = da_numc(body, bo, cidout[0]) 175 bo = da_cat(body, bo, " policy=" as *u8); bo = da_numc(body, bo, flags) 176 return da_text_response(out, "200 OK" as *u8, body, bo) 177 } } 178 179 // POST /admin/policy?domain=D&visibility=V&cid=C&pub_search=&ai_blog=... -> AMEND an upload's usage policy 180 // (the owner changes consent: e.g. withdraw AI-blog permission). Enforced live at every consumer. 181 if da_is_post(req, n) == 1 { if da_eq(pathbuf, pl, "/admin/policy" as *u8) == 1 { 182 let dom: *u8 = sys_mmap(256); let vis: *u8 = sys_mmap(32); let cids: *u8 = sys_mmap(40) 183 let dl: i64 = da_query_str(req, n, "domain" as *u8, dom, 256) 184 da_query_str(req, n, "visibility" as *u8, vis, 32) 185 let cl: i64 = da_query_str(req, n, "cid" as *u8, cids, 40) 186 if dl <= 0 { return da_text_response(out, "400 Bad Request" as *u8, "missing domain" as *u8, 14) } 187 if cl <= 0 { return da_text_response(out, "400 Bad Request" as *u8, "missing cid" as *u8, 11) } 188 var visnum: i64 = DP_VIS_PUBLIC 189 if da_eq(vis, da_slen(vis), "private" as *u8) == 1 { visnum = DP_VIS_PRIVATE } 190 let anyp2: *i64 = sys_mmap(16) as *i64 191 let flags2: i64 = da_parse_flags(req, n, anyp2) 192 let rc2: i64 = dp_set_policy(dom, visnum, da_atoi(cids), flags2) 193 if rc2 != 0 { return da_text_response(out, "404 Not Found" as *u8, "no such document" as *u8, 16) } 194 let body: *u8 = sys_mmap(256); var bo: i64 = da_cat(body, 0, "POLICY-UPDATED cid=" as *u8); bo = da_cat(body, bo, cids); bo = da_cat(body, bo, " flags=" as *u8); bo = da_numc(body, bo, flags2) 195 return da_text_response(out, "200 OK" as *u8, body, bo) 196 } } 197 198 // GET /admin/policy?domain=D&visibility=V&cid=C -> the doc's current usage flags (so the UI can show them) 199 if da_eq(pathbuf, pl, "/admin/policy" as *u8) == 1 { 200 let dom: *u8 = sys_mmap(256); let vis: *u8 = sys_mmap(32); let cids: *u8 = sys_mmap(40) 201 if da_query_str(req, n, "domain" as *u8, dom, 256) <= 0 { return da_text_response(out, "400 Bad Request" as *u8, "missing domain" as *u8, 14) } 202 da_query_str(req, n, "visibility" as *u8, vis, 32) 203 if da_query_str(req, n, "cid" as *u8, cids, 40) <= 0 { return da_text_response(out, "400 Bad Request" as *u8, "missing cid" as *u8, 11) } 204 var visnum: i64 = DP_VIS_PUBLIC 205 if da_eq(vis, da_slen(vis), "private" as *u8) == 1 { visnum = DP_VIS_PRIVATE } 206 let f: i64 = dp_get_policy(dom, visnum, da_atoi(cids)) 207 let body: *u8 = sys_mmap(128); var bo: i64 = da_cat(body, 0, "policy cid=" as *u8); bo = da_cat(body, bo, cids); bo = da_cat(body, bo, " flags=" as *u8); bo = da_numc(body, bo, f) 208 return da_text_response(out, "200 OK" as *u8, body, bo) 209 } 210 211 // GET /admin/doc?domain=D&cid=C -> P2 authenticated EYES-ONLY private read (owner-gated + decrypts). The subject 212 // is the session identity (ctx.subject, set by the daemon from nx_sa_validate_handle). authz_doc_get DENIES unless 213 // the subject OWNS the confidential doc; public docs pass through. Fail-closed: subject unset or len<0 -> 403. 214 // Confidential docs are <= the 64KB request cap, so the plaintext fits docbuf + the response buffer. 215 if da_eq(pathbuf, pl, "/admin/doc" as *u8) == 1 { 216 let dom: *u8 = sys_mmap(256); let cids: *u8 = sys_mmap(40) 217 if da_query_str(req, n, "domain" as *u8, dom, 256) <= 0 { return da_text_response(out, "400 Bad Request" as *u8, "missing domain" as *u8, 14) } 218 if da_query_str(req, n, "cid" as *u8, cids, 40) <= 0 { return da_text_response(out, "400 Bad Request" as *u8, "missing cid" as *u8, 11) } 219 if (ctx.subject as i64) == 0 { return da_text_response(out, "403 Forbidden" as *u8, "not authorized" as *u8, 14) } 220 let docbuf: *u8 = sys_mmap(K_MAGIC_131072) 221 let dlen: i64 = authz_doc_get(dom, DP_VIS_PRIVATE, da_atoi(cids), ctx.subject, ctx.user_level, docbuf) 222 if dlen < 0 { return da_text_response(out, "403 Forbidden" as *u8, "not authorized" as *u8, 14) } 223 return da_text_response(out, "200 OK" as *u8, docbuf, dlen) 224 } 225 226 // GET /admin/whoami -> the authenticated level (proves the authz plumbing) 227 if da_eq(pathbuf, pl, "/admin/whoami" as *u8) == 1 { 228 let body: *u8 = sys_mmap(64); var bo: i64 = da_cat(body, 0, "level=" as *u8); bo = da_numc(body, bo, ctx.user_level) 229 return da_text_response(out, "200 OK" as *u8, body, bo) 230 } 231 232 return da_text_response(out, "404 Not Found" as *u8, "no such resource" as *u8, 16) 233}