e94038481e
- File Explorer (Files in the sidebar, /files; also from a workspace's Files tab, portalled so no panel clips it), laid out like GNOME Files: places (Home, workspaces, Trash), Back/Forward/Up, path bar, search, icon grid with image thumbnails or list, labelled New/Upload, selection action bar, hidden files behind Ctrl+H, right-click menus and the usual keys. Make, rename, move, copy (within a workspace, between workspaces, home ↔ workspace), delete into a per-account trash restorable for 7 days, streamed upload of files and whole folders (≤ 500 MB a batch, Replace / Keep both / Skip on a taken name), download a file or a zip of anything. server/lib/file-explorer.js, /api/lanes/:id/fs/*, /api/homes/:who/fs/*. - The user's home: every dot entry at its top stays hidden and refused (also via a symlink, download, zip or as a copy source); an admin may view other users' homes read-only, audited. - write-as learns move/copy (re-run `sudo agenthub server install-system`). - Hardening: a write to a workspace re-checks its owner by the live role; S3 keeps its server.conf if the file stops reading and a tmux call never falls back to the host's server; the AI Proxy gateway marks what it relays and the dashboard refuses it; a NUL in a pane's argv/env is refused. - OpenAPI for the new routes (server/openapi-extra/files.js); docs (API, SERVER-MODE, CLAUDE.md) updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.2 KiB
JavaScript
84 lines
3.2 KiB
JavaScript
/**
|
|
* @file audit.js
|
|
* @description Server mode's audit trail (docs/SERVER-MODE.md): every read by
|
|
* an admin or analyst of a session that a MEMBER owns — the session page, its
|
|
* transcript, its events, its cost — is written to `audit_log` with who read
|
|
* it, whose it was and which path. `auditGuard` runs on `/api` after the
|
|
* scope guard; it never blocks a request, and it is a no-op in local mode and
|
|
* for a member (who can only read their own). An admin's account change on
|
|
* the Users page is written here too (`recordAccountChange`), and so is an
|
|
* admin's look into another user's home (`recordHomeRead`, File Explorer).
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const sql = (q) => require("../db").db.prepare(q);
|
|
|
|
// Paths that name one session's content.
|
|
const SESSION_PATHS = [
|
|
/^\/sessions\/(?!facets\/?$)([^/]+)/i,
|
|
/^\/pricing\/cost\/([^/]+)/i,
|
|
/^\/workflows\/session\/([^/]+)/i,
|
|
];
|
|
|
|
function auditGuard(req, res, next) {
|
|
const u = req.user;
|
|
if (!u || u.role === "developer" || (req.method !== "GET" && req.method !== "HEAD"))
|
|
return next();
|
|
const p = require("./auth").apiPath(req);
|
|
for (const re of SESSION_PATHS) {
|
|
const m = re.exec(p);
|
|
if (!m) continue;
|
|
try {
|
|
const { sessionOwners } = require("./scope");
|
|
const id = m[1];
|
|
for (const owner of sessionOwners(id)) {
|
|
if (owner === u.id) continue;
|
|
const ownerRole = sql("SELECT role FROM users WHERE id = ?").get(owner)?.role;
|
|
if (ownerRole !== "developer") continue;
|
|
sql(
|
|
"INSERT INTO audit_log (user_id, owner_id, session_id, method, path) VALUES (?, ?, ?, ?, ?)"
|
|
).run(u.id, owner, id, req.method, req.originalUrl.slice(0, 500));
|
|
}
|
|
} catch (err) {
|
|
console.warn("[audit] could not record:", err.message);
|
|
}
|
|
break;
|
|
}
|
|
return next();
|
|
}
|
|
|
|
/** The newest entries, with user names. */
|
|
function listAudit(limit = 100) {
|
|
const n = Math.min(Math.max(Number(limit) || 100, 1), 1000);
|
|
return sql(
|
|
`SELECT a.id, a.at, a.method, a.path, a.session_id,
|
|
r.username AS reader, o.username AS owner,
|
|
r.display_name AS reader_display, o.display_name AS owner_display
|
|
FROM audit_log a
|
|
LEFT JOIN users r ON r.id = a.user_id
|
|
LEFT JOIN users o ON o.id = a.owner_id
|
|
ORDER BY a.id DESC LIMIT ?`
|
|
).all(n);
|
|
}
|
|
|
|
/** An admin's account change from the Users page: who, on whom, what (never
|
|
* a password). `owner_id` is the account changed; `path` carries the action. */
|
|
function recordAccountChange(actorId, targetId, method, action) {
|
|
sql(
|
|
"INSERT INTO audit_log (user_id, owner_id, session_id, method, path) VALUES (?, ?, NULL, ?, ?)"
|
|
).run(actorId, targetId, method, String(action).slice(0, 500));
|
|
}
|
|
|
|
/** An admin opening another user's home in the File Explorer (read-only). */
|
|
function recordHomeRead(actorId, ownerId, method, what) {
|
|
try {
|
|
sql(
|
|
"INSERT INTO audit_log (user_id, owner_id, session_id, method, path) VALUES (?, ?, NULL, ?, ?)"
|
|
).run(actorId, ownerId, method, String(what).slice(0, 500));
|
|
} catch (err) {
|
|
console.warn("[audit] could not record:", err.message);
|
|
}
|
|
}
|
|
|
|
module.exports = { auditGuard, listAudit, recordAccountChange, recordHomeRead };
|