improve unix group and sudo commands inputs

This commit is contained in:
miloschwartz
2026-06-08 14:36:10 -07:00
parent b81bfcfcee
commit 3c8fea382f
4 changed files with 52 additions and 32 deletions

View File

@@ -2160,10 +2160,10 @@
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.", "sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
"sshSudo": "Allow sudo", "sshSudo": "Allow sudo",
"sshSudoCommands": "Sudo Commands", "sshSudoCommands": "Sudo Commands",
"sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo. Absolute paths must be used.", "sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, separated by commas, spaces, or new lines. Absolute paths must be used.",
"sshCreateHomeDir": "Create Home Directory", "sshCreateHomeDir": "Create Home Directory",
"sshUnixGroups": "Unix Groups", "sshUnixGroups": "Unix Groups",
"sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.", "sshUnixGroupsDescription": "Unix groups to add the user to on the target host, separated by commas, spaces, or new lines.",
"retryAttempts": "Retry Attempts", "retryAttempts": "Retry Attempts",
"expectedResponseCodes": "Expected Response Codes", "expectedResponseCodes": "Expected Response Codes",
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.", "expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",

View File

@@ -20,7 +20,12 @@ import type { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useTransition } from "react"; import { useTransition } from "react";
import { RoleForm, type RoleFormValues } from "./RoleForm"; import {
parseSudoCommands,
parseUnixGroups,
RoleForm,
type RoleFormValues
} from "./RoleForm";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
type CreateRoleFormProps = { type CreateRoleFormProps = {
@@ -53,16 +58,10 @@ export default function CreateRoleForm({
payload.sshSudoCommands = payload.sshSudoCommands =
values.sshSudoMode === "commands" && values.sshSudoMode === "commands" &&
values.sshSudoCommands?.trim() values.sshSudoCommands?.trim()
? values.sshSudoCommands ? parseSudoCommands(values.sshSudoCommands)
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: []; : [];
if (values.sshUnixGroups?.trim()) { if (values.sshUnixGroups?.trim()) {
payload.sshUnixGroups = values.sshUnixGroups payload.sshUnixGroups = parseUnixGroups(values.sshUnixGroups);
.split(",")
.map((s) => s.trim())
.filter(Boolean);
} }
} }
const res = await api const res = await api

View File

@@ -20,7 +20,12 @@ import type { UpdateRoleBody, UpdateRoleResponse } from "@server/routers/role";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useTransition } from "react"; import { useTransition } from "react";
import { RoleForm, type RoleFormValues } from "./RoleForm"; import {
parseSudoCommands,
parseUnixGroups,
RoleForm,
type RoleFormValues
} from "./RoleForm";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
type EditRoleFormProps = { type EditRoleFormProps = {
@@ -56,16 +61,10 @@ export default function EditRoleForm({
payload.sshSudoCommands = payload.sshSudoCommands =
values.sshSudoMode === "commands" && values.sshSudoMode === "commands" &&
values.sshSudoCommands?.trim() values.sshSudoCommands?.trim()
? values.sshSudoCommands ? parseSudoCommands(values.sshSudoCommands)
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: []; : [];
if (values.sshUnixGroups !== undefined) { if (values.sshUnixGroups !== undefined) {
payload.sshUnixGroups = values.sshUnixGroups payload.sshUnixGroups = parseUnixGroups(values.sshUnixGroups);
.split(",")
.map((s) => s.trim())
.filter(Boolean);
} }
} }
const res = await api const res = await api

View File

@@ -10,6 +10,7 @@ import {
FormMessage FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { Textarea } from "@app/components/ui/textarea";
import { import {
OptionSelect, OptionSelect,
type OptionSelectOption type OptionSelectOption
@@ -46,15 +47,34 @@ function toSshSudoMode(value: string | null | undefined): SshSudoMode {
return "none"; return "none";
} }
function hasOnlyAbsoluteSudoCommands(value: string | undefined): boolean { export function parseUnixGroups(value: string | undefined): string[] {
if (!value?.trim()) return true; if (!value?.trim()) return [];
const commands = value return value
.split(",") .split(/[,\s\n]+/)
.map((command) => command.trim()) .map((group) => group.trim())
.filter(Boolean); .filter(Boolean);
}
return commands.every((command) => { export function parseSudoCommands(value: string | undefined): string[] {
if (!value?.trim()) return [];
const commands: string[] = [];
for (const segment of value.split(/[,\n]+/)) {
const trimmed = segment.trim();
if (!trimmed) continue;
for (const part of trimmed.split(/ (?=\/)/)) {
const command = part.trim();
if (command) commands.push(command);
}
}
return commands;
}
function hasOnlyAbsoluteSudoCommands(value: string | undefined): boolean {
return parseSudoCommands(value).every((command) => {
const executable = command.split(/\s+/)[0]; const executable = command.split(/\s+/)[0];
return executable.startsWith("/"); return executable.startsWith("/");
}); });
@@ -125,10 +145,10 @@ export function RoleForm({
(role as Role & { allowSsh?: boolean }).allowSsh ?? false, (role as Role & { allowSsh?: boolean }).allowSsh ?? false,
sshSudoMode: toSshSudoMode(role.sshSudoMode), sshSudoMode: toSshSudoMode(role.sshSudoMode),
sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join( sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join(
", " "\n"
), ),
sshCreateHomeDir: role.sshCreateHomeDir ?? false, sshCreateHomeDir: role.sshCreateHomeDir ?? false,
sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join(", ") sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join("\n")
} }
: { : {
name: "", name: "",
@@ -156,10 +176,10 @@ export function RoleForm({
(role as Role & { allowSsh?: boolean }).allowSsh ?? false, (role as Role & { allowSsh?: boolean }).allowSsh ?? false,
sshSudoMode: toSshSudoMode(role.sshSudoMode), sshSudoMode: toSshSudoMode(role.sshSudoMode),
sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join( sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join(
", " "\n"
), ),
sshCreateHomeDir: role.sshCreateHomeDir ?? false, sshCreateHomeDir: role.sshCreateHomeDir ?? false,
sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join(", ") sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join("\n")
}); });
} }
}, [variant, role, form]); }, [variant, role, form]);
@@ -421,9 +441,10 @@ export function RoleForm({
{t("sshSudoCommands")} {t("sshSudoCommands")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Textarea
{...field} {...field}
disabled={sshDisabled} disabled={sshDisabled}
className="h-20 min-h-20"
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
@@ -446,9 +467,10 @@ export function RoleForm({
{t("sshUnixGroups")} {t("sshUnixGroups")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Textarea
{...field} {...field}
disabled={sshDisabled} disabled={sshDisabled}
className="h-20 min-h-20"
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>