user provisioning: the platform pushes the account list to plugins that declare it
a plugin that creates accounts in an upstream service — jellyfin first, immich next — needs to know who the platform's users are. it now gets told. DECLARED IN THE MANIFEST, not asserted at registration. `name` and `handles` arrive on an unauthenticated socket and are believed, so a capability claimed there would be a plugin granting itself one. the manifest was reviewed when the owner installed it, and sits beside `permissions` where "this plugin will be told who your users are" belongs — on the install screen, not discovered later. PUSHED, not requested. a sidecar has no authenticated way to ask, so a request could only be answered, never authorised. a push inverts that: the platform decides who gets it, and a caller registering under a name with no such manifest gets nothing. `listProvisionableUsers` is a hand-written projection — id, email, username, name, role — and NOT `select *`. `users` holds `password` and `os_user`, and `getUsers()` returns both, so a plugin reaching for the obvious function would get every account's password hash. naming the columns means adding a secret to the table cannot silently add it to what plugins receive. pinned by a test that asserts the key set, not just the absence of today's secrets. the owner is excluded. they bypass every permission check and already have whatever upstream account they want; provisioning them is at best a no-op and at worst a second admin nobody asked for. sent after the ack rather than inside it, so registration never waits on a database query — a slow postgres would otherwise stall every sidecar's startup, and none of them need the list to start. and re-sent on every account create and delete, because the registration copy is a snapshot: a plugin still holding a deleted user would keep an upstream login alive for someone the platform has removed. read once per broadcast, so two plugins cannot be told different things about the same moment.
This commit is contained in:
@@ -5,6 +5,7 @@ import argon2 from 'argon2';
|
||||
import * as errors from '@@/custom-errors';
|
||||
import { validatePublicKey } from '@@/os-user-ssh';
|
||||
import { provisionOsAccount } from './provision-os';
|
||||
import { broadcastProvisionableUsers } from '@@/plugins/user-provisioning';
|
||||
import { validatePassword } from '../auth/validate-password';
|
||||
import { validateUsername } from '../auth/validate-username';
|
||||
import { toPublicUser } from './manage-users';
|
||||
@@ -98,6 +99,10 @@ export const createUserHandler: Handler = async function (ctx) {
|
||||
// account was made does not cost anybody their password and dashboards.
|
||||
const os = await provisionOsAccount({ userId: user.id, email, username, role: role as UserRole, inboundKey });
|
||||
|
||||
// Tell any plugin that provisions upstream accounts. Detached and unawaited: a plugin's sidecar being
|
||||
// down must not fail the creation of a platform account, and the list is re-sent when it registers.
|
||||
void broadcastProvisionableUsers();
|
||||
|
||||
if (os.error) console.warn(`[users] created ${email} but its Linux side did not finish: ${os.error}`);
|
||||
|
||||
return ctx.json(
|
||||
|
||||
@@ -6,6 +6,7 @@ import { deprovisionOsAccount } from '@@/os-user-deprovision';
|
||||
import { dropPostgresRole, provisionPostgresRole, rolePermitsDevTools } from '@@/os-user-postgres';
|
||||
import { lookupOsUser } from '@@/os-user';
|
||||
import { DATA_PATH } from '@@/data-path';
|
||||
import { broadcastProvisionableUsers } from '@@/plugins/user-provisioning';
|
||||
|
||||
// Owner-only management of the other accounts. Everything here is gated by ownerGate in
|
||||
// users-router.ts; these handlers assume the caller is the Super Admin.
|
||||
@@ -242,5 +243,8 @@ export const deleteUserHandler: Handler = async function (ctx) {
|
||||
// Deleting a user cascades: passkeys, dashboards, screens, email accounts, playlists, everything keyed
|
||||
// to them. There is no undo, which is why the UI asks first.
|
||||
await deleteUser(id);
|
||||
// Same channel, same reason as creation — a plugin holding a list that still names a deleted account
|
||||
// would keep an upstream login alive for someone the platform has removed.
|
||||
void broadcastProvisionableUsers();
|
||||
return ctx.json({ ok: true });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user