/** * Capability-oriented ACL evaluation (advisory; callers enforce verdict). * @param {{ user?: string, groups?: string[], caps?: string[] }} subject * @param {{ path?: string, pathClass?: string }} resource * @param {{ id?: string, effect: 'allow' | 'deny', users?: string[], groups?: string[], caps?: string[], pathPrefix?: string, pathClass?: string }[]} rules */ export function evaluateBareOsAcl(subject, resource, rules = []) { const u = String(subject?.user || '').trim() const groups = new Set( Array.isArray(subject?.groups) ? subject.groups.map((g) => String(g).trim()).filter(Boolean) : [] ) const caps = new Set( Array.isArray(subject?.caps) ? subject.caps.map((c) => String(c).trim()).filter(Boolean) : [] ) const path = String(resource?.path || '').replace(/\\/g, '/') const pClass = String(resource?.pathClass || '').trim() /** @type {{ id?: string, effect: 'allow' | 'deny' }[]} */ const matched = [] let verdict = 'neutral' for (const r of rules) { if (!r || (r.effect !== 'allow' && r.effect !== 'deny')) continue if (r.pathClass && r.pathClass !== pClass) continue if (r.pathPrefix && !path.startsWith(String(r.pathPrefix))) continue let hit = false if (r.users && r.users.length) { if (u && r.users.includes(u)) hit = true } if (!hit && r.groups && r.groups.length) { for (const g of r.groups) { if (groups.has(g)) { hit = true break } } } if (!hit && r.caps && r.caps.length) { for (const c of r.caps) { if (caps.has(c)) { hit = true break } } } if (!hit && !r.users && !r.groups && !r.caps) hit = true if (!hit) continue matched.push({ id: r.id, effect: r.effect }) verdict = r.effect } return { subject: { user: u, groupCount: groups.size, capCount: caps.size }, resource: { path, pathClass: pClass }, matched, verdict, atMs: Date.now() } }