code wiki / _hdl_build / nx_mgmt_authz.nx
nx_mgmt_authz.nx source
↩ module page · 75 lines · 3799 B
1// nx_mgmt_authz.nx -- the AUTHORIZATION tier of the management plane (closes the documented privilege-escalation:
2// today ma_authed (nx_mgmt_api.nx:248) = nx_sa_validate ONLY, so ANY valid session -- including a plain library
3// reader, because the deployed daemon reuses the shared library auth store -- can call /api/deploy|rollback|
4// reconcile|restart|migrate|update). This adds least-privilege: PUBLIC routes need nothing, READ routes need any
5// valid session, PRIVILEGED (mutating) routes need a valid session AND an admin handle. PURE logic (mz_*, imports
6// only nx_syscalls) -> isolation-gateable; it slots into ma_handle in place of the bare ma_authed check (the
7// deploy-coupled wiring step). Mirrors the hexagonal CORE pattern of nx_mgmt_core. license_tier: ORIGINAL
8import "nx_syscalls.nx"
9
10// route privilege classes
11const MZ_PUBLIC: i64 = 0 // no auth: GET /api, GET /api/login
12const MZ_READ: i64 = 1 // any valid session: /api/health, /api/services
13const MZ_PRIV: i64 = 2 // valid session AND admin: /api/{deploy,rollback,reconcile,restart,migrate,update}
14
15func mz_starts(path: *u8, pn: i64, s: *u8) -> i64 {
16 var i: i64 = 0
17 while s[i] != (0 as u8) {
18 if i >= pn { return 0 }
19 if (path[i] as i64) != (s[i] as i64) { return 0 }
20 i = i + 1
21 }
22 return 1
23}
24
25// classify a request path into its required privilege (mirrors ma_handle's route set exactly).
26func mz_route_class(path: *u8, pn: i64) -> i64 {
27 if mz_starts(path, pn, "/api/deploy" as *u8) == 1 { return MZ_PRIV }
28 if mz_starts(path, pn, "/api/rollback" as *u8) == 1 { return MZ_PRIV }
29 if mz_starts(path, pn, "/api/reconcile" as *u8) == 1 { return MZ_PRIV }
30 if mz_starts(path, pn, "/api/restart" as *u8) == 1 { return MZ_PRIV }
31 if mz_starts(path, pn, "/api/migrate" as *u8) == 1 { return MZ_PRIV }
32 if mz_starts(path, pn, "/api/update" as *u8) == 1 { return MZ_PRIV }
33 if mz_starts(path, pn, "/api/health" as *u8) == 1 { return MZ_READ }
34 if mz_starts(path, pn, "/api/services" as *u8) == 1 { return MZ_READ }
35 return MZ_PUBLIC
36}
37
38// THE authorization decision -> the HTTP status the router should emit.
39// 200 = allowed · 401 = unauthenticated (no/invalid session) · 403 = authenticated but NOT admin (the escalation fix).
40func mz_status(cls: i64, valid: i64, is_admin: i64) -> i64 {
41 if cls == MZ_PUBLIC { return 200 }
42 if valid == 0 { return 401 }
43 if cls == MZ_READ { return 200 }
44 if is_admin == 1 { return 200 }
45 return 403
46}
47
48// boolean convenience: is the request allowed?
49func mz_allowed(cls: i64, valid: i64, is_admin: i64) -> i64 { if mz_status(cls, valid, is_admin) == 200 { return 1 } return 0 }
50
51func mz_slice_eq(a: *u8, ao: i64, al: i64, b: *u8, bo: i64, bl: i64) -> i64 {
52 if al != bl { return 0 }
53 var i: i64 = 0
54 while i < al { if (a[ao + i] as i64) != (b[bo + i] as i64) { return 0 } i = i + 1 }
55 return 1
56}
57
58// data-driven admin allowlist matcher (PURE over a buffer; the file read is the caller's data adapter).
59// allowlist = one handle per line; '#' starts a comment line; trailing '\r' tolerated. exact handle match only.
60func mz_is_admin_in(buf: *u8, n: i64, hbuf: *u8, hoff: i64, hlen: i64) -> i64 {
61 if hlen == 0 { return 0 }
62 var cur: i64 = 0
63 while cur < n {
64 var le: i64 = cur
65 var f: i64 = 0
66 while f == 0 { if le >= n { f = 1 } else { if (buf[le] as i64) == 10 { f = 1 } else { le = le + 1 } } }
67 var end: i64 = le
68 if end > cur { if (buf[end - 1] as i64) == 13 { end = end - 1 } } // strip trailing '\r'
69 if end > cur { if (buf[cur] as i64) != 35 { // 35 = '#' comment
70 if mz_slice_eq(buf, cur, end - cur, hbuf, hoff, hlen) == 1 { return 1 }
71 } }
72 cur = le + 1
73 }
74 return 0
75}