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}