Files
bare-operating-system/packages/bare-os-booter/lib/bare-os-vfs-acl-enforce.js
T
Raven Scott 433cb2176d feat: complete Bare OS POSIX mega-plan (20-track)
Migrate booter and seeder host file/path access to bare-fs/bare-path via package
imports while keeping Node defaults for dev/CI; extend CI to reject bare fs/path
imports on Pear surfaces.

Wire bareOsPearUpdaterDelegate through pear-runtime-updater-style dynamic import,
expand process table and signal routing, add system RO alias mount and optional
Hyperbee bin hint, protomux proc metrics, cron dom/dow OR rule, boot-perf detail
with optional bare-hrtime, /lib/bare warm cache with metrics_live warmReadCache,
vault threat-model and users-manual crypto pointers, Pear inspect emit, gated
shell break/continue, kernel-ext-graph output and example schema, POSIX compliance
matrix + verifier in pretest.

Harden ACL sidecar evaluation toward POSIX-like mask semantics; tighten test/expr/
printf edge cases and man JSON; add blind-bootstrap/DHT registry protocol test and
troubleshooting notes; refresh handbook, KERNEL_CONTRACT, environment appendix, and
reference hub links.
2026-04-04 22:13:19 -04:00

169 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Optional ACL enforcement using `PATH.bare_acl` sidecars (getfacl/setfacl layout).
*
* Evaluates POSIX ACLshaped lines: `user::rwx`, `user:UID:rwx`, `group::rwx`,
* `group:GID:rwx`, `other::rwx`, `mask::rwx`. The mask caps named users, named
* groups, and the owning group entry (`group::`); `user::` and `other::` are not
* masked (Linux acl(5) semantics). On the personal drive the object owner defaults
* to `env.UID` / `env.GID` when resolving `user::` / `group::` (override with
* `BARE_OS_ACL_OBJECT_UID` / `BARE_OS_ACL_OBJECT_GID` when metadata differs).
*/
import b4a from 'b4a'
/** @param {string} triple */
function parseTriple(triple) {
const t = String(triple || '')
if (t.length !== 3) return null
return {
r: t[0] === 'r',
w: t[1] === 'w',
x: t[2] === 'x'
}
}
function intersectPerm(a, b) {
return { r: a.r && b.r, w: a.w && b.w, x: a.x && b.x }
}
/** @param {{ r: boolean, w: boolean, x: boolean } | null} p @param {'read' | 'write'} op */
function permAllows(p, op) {
if (!p) return true
if (op === 'read') return p.r
if (op === 'write') return p.w
return false
}
/**
* @param {string} text
* @returns {{
* userObj: ReturnType<typeof parseTriple> | null
* groupObj: ReturnType<typeof parseTriple> | null
* otherObj: ReturnType<typeof parseTriple> | null
* mask: ReturnType<typeof parseTriple> | null
* namedUsers: Map<string, ReturnType<typeof parseTriple>>
* namedGroups: Map<string, ReturnType<typeof parseTriple>>
* }}
*/
export function bareOsParseBareAclText(text) {
const namedUsers = new Map()
const namedGroups = new Map()
let userObj = null
let groupObj = null
let otherObj = null
let mask = null
for (const line0 of String(text || '').split(/\r?\n/)) {
const line = line0.replace(/^\s+/, '').split(/\s*#/)[0].trim()
if (!line) continue
let m = line.match(/^user::([rwx-]{3})$/)
if (m) {
userObj = parseTriple(m[1])
continue
}
m = line.match(/^group::([rwx-]{3})$/)
if (m) {
groupObj = parseTriple(m[1])
continue
}
m = line.match(/^other::([rwx-]{3})$/)
if (m) {
otherObj = parseTriple(m[1])
continue
}
m = line.match(/^mask::([rwx-]{3})$/)
if (m) {
mask = parseTriple(m[1])
continue
}
m = line.match(/^user:([^:]+):([rwx-]{3})$/)
if (m) {
namedUsers.set(String(m[1]).trim(), parseTriple(m[2]))
continue
}
m = line.match(/^group:([^:]+):([rwx-]{3})$/)
if (m) {
namedGroups.set(String(m[1]).trim(), parseTriple(m[2]))
continue
}
}
const maskOrAll = mask || { r: true, w: true, x: true }
return {
userObj,
groupObj,
otherObj,
mask: maskOrAll,
namedUsers,
namedGroups,
_rawMask: mask
}
}
/**
* @param {ReturnType<typeof bareOsParseBareAclText>} parsed
* @param {Record<string, string>} env
* @param {string} uidStr
* @param {string} gidStr
* @param {'read' | 'write'} op
*/
export function bareOsAclDeniesSubject(parsed, env, uidStr, gidStr, op) {
const objectUid = String(
env.BARE_OS_ACL_OBJECT_UID != null ? env.BARE_OS_ACL_OBJECT_UID : env.UID || ''
).trim()
const objectGid = String(
env.BARE_OS_ACL_OBJECT_GID != null ? env.BARE_OS_ACL_OBJECT_GID : env.GID || ''
).trim()
const mask = parsed.mask || { r: true, w: true, x: true }
const isOwner = objectUid !== '' && uidStr === objectUid
if (isOwner && parsed.userObj) {
return !permAllows(parsed.userObj, op)
}
const nu = parsed.namedUsers.get(uidStr)
if (nu) {
return !permAllows(intersectPerm(nu, mask), op)
}
const inOwningGroup = objectGid !== '' && gidStr === objectGid
if (inOwningGroup && parsed.groupObj) {
return !permAllows(intersectPerm(parsed.groupObj, mask), op)
}
const ng = parsed.namedGroups.get(gidStr)
if (ng) {
return !permAllows(intersectPerm(ng, mask), op)
}
if (parsed.otherObj) {
return !permAllows(parsed.otherObj, op)
}
return false
}
/**
* @param {Record<string, string>} env
* @param {import('hyperdrive').default} drive
* @param {string} drivePath absolute path on the Hyperdrive (no leading slash inconsistency — callers pass the same `path` as `drive.get`)
* @param {'read' | 'write'} op
*/
export async function bareOsVfsAclDeniesDriveOp(env, drive, drivePath, op) {
const on =
env.BARE_OS_VFS_ENFORCE_ACL === '1' ||
env.BARE_OS_VFS_ENFORCE_ACL === 'true'
if (!on || !drive || typeof drive.get !== 'function') return false
const p = String(drivePath || '').replace(/\/+$/, '') || '/'
const sidePath = p + '.bare_acl'
let raw
try {
raw = await drive.get(sidePath, { follow: false })
} catch {
return false
}
if (!raw || !raw.byteLength) return false
const text = b4a.toString(raw)
const uidStr = String(env.UID != null ? env.UID : '').trim()
const gidStr = String(env.GID != null ? env.GID : '').trim()
const parsed = bareOsParseBareAclText(text)
return bareOsAclDeniesSubject(parsed, env, uidStr, gidStr, op)
}