import { getUserById } from 'officerdb'; import { getHomeDir, getOwnerHomeDir } from './data-path'; // Whose home does a session run in. // // This is the seam the whole per-user story turns on, and it replaces `getOwnerHomeDir(email)` at every // point where a REQUEST decides which directory it may touch. That function takes an email and discards it // whenever `HOME_DIR` is set — which is always, on a real install — so every caller resolved to the owner's // login home regardless of who was asking. Harmless while the surfaces around it were owner-only. Not // harmless the moment a member can open a file browser. // // ── Why a member with no Linux account is REFUSED, not defaulted ── // // The tempting fallback is "no `osUser`? use the managed home under DATA_PATH anyway." It would work, and // it would be wrong in the one direction that matters: a member's files would sit in a directory the // platform's own service user owns and every other part of the system can read, while the UI showed them a // private-looking home. Worse, an error in the resolution order — an owner check that stopped matching, say // — would land a member in the OWNER'S home with write access, silently. // // So the rule is: a non-owner reaches an execution surface only if the kernel is enforcing the boundary. // No Linux account means no confinement means no access, and the refusal names the fix. export type HomeResolution = | { ok: true; home: string; isOwner: boolean } | { ok: false; reason: string; needsOsAccount: boolean }; /** * The directory this account's file browser, terminal and (eventually) agents are confined to. * * Reads the row rather than trusting the token, for the same reason `authorize.ts` re-reads `role`: a claim * minted at sign-in would keep working for thirty days after the account it describes changed. Enabling OS * users, or provisioning one for an existing member, must take effect on the next request. */ export async function resolveHomeDir(userId: number): Promise { let user; try { user = await getUserById(userId); } catch { // Same posture as every catch in authorize.ts: a database error denies. There is no safe guess about // whose home to open. return { ok: false, reason: 'could not resolve your account', needsOsAccount: false }; } if (!user) return { ok: false, reason: 'account not found', needsOsAccount: false }; // The owner runs in their real login home — the whole point of HOME_DIR, and what makes platform // terminals share config and credentials with the shell they use outside Officer. if (user.role === 'Super Admin') { return { ok: true, home: getOwnerHomeDir(user.email), isOwner: true }; } if (!user.osUser) { return { ok: false, reason: 'this account has no Linux user on this machine, so there is nothing to confine it to', needsOsAccount: true, }; } // `getHomeDir` and `osUserHome` are deliberately the same path: DATA_PATH//home is both the // managed home the platform provisions and the real passwd home of the Linux account. If those ever // diverge, a member's shell and their file browser would show different directories. return { ok: true, home: getHomeDir(user.email), isOwner: false }; }