feature secret rotations v2

This commit is contained in:
Scott Wilson
2025-03-31 20:34:48 -07:00
parent da094383b8
commit 766c1242fd
275 changed files with 9692 additions and 783 deletions
@@ -16,6 +16,8 @@ import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
@@ -31,7 +33,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
const { name: appName } = APP_CONNECTION_MAP[app];
const onSubmit = async (
formData: DiscriminativePick<TAppConnection, "method" | "name" | "app" | "credentials">
formData: DiscriminativePick<
TAppConnection,
"method" | "name" | "app" | "credentials" | "isPlatformManagedCredentials"
>
) => {
try {
const connection = await createAppConnection.mutateAsync(formData);
@@ -65,6 +70,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <DatabricksConnectionForm onSubmit={onSubmit} />;
case AppConnection.Humanitec:
return <HumanitecConnectionForm onSubmit={onSubmit} />;
case AppConnection.Postgres:
return <PostgresConnectionForm onSubmit={onSubmit} />;
case AppConnection.MsSql:
return <MsSqlConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -75,7 +84,10 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
const { name: appName } = APP_CONNECTION_MAP[appConnection.app];
const onSubmit = async (
formData: DiscriminativePick<TAppConnection, "method" | "name" | "app" | "credentials">
formData: DiscriminativePick<
TAppConnection,
"method" | "name" | "app" | "credentials" | "isPlatformManagedCredentials"
>
) => {
try {
const connection = await updateAppConnection.mutateAsync({
@@ -112,6 +124,10 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <DatabricksConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Humanitec:
return <HumanitecConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Postgres:
return <PostgresConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.MsSql:
return <MsSqlConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
@@ -32,7 +32,8 @@ export const GenericAppConnectionsFields = () => {
isOptional
>
<TextArea
className="h-20 !resize-none"
className="!resize-none"
rows={1}
placeholder="Connection description..."
{...register("description")}
/>
@@ -30,7 +30,7 @@ const rootSchema = genericAppConnectionFieldsSchema.extend({
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(HumanitecConnectionMethod.API_TOKEN),
method: z.literal(HumanitecConnectionMethod.ApiToken),
credentials: z.object({
apiToken: z.string().trim().min(1, "Service API Token required")
})
@@ -46,7 +46,7 @@ export const HumanitecConnectionForm = ({ appConnection, onSubmit }: Props) => {
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Humanitec,
method: HumanitecConnectionMethod.API_TOKEN
method: HumanitecConnectionMethod.ApiToken
}
});
@@ -0,0 +1,146 @@
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
MsSqlConnectionMethod,
TMsSqlConnection
} from "@app/hooks/api/appConnections/types/mssql-connection";
import { PlatformManagedConfirmationModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/shared/PlatformManagedConfirmationModal";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
import {
BaseSqlUsernameAndPasswordConnectionSchema,
PlatformManagedNoticeBanner,
SqlConnectionFields
} from "./shared";
type Props = {
appConnection?: TMsSqlConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.MsSql),
isPlatformManagedCredentials: z.boolean().optional()
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(MsSqlConnectionMethod.UsernameAndPassword),
credentials: BaseSqlUsernameAndPasswordConnectionSchema
})
]);
type FormData = z.infer<typeof formSchema>;
export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [showConfirmation, setShowConfirmation] = useState(false);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.MsSql,
method: MsSqlConnectionMethod.UsernameAndPassword,
credentials: {
host: "",
port: 1433,
database: "default",
username: "",
password: "",
sslCertificate: ""
}
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
const confirmSubmit = (formData: FormData) => {
if (formData.isPlatformManagedCredentials) {
setShowConfirmation(true);
return;
}
onSubmit(formData);
};
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(confirmSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Postgres].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(MsSqlConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<SqlConnectionFields isPlatformManagedCredentials={isPlatformManagedCredentials} />
{isPlatformManagedCredentials ? (
<PlatformManagedNoticeBanner />
) : (
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Database"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
)}
</form>
<PlatformManagedConfirmationModal
onConfirm={() => handleSubmit(onSubmit)()}
onOpenChange={setShowConfirmation}
isOpen={showConfirmation}
/>
</FormProvider>
);
};
@@ -0,0 +1,143 @@
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { PostgresConnectionMethod, TPostgresConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { PlatformManagedConfirmationModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/shared/PlatformManagedConfirmationModal";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
import {
BaseSqlUsernameAndPasswordConnectionSchema,
PlatformManagedNoticeBanner,
SqlConnectionFields
} from "./shared";
type Props = {
appConnection?: TPostgresConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Postgres),
isPlatformManagedCredentials: z.boolean().optional()
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(PostgresConnectionMethod.UsernameAndPassword),
credentials: BaseSqlUsernameAndPasswordConnectionSchema
})
]);
type FormData = z.infer<typeof formSchema>;
export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [showConfirmation, setShowConfirmation] = useState(false);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Postgres,
method: PostgresConnectionMethod.UsernameAndPassword,
credentials: {
host: "",
port: 5432,
database: "default",
username: "",
password: "",
sslCertificate: ""
}
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
const confirmSubmit = (formData: FormData) => {
if (formData.isPlatformManagedCredentials) {
setShowConfirmation(true);
return;
}
onSubmit(formData);
};
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(confirmSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Postgres].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(PostgresConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<SqlConnectionFields isPlatformManagedCredentials={isPlatformManagedCredentials} />
{isPlatformManagedCredentials ? (
<PlatformManagedNoticeBanner />
) : (
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Database"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
)}
</form>
<PlatformManagedConfirmationModal
onConfirm={() => handleSubmit(onSubmit)()}
onOpenChange={setShowConfirmation}
isOpen={showConfirmation}
/>
</FormProvider>
);
};
@@ -0,0 +1,43 @@
import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onConfirm: () => void;
};
export const PlatformManagedConfirmationModal = ({ isOpen, onOpenChange, onConfirm }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent className="max-w-2xl" title="Platform Managed Credentials">
<NoticeBannerV2 title="Are you sure you want have Infisical manage the credentials of this connection?">
<p className="my-1 text-sm text-mineshaft-300">
Once created, Infisical will update the password of this connection.
</p>
<p className="text-sm text-mineshaft-300">
You will not be able to access the updated password.
</p>
</NoticeBannerV2>
<div className="mt-4 flex gap-2">
<ModalClose asChild>
<Button
onClick={onConfirm}
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
>
Grant Infisical Ownership
</Button>
</ModalClose>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,9 @@
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
export const PlatformManagedNoticeBanner = () => (
<NoticeBannerV2 title="Platform Managed Credentials">
<p className="text-sm text-bunker-300">
This App Connection&#39;s credentials are managed by Infisical and cannot be updated.
</p>
</NoticeBannerV2>
);
@@ -0,0 +1,155 @@
import { Controller, useFormContext } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, Input, SecretInput, Switch, TextArea, Tooltip } from "@app/components/v2";
type Props = {
isPlatformManagedCredentials: boolean;
};
export const SqlConnectionFields = ({ isPlatformManagedCredentials }: Props) => {
const { control } = useFormContext();
return (
<>
<div className="flex items-start gap-2">
<Controller
name="credentials.host"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Host"
>
<Input {...field} isDisabled={isPlatformManagedCredentials} />
</FormControl>
)}
/>
<Controller
name="credentials.database"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Database Name"
>
<Input {...field} isDisabled={isPlatformManagedCredentials} />
</FormControl>
)}
/>
<Controller
name="credentials.port"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
className="w-28"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Port"
>
<Input type="number" {...field} isDisabled={isPlatformManagedCredentials} />
</FormControl>
)}
/>
</div>
<div className="flex items-start gap-2">
<Controller
name="credentials.username"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Username"
className="flex-1"
>
<Input {...field} isDisabled={isPlatformManagedCredentials} />
</FormControl>
)}
/>
<Controller
name="credentials.password"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Password"
className="flex-1"
>
<SecretInput
containerClassName="text-gray-400 w-full group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
isDisabled={isPlatformManagedCredentials}
/>
</FormControl>
)}
/>
</div>
<Controller
name="credentials.sslCertificate"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="SSL Certificate"
isOptional
>
<TextArea
className="!resize-none"
rows={1}
{...field}
isDisabled={isPlatformManagedCredentials}
/>
</FormControl>
)}
/>
{!isPlatformManagedCredentials && (
<Controller
name="isPlatformManagedCredentials"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="platform-managed"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
isDisabled={isPlatformManagedCredentials}
>
<p className="w-[13.6rem]">
Platform Managed Credentials
<Tooltip
className="max-w-md"
content={
<p>
If enabled, Infisical will manage the credentials of this App Connection by
updating the password on creation.
</p>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
)}
</>
);
};
@@ -0,0 +1,3 @@
export * from "./PlatformManagedNoticeBanner";
export * from "./sql-connection-schemas";
export * from "./SqlConnectionFields";
@@ -0,0 +1,10 @@
import { z } from "zod";
export const BaseSqlUsernameAndPasswordConnectionSchema = z.object({
host: z.string().trim().min(1, "Host required"),
port: z.coerce.number().default(5432),
database: z.string().trim().min(1, "Database required").default("default"),
username: z.string().trim().min(1, "Username required"),
password: z.string().trim().min(1, "Password required"),
sslCertificate: z.string().trim().optional()
});
@@ -6,6 +6,7 @@ import {
faEdit,
faEllipsisV,
faInfoCircle,
faServer,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -14,6 +15,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -42,7 +44,7 @@ export const AppConnectionRow = ({
onEditCredentials,
onEditDetails
}: Props) => {
const { id, name, method, app, description } = appConnection;
const { id, name, method, app, description, isPlatformManagedCredentials } = appConnection;
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -100,70 +102,82 @@ export const AppConnectionRow = ({
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={() => handleCopyId()}
>
Copy Connection ID
</DropdownMenuItem>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Edit}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onEditDetails(appConnection)}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Edit}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faAsterisk} />}
onClick={() => onEditCredentials(appConnection)}
>
Edit Credentials
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Delete}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(appConnection)}
>
Delete Connection
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
<div className="flex items-center justify-end gap-2">
{isPlatformManagedCredentials && (
<Tooltip side="left" content="This connection's credentials are managed by Infisical.">
<div>
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap">
<FontAwesomeIcon icon={faServer} />
<span>Platform Managed Credentials</span>
</Badge>
</div>
</Tooltip>
)}
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={() => handleCopyId()}
>
Copy Connection ID
</DropdownMenuItem>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Edit}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onEditDetails(appConnection)}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Edit}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faAsterisk} />}
onClick={() => onEditCredentials(appConnection)}
>
{isPlatformManagedCredentials ? "View" : "Edit"} Credentials
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Delete}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(appConnection)}
>
Delete Connection
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</div>
</Td>
</Tr>
);