Fix account creation, remove rotation stuff for now

This commit is contained in:
Fang-Pen Lin
2025-12-05 12:07:34 -08:00
parent 5ac4e0a5bb
commit 2140956ba8
7 changed files with 136 additions and 155 deletions
@@ -0,0 +1,119 @@
import { Controller, FormProvider, useForm, useFormContext } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, ModalClose, TextArea } from "@app/components/v2";
import { KubernetesAuthMethod, TKubernetesAccount } from "@app/hooks/api/pam";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
import { rotateAccountFieldsSchema } from "./RotateAccountFields";
type Props = {
account?: TKubernetesAccount;
onSubmit: (formData: FormData) => Promise<void>;
};
const KubernetesServiceAccountTokenCredentialsSchema = z.object({
authMethod: z.literal(KubernetesAuthMethod.ServiceAccountToken),
serviceAccountToken: z.string().trim().min(1, "Service account token is required")
});
const formSchema = genericAccountFieldsSchema.extend(rotateAccountFieldsSchema.shape).extend({
credentials: KubernetesServiceAccountTokenCredentialsSchema
});
type FormData = z.infer<typeof formSchema>;
const KubernetesAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
const { control } = useFormContext<FormData>();
return (
<div className="mb-4 rounded-sm border border-mineshaft-600 bg-mineshaft-700/70 p-3">
<Controller
name="credentials.serviceAccountToken"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-0"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Service Account Token"
helperText="The bearer token for the service account"
>
<TextArea
{...field}
value={field.value === UNCHANGED_PASSWORD_SENTINEL ? "" : field.value || ""}
className="min-h-32 resize-y font-mono text-xs"
placeholder={
isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL
? "Token unchanged - click to update"
: "eyJhbGciOiJSUzI1NiIsImtpZCI6..."
}
/>
</FormControl>
)}
/>
</div>
);
};
export const KubernetesAccountForm = ({ account, onSubmit }: Props) => {
const isUpdate = Boolean(account);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: account
? {
...account,
credentials: {
...account.credentials,
serviceAccountToken: UNCHANGED_PASSWORD_SENTINEL
}
}
: {
name: "",
description: "",
credentials: {
authMethod: KubernetesAuthMethod.ServiceAccountToken,
serviceAccountToken: ""
},
rotationEnabled: false
}
});
const {
handleSubmit,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
handleSubmit(onSubmit)(e);
}}
>
<GenericAccountFields />
<KubernetesAccountFields isUpdate={isUpdate} />
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Account" : "Create Account"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -8,6 +8,7 @@ import {
import { DiscriminativePick } from "@app/types";
import { PamAccountHeader } from "../PamAccountHeader";
import { KubernetesAccountForm } from "./KubernetesAccountForm";
import { MySQLAccountForm } from "./MySQLAccountForm";
import { PostgresAccountForm } from "./PostgresAccountForm";
import { SshAccountForm } from "./SshAccountForm";
@@ -70,6 +71,14 @@ const CreateForm = ({
return (
<SshAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} />
);
case PamResourceType.Kubernetes:
return (
<KubernetesAccountForm
onSubmit={onSubmit}
resourceId={resourceId}
resourceType={resourceType}
/>
);
default:
throw new Error(`Unhandled resource: ${resourceType}`);
}
@@ -100,6 +109,8 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
return <MySQLAccountForm account={account as any} onSubmit={onSubmit} />;
case PamResourceType.SSH:
return <SshAccountForm account={account as any} onSubmit={onSubmit} />;
case PamResourceType.Kubernetes:
return <KubernetesAccountForm account={account as any} onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
}
@@ -7,7 +7,6 @@ import { KubernetesAuthMethod, PamResourceType, TKubernetesResource } from "@app
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
import { KubernetesResourceFields } from "./shared/KubernetesResourceFields";
import { KubernetesRotateAccountFields } from "./shared/KubernetesRotateAccountFields";
import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields";
type Props = {
@@ -24,7 +23,6 @@ const KubernetesConnectionDetailsSchema = z.object({
const KubernetesServiceAccountTokenCredentialsSchema = z.object({
authMethod: z.literal(KubernetesAuthMethod.ServiceAccountToken),
serviceAccountName: z.string().trim().max(255),
serviceAccountToken: z.string().trim().max(10000)
});
@@ -72,7 +70,6 @@ export const KubernetesResourceForm = ({ resource, onSubmit }: Props) => {
<form onSubmit={handleSubmit(onSubmit)}>
<GenericResourceFields />
<KubernetesResourceFields />
<KubernetesRotateAccountFields isUpdate={isUpdate} />
<div className="mt-6 flex items-center">
<Button
className="mr-4"
@@ -1,144 +0,0 @@
import { useEffect, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { FormControl, Input, Switch } from "@app/components/v2";
import { KubernetesAuthMethod } from "@app/hooks/api/pam";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
export const KubernetesRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
const { control, setValue, getValues } = useFormContext();
const [showToken, setShowToken] = useState(false);
const rotationServiceAccountName = useWatch({
control,
name: "rotationAccountCredentials.serviceAccountName"
});
const rotationServiceAccountToken = useWatch({
control,
name: "rotationAccountCredentials.serviceAccountToken"
});
const [enabled, setEnabled] = useState(false);
const [wasRotationTokenSentinelInitially, setWasRotationTokenSentinelInitially] = useState(false);
useEffect(() => {
const initialRotationToken = getValues("rotationAccountCredentials.serviceAccountToken");
if (initialRotationToken === UNCHANGED_PASSWORD_SENTINEL) {
setWasRotationTokenSentinelInitially(true);
}
}, [getValues]);
useEffect(() => {
if (rotationServiceAccountToken === UNCHANGED_PASSWORD_SENTINEL) {
setShowToken(false);
}
}, [rotationServiceAccountToken]);
useEffect(() => {
const isServiceAccountNamePopulated =
rotationServiceAccountName && rotationServiceAccountName !== "";
const isTokenPopulated =
rotationServiceAccountToken &&
rotationServiceAccountToken !== "" &&
rotationServiceAccountToken !== UNCHANGED_PASSWORD_SENTINEL;
if (isServiceAccountNamePopulated || isTokenPopulated) {
setEnabled(true);
}
}, [rotationServiceAccountName, rotationServiceAccountToken]);
return (
<div className="flex flex-col gap-2">
<Switch
id="account-rotation"
onCheckedChange={(value) => {
setEnabled(value);
if (value) {
setValue(
"rotationAccountCredentials.authMethod",
KubernetesAuthMethod.ServiceAccountToken,
{
shouldDirty: true
}
);
setValue("rotationAccountCredentials.serviceAccountName", "", {
shouldDirty: true
});
setValue("rotationAccountCredentials.serviceAccountToken", "", {
shouldDirty: true
});
} else {
setValue("rotationAccountCredentials", null, {
shouldDirty: true
});
}
}}
isChecked={enabled}
containerClassName="flex-row-reverse w-fit"
className="ml-0"
>
<p className="ml-2">Credential Rotation</p>
</Switch>
{enabled && (
<>
<div className="flex gap-2">
<Controller
name="rotationAccountCredentials.serviceAccountName"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-0 flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Service Account Name"
>
<div className="relative">
<Input {...field} autoComplete="off" />
</div>
</FormControl>
)}
/>
<Controller
name="rotationAccountCredentials.serviceAccountToken"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-0 flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Service Account Token"
>
<div className="relative">
<Input
{...field}
type={showToken ? "text" : "password"}
autoComplete="new-password"
onFocus={() => {
if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
field.onChange("");
}
setShowToken(true);
}}
onBlur={() => {
if (isUpdate && field.value === "" && wasRotationTokenSentinelInitially) {
field.onChange(UNCHANGED_PASSWORD_SENTINEL);
}
setShowToken(false);
}}
/>
</div>
</FormControl>
)}
/>
</div>
<p className="mb-2 text-xs text-mineshaft-400">
Credentials of the privileged service account which will be used for rotating other
accounts under this resource
</p>
</>
)}
</div>
);
};