lib/os-user-diagnostics.js
Rama main · réplica de lectura del repositorio original.
// Best-effort, read-only diagnostics for the current OS-user compatibility mode.
var fs = require("fs");
var childProcess = require("child_process");
var path = require("path");
var execFileSync = childProcess.execFileSync;
var osUsers = require("./os-users");
function commandAvailable(command, execFile) {
try {
execFile(command, ["--help"], { stdio: "ignore", timeout: 5000 });
return true;
} catch (e) {
return false;
}
}
function credentialPaths(home) {
if (!home) return [];
return require("./yoke/maintenance").credentialPaths(home).concat([
home + "/.config/gh",
home + "/.config/gcloud",
]);
}
function accessStatus(checks) {
var names = ["read", "traverse", "write"];
for (var i = 0; i < names.length; i++) {
if (!checks[names[i]] || checks[names[i]].status === "unknown") return "unknown";
}
for (var j = 0; j < names.length; j++) {
if (checks[names[j]].status === "deny") return "deny";
}
return "allow";
}
function unknownChecks() {
return {
read: { status: "unknown" },
traverse: { status: "unknown" },
write: { status: "unknown" },
};
}
function projectAccessScript(projectPath) {
return [
"var fs = require('fs');",
"var target = " + JSON.stringify(projectPath) + ";",
"var checks = {};",
"function check(name, mode) {",
" try { fs.accessSync(target, mode); checks[name] = { status: 'allow' }; }",
" catch (error) { checks[name] = { status: 'deny', code: String(error && error.code || 'EACCES') }; }",
"}",
"check('read', fs.constants.R_OK);",
"check('traverse', fs.constants.X_OK);",
"check('write', fs.constants.W_OK);",
"process.stdout.write(JSON.stringify(checks));",
].join(" ");
}
/**
* Check directory access as the mapped identity without creating or changing
* any file. The spawn wrapper intentionally follows the runtime's current
* supplementary-group behavior, including its setpriv fallback.
*/
function probeMappedProjectAccess(project, user, options) {
options = options || {};
var result = {
projectSlug: project && project.slug || null,
userId: user && user.id || null,
result: "unknown",
checks: unknownChecks(),
};
if (!project || !project.path) {
result.evidence = "project_path_unavailable";
return result;
}
if (!user || !user.linuxUser) {
result.evidence = "linux_mapping_missing";
return result;
}
var resolveUser = options.resolveUser || osUsers.resolveOsUserInfo;
var wrapSpawn = options.wrapSpawn || osUsers.wrapSpawnAsUser;
var execFile = options.execFile || execFileSync;
var nodePath = options.nodePath || process.execPath;
var account;
try {
account = resolveUser(user.linuxUser);
} catch (e) {
result.evidence = "linux_account_unavailable";
return result;
}
if (!account || account.uid == null || account.gid == null) {
result.evidence = "linux_account_invalid";
return result;
}
try {
var spawn = wrapSpawn(nodePath, ["-e", projectAccessScript(project.path)], {
encoding: "utf8",
timeout: 5000,
stdio: "pipe",
uid: account.uid,
gid: account.gid,
}, options.setprivCheck);
var output = execFile(spawn.command, spawn.args, spawn.options);
var checks = JSON.parse(String(output).trim());
if (!checks || typeof checks !== "object") throw new Error("Invalid access probe result");
result.checks = checks;
result.result = accessStatus(checks);
result.evidence = "mapped_identity_access";
} catch (e) {
result.evidence = "probe_unavailable";
}
return result;
}
function collectUserDiagnostics(user, options) {
options = options || {};
var resolveUser = options.resolveUser || osUsers.resolveOsUserInfo;
var exists = options.exists || fs.existsSync;
var execFile = options.execFile || execFileSync;
var result = {
userId: user && user.id || null,
linuxUser: user && user.linuxUser || null,
mapping: "missing",
account: "unknown",
supplementaryGroups: "unknown",
credentialPaths: [],
};
if (!user || !user.linuxUser) return result;
var account;
try {
account = resolveUser(user.linuxUser);
result.mapping = "resolved";
result.account = "present";
} catch (e) {
result.mapping = "unavailable";
result.account = "missing";
result.evidence = "linux_account_unavailable";
return result;
}
try {
var output = execFile("id", ["-Gn", user.linuxUser], { encoding: "utf8", timeout: 5000, stdio: "pipe" });
result.supplementaryGroups = output.trim() ? output.trim().split(/\s+/) : [];
} catch (e) {
result.supplementaryGroups = "unknown";
}
var paths = credentialPaths(account.home);
for (var i = 0; i < paths.length; i++) {
var present = "unknown";
try { present = !!exists(paths[i]); } catch (e) {}
result.credentialPaths.push({ path: paths[i], present: present });
}
return result;
}
function collectOsUserDiagnostics(options) {
options = options || {};
var users = options.users || [];
var projects = options.projects || [];
var probeProjectAccess = options.probeProjectAccess || probeMappedProjectAccess;
var execFile = options.execFile || execFileSync;
var result = {
collectedAt: Date.now(),
setprivAvailable: commandAvailable("setpriv", execFile),
users: [],
projectAccess: [],
};
for (var i = 0; i < users.length; i++) {
try {
result.users.push(collectUserDiagnostics(users[i], options));
} catch (e) {
result.users.push({
userId: users[i] && users[i].id || null,
linuxUser: users[i] && users[i].linuxUser || null,
mapping: "unknown",
account: "unknown",
supplementaryGroups: "unknown",
credentialPaths: [],
});
}
}
for (var pi = 0; pi < projects.length; pi++) {
var project = projects[pi];
for (var ui = 0; ui < users.length; ui++) {
var user = users[ui];
if (!project || !user) continue;
var probe;
try {
probe = probeProjectAccess(project, user, options);
if (!probe || typeof probe !== "object") probe = { result: "unknown", evidence: "invalid_probe_result" };
} catch (e) {
probe = { result: "unknown", evidence: "probe_failed" };
}
if (!probe.projectSlug) probe.projectSlug = project.slug || null;
if (!probe.userId) probe.userId = user.id || null;
if (!probe.result) probe.result = "unknown";
result.projectAccess.push(probe);
}
}
return result;
}
function summarizeDiagnostics(result) {
var users = result.users || [];
var projectAccess = result.projectAccess || [];
var unavailableMappings = 0;
var unknownProbes = 0;
for (var i = 0; i < users.length; i++) {
if (users[i].mapping !== "resolved") unavailableMappings++;
}
for (var j = 0; j < projectAccess.length; j++) {
if (projectAccess[j].result === "unknown") unknownProbes++;
}
return "OS-user diagnostics: " + users.length + " user mappings, " + unavailableMappings + " unavailable, " + projectAccess.length + " project probes, " + unknownProbes + " unavailable";
}
function diagnosticsPayload(options) {
var users = typeof options.getUsers === "function" ? options.getUsers() : options.users || [];
var projects = typeof options.getProjects === "function" ? options.getProjects() : options.projects || [];
return {
users: users.map(function(user) {
return { id: user && user.id || null, linuxUser: user && user.linuxUser || null };
}),
projects: projects.map(function(project) {
return { slug: project && project.slug || null, path: project && project.path || null };
}),
};
}
function logDiagnosticMessage(logger, level, message) {
try {
if (logger && typeof logger[level] === "function") logger[level]("[daemon] " + message);
} catch (e) {}
}
/**
* Run blocking host probes in a short-lived child so diagnostics cannot stall
* the daemon event loop after it starts serving requests.
*/
function scheduleOsUserDiagnosticsAsync(options) {
options = options || {};
var schedule = options.schedule || setTimeout;
var clearSchedule = options.clearSchedule || clearTimeout;
var spawn = options.spawn || childProcess.spawn;
var logger = options.logger || console;
var timeoutMs = options.timeoutMs || 30000;
try {
schedule(function() {
var child;
var timeout;
var output = "";
var completed = false;
function finish(level, message) {
if (completed) return;
completed = true;
if (timeout) clearSchedule(timeout);
logDiagnosticMessage(logger, level, message);
}
try {
child = spawn(options.nodePath || process.execPath, [path.join(__dirname, "os-user-diagnostics-worker.js")], {
stdio: ["pipe", "pipe", "ignore"],
});
if (!child || !child.stdin || !child.stdout || typeof child.once !== "function") {
finish("warn", "OS-user diagnostics unavailable");
return;
}
child.stdout.on("data", function(chunk) { output += String(chunk); });
child.once("error", function() { finish("warn", "OS-user diagnostics unavailable"); });
child.once("close", function(code) {
if (completed) return;
if (code !== 0) {
finish("warn", "OS-user diagnostics unavailable");
return;
}
try {
var report = JSON.parse(output);
if (!report || typeof report.summary !== "string") throw new Error("Invalid diagnostics report");
finish("log", report.summary);
} catch (e) {
finish("warn", "OS-user diagnostics unavailable");
}
});
timeout = schedule(function() {
try { child.kill(); } catch (e) {}
finish("warn", "OS-user diagnostics timed out");
}, timeoutMs);
child.stdin.end(JSON.stringify(diagnosticsPayload(options)));
} catch (e) {
try { if (child && typeof child.kill === "function") child.kill(); } catch (killError) {}
finish("warn", "OS-user diagnostics unavailable");
}
}, 0);
} catch (e) {
logDiagnosticMessage(logger, "warn", "OS-user diagnostics could not be scheduled");
}
}
module.exports = {
collectOsUserDiagnostics: collectOsUserDiagnostics,
collectUserDiagnostics: collectUserDiagnostics,
credentialPaths: credentialPaths,
probeMappedProjectAccess: probeMappedProjectAccess,
scheduleOsUserDiagnosticsAsync: scheduleOsUserDiagnosticsAsync,
summarizeDiagnostics: summarizeDiagnostics,
};