diff --git a/src/components/RoleForm.tsx b/src/components/RoleForm.tsx
index 8fe2c55e9..2194344b7 100644
--- a/src/components/RoleForm.tsx
+++ b/src/components/RoleForm.tsx
@@ -46,6 +46,20 @@ function toSshSudoMode(value: string | null | undefined): SshSudoMode {
return "none";
}
+function hasOnlyAbsoluteSudoCommands(value: string | undefined): boolean {
+ if (!value?.trim()) return true;
+
+ const commands = value
+ .split(",")
+ .map((command) => command.trim())
+ .filter(Boolean);
+
+ return commands.every((command) => {
+ const executable = command.split(/\s+/)[0];
+ return executable.startsWith("/");
+ });
+}
+
export type RoleFormValues = {
name: string;
description?: string;
@@ -74,19 +88,33 @@ export function RoleForm({
const { isPaidUser } = usePaidStatus();
const { env } = useEnvContext();
- const formSchema = z.object({
- name: z
- .string({ message: t("nameRequired") })
- .min(1)
- .max(32),
- description: z.string().max(255).optional(),
- requireDeviceApproval: z.boolean().optional(),
- allowSsh: z.boolean().optional(),
- sshSudoMode: z.enum(SSH_SUDO_MODE_VALUES),
- sshSudoCommands: z.string().optional(),
- sshCreateHomeDir: z.boolean().optional(),
- sshUnixGroups: z.string().optional()
- });
+ const formSchema = z
+ .object({
+ name: z
+ .string({ message: t("nameRequired") })
+ .min(1)
+ .max(32),
+ description: z.string().max(255).optional(),
+ requireDeviceApproval: z.boolean().optional(),
+ allowSsh: z.boolean().optional(),
+ sshSudoMode: z.enum(SSH_SUDO_MODE_VALUES),
+ sshSudoCommands: z.string().optional(),
+ sshCreateHomeDir: z.boolean().optional(),
+ sshUnixGroups: z.string().optional()
+ })
+ .superRefine((values, ctx) => {
+ if (
+ values.sshSudoMode === "commands" &&
+ !hasOnlyAbsoluteSudoCommands(values.sshSudoCommands)
+ ) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["sshSudoCommands"],
+ message:
+ "Each sudo command must start with an absolute path (for example, /usr/bin/systemctl)."
+ });
+ }
+ });
const defaultValues: RoleFormValues = role
? {
@@ -296,7 +324,9 @@ export function RoleForm({
control={form.control}
name="allowSsh"
render={({ field }) => {
- const allowSshOptions: OptionSelectOption<"allow" | "disallow">[] = [
+ const allowSshOptions: OptionSelectOption<
+ "allow" | "disallow"
+ >[] = [
{
value: "allow",
label: t("roleAllowSshAllow")
@@ -311,7 +341,9 @@ export function RoleForm({
{t("roleAllowSsh")}
-
+
options={allowSshOptions}
value={
sshDisabled
@@ -322,7 +354,9 @@ export function RoleForm({
}
onChange={(v) => {
if (sshDisabled) return;
- field.onChange(v === "allow");
+ field.onChange(
+ v === "allow"
+ );
}}
cols={2}
disabled={sshDisabled}