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.
169 lines
4.9 KiB
JavaScript
169 lines
4.9 KiB
JavaScript
/**
|
||
* Optional ACL enforcement using `PATH.bare_acl` sidecars (getfacl/setfacl layout).
|
||
*
|
||
* Evaluates POSIX ACL–shaped 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)
|
||
}
|