mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 21:33:41 +00:00
8b6037b7ff
- ssh/ renamed to hosts/ (it covers SSH, RDP, VNC, Telnet, Docker, metrics) - serial/serial.ts and guacamole/ moved inside hosts/ - dashboard.ts and homepage.ts moved to services/ - swagger.ts moved to utils/ with adjusted scan globs Import paths and the generate:openapi script updated; ports and endpoints unchanged.
152 lines
4.3 KiB
TypeScript
152 lines
4.3 KiB
TypeScript
import type { Express } from "express";
|
|
import { execCommand } from "../widgets/common-utils.js";
|
|
import { execElevated } from "./exec-elevated.js";
|
|
import { managerHandler, ManagerInputError } from "./route-helpers.js";
|
|
import { isValidUsername, isValidGroupName } from "./validation.js";
|
|
import type { ManagerRoutesDeps } from "./types.js";
|
|
|
|
export interface SystemUser {
|
|
name: string;
|
|
uid: number;
|
|
gid: number;
|
|
home: string;
|
|
shell: string;
|
|
}
|
|
|
|
export interface SystemGroup {
|
|
name: string;
|
|
gid: number;
|
|
members: string[];
|
|
}
|
|
|
|
// Human users only (uid >= 1000, excluding nobody at 65534).
|
|
const READ_USERS_CMD = "getent passwd 2>/dev/null";
|
|
const READ_GROUPS_CMD = "getent group 2>/dev/null";
|
|
const READ_SUDOERS_CMD = "getent group sudo wheel 2>/dev/null";
|
|
|
|
export function parsePasswd(output: string): SystemUser[] {
|
|
const users: SystemUser[] = [];
|
|
for (const line of output.split("\n")) {
|
|
const parts = line.split(":");
|
|
if (parts.length < 7) continue;
|
|
const uid = Number(parts[2]);
|
|
if (!Number.isFinite(uid)) continue;
|
|
if (uid < 1000 || uid === 65534) continue;
|
|
users.push({
|
|
name: parts[0],
|
|
uid,
|
|
gid: Number(parts[3]),
|
|
home: parts[5],
|
|
shell: parts[6],
|
|
});
|
|
}
|
|
return users;
|
|
}
|
|
|
|
export function parseGroups(output: string): SystemGroup[] {
|
|
const groups: SystemGroup[] = [];
|
|
for (const line of output.split("\n")) {
|
|
const parts = line.split(":");
|
|
if (parts.length < 4) continue;
|
|
groups.push({
|
|
name: parts[0],
|
|
gid: Number(parts[2]),
|
|
members: parts[3].split(",").filter(Boolean),
|
|
});
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
export function parseSudoers(output: string): string[] {
|
|
const members = new Set<string>();
|
|
for (const line of output.split("\n")) {
|
|
const parts = line.split(":");
|
|
if (parts.length < 4) continue;
|
|
parts[3]
|
|
.split(",")
|
|
.filter(Boolean)
|
|
.forEach((m) => members.add(m));
|
|
}
|
|
return [...members];
|
|
}
|
|
|
|
export type UserAction = "create" | "delete" | "addToGroup" | "removeFromGroup";
|
|
|
|
export function registerUserRoutes(
|
|
app: Express,
|
|
{ validateHostId, runOnHost }: ManagerRoutesDeps,
|
|
): void {
|
|
app.get(
|
|
"/host-metrics/managers/users/:id",
|
|
validateHostId,
|
|
managerHandler(runOnHost, "read", "users_list", async (client) => {
|
|
const [passwd, groups, sudoers] = await Promise.all([
|
|
execCommand(client, READ_USERS_CMD, 15000),
|
|
execCommand(client, READ_GROUPS_CMD, 15000),
|
|
execCommand(client, READ_SUDOERS_CMD, 15000),
|
|
]);
|
|
return {
|
|
users: parsePasswd(passwd.stdout),
|
|
groups: parseGroups(groups.stdout),
|
|
sudoers: parseSudoers(sudoers.stdout),
|
|
};
|
|
}),
|
|
);
|
|
|
|
app.post(
|
|
"/host-metrics/managers/users/:id/action",
|
|
validateHostId,
|
|
managerHandler(
|
|
runOnHost,
|
|
"execute",
|
|
"users_action",
|
|
async (client, host, req) => {
|
|
const { action, username, group } = req.body as {
|
|
action?: UserAction;
|
|
username?: string;
|
|
group?: string;
|
|
};
|
|
if (!isValidUsername(username))
|
|
throw new ManagerInputError("Invalid username");
|
|
|
|
// Never modify/delete the user we're connected as, or root.
|
|
const who = (await execCommand(client, "id -un", 8000)).stdout.trim();
|
|
if (username === who || username === "root") {
|
|
throw new ManagerInputError(
|
|
"Refusing to modify the connected user or root",
|
|
);
|
|
}
|
|
|
|
let cmd: string;
|
|
switch (action) {
|
|
case "create":
|
|
cmd = `useradd -m ${username}`;
|
|
break;
|
|
case "delete":
|
|
cmd = `userdel -r ${username}`;
|
|
break;
|
|
case "addToGroup":
|
|
case "removeFromGroup":
|
|
if (!isValidGroupName(group))
|
|
throw new ManagerInputError("Invalid group");
|
|
cmd =
|
|
action === "addToGroup"
|
|
? `usermod -aG ${group} ${username}`
|
|
: `gpasswd -d ${username} ${group}`;
|
|
break;
|
|
default:
|
|
throw new ManagerInputError("Invalid action");
|
|
}
|
|
|
|
const result = await execElevated(client, cmd, host.sudoPassword, {
|
|
forceSudo: true,
|
|
});
|
|
return {
|
|
success: result.code === 0,
|
|
output: result.stdout || result.stderr,
|
|
};
|
|
},
|
|
),
|
|
);
|
|
}
|