/** * 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 | null * groupObj: ReturnType | null * otherObj: ReturnType | null * mask: ReturnType | null * namedUsers: Map> * namedGroups: Map> * }} */ 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} parsed * @param {Record} 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} 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) }