code wiki / _hdl_build / nx_doc_authz.nx
nx_doc_authz.nx source
↩ module page · 80 lines · 5263 B
1// nx_doc_authz.nx -- PER-CLIENT EYES-ONLY authorization for confidential docs (the P2 vault read gate). Closes the
2// audit's most-dangerous gaps: no per-doc ownership, no authenticated private-read route (a WRITE-ONLY vault), and
3// the free domain request param. HIPAA minimum-necessary 164.502(b) + ABA Model Rule 1.6(c) + iManage ethical
4// walls: a confidential doc is readable ONLY by its OWNER (the authenticated subject who uploaded it) -- or a
5// future attorney/partner tier -- and DENIED to everyone else, FAIL-CLOSED. Composes nx_doc_at_rest (doc_get
6// decrypts) + the sovereign seg_store (own:<cid> ownership row, additive alongside doc:<cid> / pol:<cid>).
7// STANDALONE lib -- wires into no daemon (the operator-approved swap is a later step, like P1). license_tier: ORIGINAL
8import "nx_doc_at_rest.nx" // doc_get (decrypt confidential / passthrough public) + at_rest_confidential
9import "nx_docportal_lib.nx" // dp_prefix + DP_VIS_* + ss_begin/ss_add/ss_commit/ss_next_segid/ss_open/ss_hget
10
11// Attorney/partner tier -- the OR path in the need-to-know rule. Set ABOVE the daemon's CURRENT hardcoded session
12// level (2), so it is UNREACHABLE today => read is OWNER-ONLY until a REAL role model assigns level >= 3. This is a
13// deliberately fail-safe forward hook: the tier bypass cannot fire until roles exist (see the gate + flags).
14const AUTHZ_TIER_OWNER: i64 = 3
15
16func az_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
17func az_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 }
18func az_eqbytes(a: *u8, b: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { if a[i] != b[i] { return 0 } i = i + 1 } return 1 }
19// "own:<cid>" ownership key
20func az_ownkey(cid: i64, out: *u8) -> i64 {
21 var o: i64 = az_cat(out, 0, "own:" as *u8)
22 if cid == 0 { out[o] = 48 as u8; o = o + 1; out[o] = 0 as u8; return o }
23 let t: *u8 = sys_mmap(28); var k: i64 = 0; var m: i64 = cid
24 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
25 var i: i64 = 0; while i < k { out[o] = t[k - 1 - i]; o = o + 1; i = i + 1 }
26 out[o] = 0 as u8; return o
27}
28
29// Record the owner (the authenticated subject) of a doc -- own:<cid> = subject, additive in the (domain,vis) shard.
30// Call at CONFIDENTIAL upload time (the daemon, having the session subject, wires this next to doc_put). 0 ok / <0.
31func authz_set_owner(domain: *u8, vis: i64, cid: i64, subject: *u8) -> i64 {
32 let prefix: *u8 = sys_mmap(512); dp_prefix(domain, vis, prefix)
33 let okey: *u8 = sys_mmap(64); az_ownkey(cid, okey)
34 let sl: i64 = az_slen(subject)
35 let w: *i64 = ss_begin()
36 if ss_add(w, 1, okey, subject, sl) < 0 { return 0 - 1 }
37 let segid: i64 = ss_next_segid(prefix)
38 if ss_commit(prefix, w, segid) != 0 { return 0 - 2 }
39 return 0
40}
41
42// Read the owner subject into out (null-terminated). Returns length, 0 if NO ownership record (-> fail-closed).
43func authz_get_owner(domain: *u8, vis: i64, cid: i64, out: *u8, outcap: i64) -> i64 {
44 let prefix: *u8 = sys_mmap(512); dp_prefix(domain, vis, prefix)
45 let h: *i64 = ss_open(prefix)
46 if (h as i64) == 0 { return 0 }
47 let okey: *u8 = sys_mmap(64); az_ownkey(cid, okey)
48 let pq: *i64 = sys_mmap(16) as *i64
49 let lq: *i64 = sys_mmap(16) as *i64
50 if ss_hget(h, okey, pq, lq) != 1 { return 0 }
51 let vp: *u8 = pq[0] as *u8
52 let vlen: i64 = lq[0]
53 var i: i64 = 0
54 while i < vlen { if i < outcap { out[i] = vp[i] } i = i + 1 }
55 if vlen < outcap { out[vlen] = 0 as u8 }
56 return vlen
57}
58
59// THE DECISION (pure, gate-lockable). PUBLIC -> anyone. CONFIDENTIAL -> owner-only (own:<cid> == subject) OR the
60// attorney/partner tier (subject_level >= AUTHZ_TIER_OWNER, inert today). FAIL-CLOSED: no owner record / no match
61// -> 0. The caller MUST pass the SESSION-BOUND domain (never a free request param), so a subject can only reach its
62// OWN domain's shard -- cross-domain isolation is then inherent: this shard holds no owner row for another firm's cid.
63func authz_may_read(domain: *u8, vis: i64, cid: i64, subject: *u8, subject_level: i64) -> i64 {
64 if at_rest_confidential(vis) == 0 { return 1 } // PUBLIC corpus -> readable by anyone
65 let owner: *u8 = sys_mmap(256)
66 let olen: i64 = authz_get_owner(domain, vis, cid, owner, 255)
67 if olen <= 0 { return 0 } // FAIL-CLOSED: unknown owner -> deny
68 let slen: i64 = az_slen(subject)
69 if olen == slen { if az_eqbytes(owner, subject, olen) == 1 { return 1 } } // owner match
70 if subject_level >= AUTHZ_TIER_OWNER { return 1 } // attorney/partner tier (inert today)
71 return 0
72}
73
74// THE READ ROUTE (the missing authenticated private-read; it also wires the encryption DECRYPT side). If permitted
75// -> doc_get (nx_doc_at_rest decrypts confidential, passes public through) : DENY. Returns plaintext length, or <0
76// on DENY / not-found / decrypt-fail. out receives plaintext ONLY on success.
77func authz_doc_get(domain: *u8, vis: i64, cid: i64, subject: *u8, subject_level: i64, out: *u8) -> i64 {
78 if authz_may_read(domain, vis, cid, subject, subject_level) == 0 { return 0 - 1 }
79 return doc_get(domain, vis, cid, out)
80}