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}