diff --git a/messages/en-US.json b/messages/en-US.json index 8a81ce226..e057c686b 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2160,10 +2160,10 @@ "sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.", "sshSudo": "Allow sudo", "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", "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", "expectedResponseCodes": "Expected Response Codes", "expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.", diff --git a/src/components/CreateRoleForm.tsx b/src/components/CreateRoleForm.tsx index 080d5d293..678a9edb5 100644 --- a/src/components/CreateRoleForm.tsx +++ b/src/components/CreateRoleForm.tsx @@ -20,7 +20,12 @@ import type { CreateRoleBody, CreateRoleResponse } from "@server/routers/role"; import { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; 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"; type CreateRoleFormProps = { @@ -53,16 +58,10 @@ export default function CreateRoleForm({ payload.sshSudoCommands = values.sshSudoMode === "commands" && values.sshSudoCommands?.trim() - ? values.sshSudoCommands - .split(",") - .map((s) => s.trim()) - .filter(Boolean) + ? parseSudoCommands(values.sshSudoCommands) : []; if (values.sshUnixGroups?.trim()) { - payload.sshUnixGroups = values.sshUnixGroups - .split(",") - .map((s) => s.trim()) - .filter(Boolean); + payload.sshUnixGroups = parseUnixGroups(values.sshUnixGroups); } } const res = await api diff --git a/src/components/EditRoleForm.tsx b/src/components/EditRoleForm.tsx index 8fa674bd1..bebf50288 100644 --- a/src/components/EditRoleForm.tsx +++ b/src/components/EditRoleForm.tsx @@ -20,7 +20,12 @@ import type { UpdateRoleBody, UpdateRoleResponse } from "@server/routers/role"; import { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; 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"; type EditRoleFormProps = { @@ -56,16 +61,10 @@ export default function EditRoleForm({ payload.sshSudoCommands = values.sshSudoMode === "commands" && values.sshSudoCommands?.trim() - ? values.sshSudoCommands - .split(",") - .map((s) => s.trim()) - .filter(Boolean) + ? parseSudoCommands(values.sshSudoCommands) : []; if (values.sshUnixGroups !== undefined) { - payload.sshUnixGroups = values.sshUnixGroups - .split(",") - .map((s) => s.trim()) - .filter(Boolean); + payload.sshUnixGroups = parseUnixGroups(values.sshUnixGroups); } } const res = await api diff --git a/src/components/RoleForm.tsx b/src/components/RoleForm.tsx index 9b8d7034b..e1890af45 100644 --- a/src/components/RoleForm.tsx +++ b/src/components/RoleForm.tsx @@ -10,6 +10,7 @@ import { FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; +import { Textarea } from "@app/components/ui/textarea"; import { OptionSelect, type OptionSelectOption @@ -46,15 +47,34 @@ function toSshSudoMode(value: string | null | undefined): SshSudoMode { return "none"; } -function hasOnlyAbsoluteSudoCommands(value: string | undefined): boolean { - if (!value?.trim()) return true; +export function parseUnixGroups(value: string | undefined): string[] { + if (!value?.trim()) return []; - const commands = value - .split(",") - .map((command) => command.trim()) + return value + .split(/[,\s\n]+/) + .map((group) => group.trim()) .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]; return executable.startsWith("/"); }); @@ -125,10 +145,10 @@ export function RoleForm({ (role as Role & { allowSsh?: boolean }).allowSsh ?? false, sshSudoMode: toSshSudoMode(role.sshSudoMode), sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join( - ", " + "\n" ), sshCreateHomeDir: role.sshCreateHomeDir ?? false, - sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join(", ") + sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join("\n") } : { name: "", @@ -156,10 +176,10 @@ export function RoleForm({ (role as Role & { allowSsh?: boolean }).allowSsh ?? false, sshSudoMode: toSshSudoMode(role.sshSudoMode), sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join( - ", " + "\n" ), sshCreateHomeDir: role.sshCreateHomeDir ?? false, - sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join(", ") + sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join("\n") }); } }, [variant, role, form]); @@ -421,9 +441,10 @@ export function RoleForm({ {t("sshSudoCommands")} - @@ -446,9 +467,10 @@ export function RoleForm({ {t("sshUnixGroups")} -