Files
platform/src/servers/os-user-verify.ts
T
pastilhas bf57201a11 bun run doctor, and provisioning checks its own work
the tenth install in a row with something new wrong, and the pattern behind
them is not the individual faults. every step in provisionOsAccount is
deliberately non-fatal — a member whose account provisioned fine should not be
rolled back because their shell config failed. that is right, and it means
"nothing reported an error" is a far weaker statement than it reads as.

the acl-mask bug was the sharp version: a clean provisioning run, every named
entry correct, and an account that could reach nothing. it was found by a
person three days later, through a file browser message three steps removed
from the cause.

so provisioning now VERIFIES rather than assuming, and it verifies by making
the same readdir the file browser makes. deliberately not by inspecting the
acl: reading getfacl and reasoning about masks is exactly how this was missed,
because the acl looked right. a check that tests something adjacent to the
failure is how you get a green report and a broken server.

a failed check is reported FIRST, ahead of keys and shells — an account that
cannot read its own home is broken in a way that working ssh does not make up
for.

and `bun run doctor` for the nine servers that already exist: same check
across every account, plus the one that is invisible by construction — a
confined grant held by an account with no linux user, which sits in the table,
shows as ticked in the UI, and resolves to nothing.

read-only, so it is safe on a production box. exits 1, so it can gate the end
of a setup run. same posture as assertPermissionTotality, except this reports
instead of refusing to boot: one broken member must not stop the platform
serving everyone else.
2026-08-19 23:07:47 +00:00

80 lines
3.5 KiB
TypeScript

import { readdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { osUserHome } from './os-user';
// Checking that a member's Linux account actually WORKS, rather than that provisioning reported success.
//
// ── Why this exists ──
//
// Every step in `provisionOsAccount` is deliberately non-fatal: a member whose account provisioned fine
// should not be rolled back because their shell config failed. That is right, and it has a cost — the
// failures accumulate silently, and the owner learns about them from a file browser saying "Officer
// cannot read this folder", which is three steps removed from the cause.
//
// The 2026-08-19 case was the sharp version. Every named ACL entry was correct. The Linux user existed.
// The home existed. The grants were right. And the platform still could not read the home, because a
// later `chmod 700` had recomputed the ACL mask to `---` and clamped the entries to nothing. Nothing in
// the pipeline was in a position to notice, because nothing tried the thing that actually matters.
//
// So: try it. `readdir` on the member's home, as the process that will do it for real. It is the same
// call the file browser makes, which is the point — a check that tests something ADJACENT to the failure
// is how you get a green report and a broken server.
//
// This is the same posture as `assertPermissionTotality` and `assertInstallLayout`: state the invariant,
// verify it, and say plainly what is wrong. The difference is that those refuse to boot and this does
// not — a member with a broken home must not stop the platform serving everyone else.
export type OsAccountCheck = {
email: string;
osUser: string | null;
home: string;
ok: boolean;
/** Null when `ok`. Otherwise what is wrong and what fixes it. */
problem: string | null;
};
/**
* Can the platform actually read this member's home?
*
* Deliberately not a permissions inspection. Reading `getfacl` and reasoning about masks is how this was
* missed in the first place: the ACL *looked* right. The only trustworthy question is whether the call
* the file browser makes succeeds.
*/
export async function checkOsAccount(email: string, osUser: string | null): Promise<OsAccountCheck> {
const home = osUserHome(email);
const base = { email, osUser, home };
if (!osUser) {
return {
...base,
ok: false,
problem:
'no Linux user — every `confined` permission (files, terminal, chat) is dropped for this account, ' +
'whatever the role grants. Fix: POST /api/users/<id>/provision-linux, or Settings → User management.',
};
}
if (!existsSync(home)) {
return { ...base, ok: false, problem: `home does not exist at ${home}. Fix: re-provision the Linux account.` };
}
try {
await readdir(home);
return { ...base, ok: true, problem: null };
} catch (ex) {
const code = (ex as { code?: string }).code;
if (code === 'EACCES' || code === 'EPERM') {
return {
...base,
ok: false,
problem:
`the platform cannot read ${home} (${code}). The named ACL entries may look correct and still ` +
`grant nothing: a chmod recomputes the ACL mask from the group bits, so a directory closed to ` +
`700 gets \`mask::---\` and clamps every entry to \`#effective:---\`. ` +
`Fix: re-provision the Linux account, or \`sudo setfacl -R -m m::rwx -m d:m::rwx '${home}'\`.`,
};
}
return { ...base, ok: false, problem: `could not read ${home}: ${ex instanceof Error ? ex.message : ex}` };
}
}