per-user linux accounts, stage 1: the account and the privilege drop
A member gets a real Linux account whose home is the directory the platform already
provisions for them. Nothing uses it yet — this is the mechanism plus the account,
deliberately with no behaviour change, so the file browser and terminal can be moved
onto something already proven.
Bun.spawn silently ignores uid/gid. Verified on 1.3.10: from uid 1000,
Bun.spawn(['id','-u'], {uid: 65534}) exits 0 and prints 1000. No throw, no warning.
Bun's types don't declare the option so typed code can't reach it by accident, but the
runtime accepts it, and a silently absent isolation boundary is the worst outcome this
feature could have. So privilege drops go through sudo -n setpriv, and a test pins Bun's
behaviour — if it's ever implemented, that test tells us we may simplify.
sudo is required for the drop and not because of the uid: --init-groups fails with
"Operation not permitted" for an unprivileged caller even when reuid'ing to its own
account, because setgroups(2) is root-only. --reset-env is what stops the platform's
environment crossing; verified POSTGRES_URL is unset on the far side and HOME arrives
from the target's passwd entry.
Three bugs that only a real run with a real useradd could find:
- chmod after chown fails forever, because chmod needs ownership. Both orderings fail
unprivileged. Both operations now go through sudo, which is what makes it re-runnable.
- a member could read ANOTHER member's home: provisionUserDirs created at the default
umask (755) and only the account being created got confined. An unlistable parent is
no protection when the child is world-readable and emails are guessable. The skeleton
is now created closed, 711 on the account dir and 700 inside.
- platform/.env was 664 and a member's shell printed JWT_SECRET, which is enough to mint
an owner token and bypass every capability check. Now a boot check that refuses to
start with OFFICER_OS_USERS on while any .env in the project root is group- or
world-readable.
Design, the measured results and the staging plan: docs/per-user-linux-accounts.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import type { Handler } from 'hono';
|
||||
import { createUser, getUserByEmail, getUserByUsername, USER_ROLES } from 'officerdb';
|
||||
import { createUser, updateUser, getUserByEmail, getUserByUsername, USER_ROLES } from 'officerdb';
|
||||
import type { UserRole } from 'officerdb';
|
||||
import argon2 from 'argon2';
|
||||
import * as errors from '@@/custom-errors';
|
||||
import { provisionUserDirs } from '@@/data-path';
|
||||
import { OS_USERS_ENABLED, ensureOsUser } from '@@/os-user';
|
||||
import { validatePassword } from '../auth/validate-password';
|
||||
import { validateUsername } from '../auth/validate-username';
|
||||
import { toPublicUser } from './manage-users';
|
||||
@@ -85,5 +86,25 @@ export const createUserHandler: Handler = async function (ctx) {
|
||||
console.warn(`[users] created ${email} but could not provision its data directories`, ex);
|
||||
}
|
||||
|
||||
return ctx.json({ user: toPublicUser(user) }, 201);
|
||||
// The Linux account, when the host is set up for it. Same posture as the directories and for the same
|
||||
// reason: this is a side effect of creating a platform account, and a failed `useradd` must not undo an
|
||||
// account that otherwise exists and can sign in. The row simply keeps `osUser: null`, which every
|
||||
// consumer already has to handle — that is what an account made before this feature looks like.
|
||||
//
|
||||
// Reported back in the response rather than only logged, so the owner sees "created, but no OS account"
|
||||
// at the moment they click rather than discovering it when a terminal opens in the wrong home.
|
||||
let osUser: string | null = null;
|
||||
let osUserError: string | null = null;
|
||||
if (OS_USERS_ENABLED) {
|
||||
const result = await ensureOsUser({ email, username });
|
||||
if (result.ok) {
|
||||
osUser = result.osUser;
|
||||
await updateUser(user.id, { osUser: result.osUser });
|
||||
} else {
|
||||
osUserError = result.error;
|
||||
console.warn(`[users] created ${email} but could not create its Linux account: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.json({ user: { ...toPublicUser(user), osUser }, osUserError }, 201);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user