mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 06:35:21 +00:00
feature secret rotations v2
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms";
|
||||
import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader";
|
||||
import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { SecretRotation, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type SharedProps = {
|
||||
secretPath: string;
|
||||
environment?: string;
|
||||
environments?: WorkspaceEnv[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
} & SharedProps;
|
||||
|
||||
type ContentProps = {
|
||||
onComplete: (secretRotation: TSecretRotationV2) => void;
|
||||
selectedRotation: SecretRotation | null;
|
||||
setSelectedRotation: (selectedRotation: SecretRotation | null) => void;
|
||||
} & SharedProps;
|
||||
|
||||
const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentProps) => {
|
||||
if (selectedRotation) {
|
||||
return (
|
||||
<SecretRotationV2Form
|
||||
onCancel={() => setSelectedRotation(null)}
|
||||
type={selectedRotation}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <SecretRotationV2Select onSelect={setSelectedRotation} />;
|
||||
};
|
||||
|
||||
export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }: Props) => {
|
||||
const [selectedRotation, setSelectedRotation] = useState<SecretRotation | null>(null);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSelectedRotation(null);
|
||||
onOpenChange(open);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title={
|
||||
selectedRotation ? (
|
||||
<SecretRotationV2ModalHeader isConfigured={false} type={selectedRotation} />
|
||||
) : (
|
||||
"Add Secret Rotation"
|
||||
)
|
||||
}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
className={selectedRotation ? "max-w-2xl" : "max-w-3xl"}
|
||||
subTitle={
|
||||
selectedRotation ? undefined : "Select a provider to create a secret rotation for."
|
||||
}
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<Content
|
||||
onComplete={() => {
|
||||
setSelectedRotation(null);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
selectedRotation={selectedRotation}
|
||||
setSelectedRotation={setSelectedRotation}
|
||||
{...props}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal, Switch } from "@app/components/v2";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { useDeleteSecretRotationV2 } from "@app/hooks/api/secretRotationsV2/mutations";
|
||||
|
||||
type Props = {
|
||||
secretRotation?: TSecretRotationV2;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
onComplete?: () => void;
|
||||
};
|
||||
|
||||
export const DeleteSecretRotationV2Modal = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
secretRotation,
|
||||
onComplete
|
||||
}: Props) => {
|
||||
const deleteSecretRotation = useDeleteSecretRotationV2();
|
||||
const [revokeGeneratedCredentials, setRevokeGeneratedCredentials] = useState(false);
|
||||
const [deleteSecrets, setDeleteSecrets] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setRevokeGeneratedCredentials(false);
|
||||
setDeleteSecrets(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!secretRotation) return null;
|
||||
|
||||
const { id: rotationId, name, type, projectId, folder } = secretRotation;
|
||||
|
||||
const handleDeleteSecretRotation = async () => {
|
||||
const rotationType = SECRET_ROTATION_MAP[type].name;
|
||||
|
||||
try {
|
||||
await deleteSecretRotation.mutateAsync({
|
||||
rotationId,
|
||||
type,
|
||||
revokeGeneratedCredentials,
|
||||
deleteSecrets,
|
||||
projectId,
|
||||
secretPath: folder.path
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully deleted ${rotationType} Rotation`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (onComplete) onComplete();
|
||||
onOpenChange(false);
|
||||
} catch {
|
||||
createNotification({
|
||||
text: `Failed to delete ${rotationType} Rotation`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure want to delete ${name}?`}
|
||||
deleteKey={name}
|
||||
onDeleteApproved={handleDeleteSecretRotation}
|
||||
>
|
||||
<Switch
|
||||
containerClassName="mt-4"
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-red/50"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={revokeGeneratedCredentials}
|
||||
onCheckedChange={setRevokeGeneratedCredentials}
|
||||
id="revoke-credentials"
|
||||
>
|
||||
Revoke Credentials
|
||||
</Switch>
|
||||
<p className="mt-1 font-inter text-sm text-mineshaft-400">
|
||||
Generated credentials will {revokeGeneratedCredentials ? "" : "not"} be revoked on deletion
|
||||
{revokeGeneratedCredentials ? "" : " and remain active"}.
|
||||
</p>
|
||||
<Switch
|
||||
containerClassName="mt-4"
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-red/50"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={deleteSecrets}
|
||||
onCheckedChange={setDeleteSecrets}
|
||||
id="delete-secrets"
|
||||
>
|
||||
Delete Secrets
|
||||
</Switch>
|
||||
<p className="mt-1 font-inter text-sm text-mineshaft-400">
|
||||
Rotation secrets will {deleteSecrets ? "" : "not"} be removed from your project on deletion.
|
||||
</p>
|
||||
</DeleteActionModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { SecretRotationV2Form } from "./forms";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
secretRotation?: TSecretRotationV2;
|
||||
};
|
||||
|
||||
export const EditSecretRotationV2Modal = ({ secretRotation, onOpenChange, ...props }: Props) => {
|
||||
if (!secretRotation) return null;
|
||||
|
||||
return (
|
||||
<Modal {...props} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title={<SecretRotationV2ModalHeader isConfigured type={secretRotation.type} />}
|
||||
className="max-w-2xl"
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<SecretRotationV2Form
|
||||
onComplete={() => onOpenChange(false)}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
secretRotation={secretRotation}
|
||||
type={secretRotation.type}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { useRotateSecretRotationV2 } from "@app/hooks/api/secretRotationsV2/mutations";
|
||||
|
||||
type Props = {
|
||||
secretRotation?: TSecretRotationV2;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
secretRotation: TSecretRotationV2;
|
||||
onComplete: () => void;
|
||||
};
|
||||
|
||||
const Content = ({ secretRotation, onComplete }: ContentProps) => {
|
||||
const rotateSecrets = useRotateSecretRotationV2();
|
||||
|
||||
const { id: rotationId, type, projectId, folder } = secretRotation;
|
||||
const rotationType = SECRET_ROTATION_MAP[type].name;
|
||||
|
||||
const handleRotateSecrets = async () => {
|
||||
try {
|
||||
await rotateSecrets.mutateAsync({
|
||||
rotationId,
|
||||
type,
|
||||
projectId,
|
||||
secretPath: folder.path
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully rotated ${rotationType} secrets`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
onComplete();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
createNotification({
|
||||
text: `Failed to rotate ${rotationType} secrets`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-8 text-sm text-mineshaft-200">
|
||||
Are you sure you want to rotate the secrets for this {rotationType} Rotation?
|
||||
</p>
|
||||
<div className="mt-8 flex w-full items-center justify-between gap-2">
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
onClick={handleRotateSecrets}
|
||||
isDisabled={rotateSecrets.isPending}
|
||||
isLoading={rotateSecrets.isPending}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Rotate Secrets
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const RotateSecretRotationV2Modal = ({ isOpen, onOpenChange, secretRotation }: Props) => {
|
||||
if (!secretRotation) return null;
|
||||
|
||||
const rotationType = SECRET_ROTATION_MAP[secretRotation.type].name;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Rotate Secrets"
|
||||
subTitle={`Rotate the secrets for this ${rotationType}.`}
|
||||
>
|
||||
<Content secretRotation={secretRotation} onComplete={() => onOpenChange(false)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
type Props = {
|
||||
type: SecretRotation;
|
||||
isConfigured: boolean;
|
||||
};
|
||||
|
||||
export const SecretRotationV2ModalHeader = ({ type, isConfigured }: Props) => {
|
||||
const destinationDetails = SECRET_ROTATION_MAP[type];
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<img
|
||||
alt={`${destinationDetails.name} logo`}
|
||||
src={`/images/integrations/${destinationDetails.image}`}
|
||||
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
{destinationDetails.name} Rotation
|
||||
<a
|
||||
target="_blank"
|
||||
href={`https://infisical.com/docs/platform/secret-rotations/${type}`}
|
||||
className="mb-1 ml-1"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">
|
||||
{isConfigured
|
||||
? `Edit ${destinationDetails.name} Rotation`
|
||||
: `Rotate ${destinationDetails.name}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Spinner, Tooltip } from "@app/components/v2";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { SecretRotation, useSecretRotationV2Options } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
type Props = {
|
||||
onSelect: (type: SecretRotation) => void;
|
||||
};
|
||||
|
||||
export const SecretRotationV2Select = ({ onSelect }: Props) => {
|
||||
const { isPending, data: secretRotationOptions } = useSecretRotationV2Options();
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
<Spinner size="lg" className="text-mineshaft-500" />
|
||||
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{secretRotationOptions?.map(({ type }) => {
|
||||
const { image, name } = SECRET_ROTATION_MAP[type];
|
||||
|
||||
let size: number;
|
||||
|
||||
switch (type) {
|
||||
case SecretRotation.MsSqlCredentials:
|
||||
size = 50;
|
||||
break;
|
||||
default:
|
||||
size = 45;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={type}
|
||||
onClick={() => onSelect(type)}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
width={size}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{/* templates for kubecon, remove once implemented */}
|
||||
{[
|
||||
{
|
||||
name: "MySQL Credentials",
|
||||
image: "MySql.png"
|
||||
},
|
||||
{
|
||||
name: "SendGrid API Key",
|
||||
image: "SendGrid.png"
|
||||
},
|
||||
{
|
||||
name: "AWS IAM User Credentials",
|
||||
image: "Amazon Web Services.png"
|
||||
}
|
||||
].map(({ name, image }) => {
|
||||
let size: number;
|
||||
|
||||
switch (name) {
|
||||
case "MySQL Credentials":
|
||||
size = 80;
|
||||
break;
|
||||
case "SendGrid API Key":
|
||||
size = 50;
|
||||
break;
|
||||
default:
|
||||
size = 45;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={name}
|
||||
onClick={() =>
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: `${name} Rotation is under development. Please check back soon.`
|
||||
})
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
width={size}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more services.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
service you're looking for,`}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://infisical.com/slack"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
let us know on Slack
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://github.com/Infisical/infisical/discussions"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
make a request on GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4 hover:bg-mineshaft-900/50">
|
||||
<FontAwesomeIcon className="mt-auto text-3xl" icon={faWrench} />
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import { faBan, faRotate, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
type Props = {
|
||||
secretRotation: TSecretRotationV2;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props) => {
|
||||
const { isAutoRotationEnabled, rotationStatus, nextRotationAt, lastRotationMessage } =
|
||||
secretRotation;
|
||||
|
||||
if (rotationStatus === SecretRotationStatus.Failed) {
|
||||
let errorMessage = lastRotationMessage;
|
||||
if (lastRotationMessage) {
|
||||
try {
|
||||
errorMessage = JSON.stringify(JSON.parse(lastRotationMessage), null, 2);
|
||||
} catch {
|
||||
errorMessage = lastRotationMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
position="left"
|
||||
className="max-w-sm select-text"
|
||||
content={
|
||||
<div className="flex flex-col gap-2 whitespace-normal py-1">
|
||||
<div>
|
||||
<div className="mb-2 flex self-start text-red">
|
||||
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
|
||||
<div className="text-xs">Failure Reason</div>
|
||||
</div>
|
||||
<div className="break-words rounded bg-mineshaft-600 p-2 text-xs">{errorMessage}</div>
|
||||
</div>
|
||||
{nextRotationAt && (
|
||||
<span className="text-xs text-mineshaft-300">
|
||||
Next rotation attempt on {format(nextRotationAt, "MM/dd/yyyy")} at{" "}
|
||||
{format(nextRotationAt, "h:mm aa")}.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Badge
|
||||
variant="danger"
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
Rotation Failed
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAutoRotationEnabled) {
|
||||
return (
|
||||
<Badge
|
||||
className={twMerge(
|
||||
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
Auto-Rotation Disabled
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
const daysToRotation =
|
||||
(new Date(nextRotationAt).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24);
|
||||
|
||||
let variant: BadgeProps["variant"];
|
||||
let label: string;
|
||||
let tooltipContent: string;
|
||||
|
||||
if (daysToRotation >= 7) {
|
||||
variant = "success";
|
||||
label = `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`;
|
||||
tooltipContent = `Rotates ${format(nextRotationAt, "MM/dd/yyyy")} at ${format(nextRotationAt, "h:mm aa")}.`;
|
||||
} else if (daysToRotation < 0) {
|
||||
variant = "primary";
|
||||
label = "Rotating";
|
||||
tooltipContent = `Rotates on ${format(nextRotationAt, "MM/dd/yyyy")} at ${format(nextRotationAt, "h:mm aa")}.`;
|
||||
} else if (daysToRotation < 1) {
|
||||
variant = "primary";
|
||||
label = `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`;
|
||||
tooltipContent = `Rotates on ${format(nextRotationAt, "MM/dd/yyyy")} at ${format(nextRotationAt, "h:mm aa")}.`;
|
||||
} else {
|
||||
variant = "primary";
|
||||
label = `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`;
|
||||
tooltipContent = `Rotates on ${format(nextRotationAt, "MM/dd/yyyy")} at ${format(nextRotationAt, "h:mm aa")}.`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip className="max-w-lg" content={tooltipContent}>
|
||||
<div>
|
||||
<Badge
|
||||
variant={variant}
|
||||
className={twMerge(
|
||||
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
{label}
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import { ReactNode } from "react";
|
||||
import { faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { Modal, ModalContent, Spinner } from "@app/components/v2";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import {
|
||||
SecretRotation,
|
||||
TSecretRotationV2,
|
||||
useViewSecretRotationV2GeneratedCredentials
|
||||
} from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { ViewSqlRotationGeneratedCredentials } from "./shared";
|
||||
|
||||
type Props = {
|
||||
secretRotation?: TSecretRotationV2;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
secretRotation: TSecretRotationV2;
|
||||
};
|
||||
|
||||
const Content = ({ secretRotation }: ContentProps) => {
|
||||
const { id: rotationId, type, nextRotationAt } = secretRotation;
|
||||
|
||||
const { data: generatedCredentialsResponse, isPending } =
|
||||
useViewSecretRotationV2GeneratedCredentials({
|
||||
rotationId,
|
||||
type
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
<Spinner size="lg" className="text-mineshaft-500" />
|
||||
<p className="mt-4 text-sm text-mineshaft-400">Loading generated credentials...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let Component: ReactNode;
|
||||
switch (generatedCredentialsResponse!.type) {
|
||||
case SecretRotation.PostgresCredentials:
|
||||
case SecretRotation.MsSqlCredentials:
|
||||
Component = (
|
||||
<ViewSqlRotationGeneratedCredentials
|
||||
generatedCredentialsResponse={generatedCredentialsResponse!}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unhandled View Generated Credential Rotation Type");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{Component}
|
||||
{nextRotationAt && (
|
||||
<div className="flex items-center gap-x-1.5 text-sm text-mineshaft-300">
|
||||
<FontAwesomeIcon icon={faRotate} className="text-mineshaft-400" />
|
||||
<span>
|
||||
Next rotation occurs on: {format(nextRotationAt, "MM/dd/yyyy")} at{" "}
|
||||
{format(nextRotationAt, "h:mm aa")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewSecretRotationV2GeneratedCredentialsModal = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
secretRotation
|
||||
}: Props) => {
|
||||
if (!secretRotation) return null;
|
||||
|
||||
const rotationType = SECRET_ROTATION_MAP[secretRotation.type].name;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
onOpenAutoFocus={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
title="Generated Credentials"
|
||||
subTitle={`View the current and retired ${rotationType}.`}
|
||||
>
|
||||
<Content secretRotation={secretRotation} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./ViewSecretRotationV2GeneratedCredentials";
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { useReducer } from "react";
|
||||
import { faCheck, faCopy, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { GenericFieldLabel, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
children?: string;
|
||||
label: string;
|
||||
isSensitive?: boolean;
|
||||
};
|
||||
|
||||
export const CredentialDisplay = ({ children, label, isSensitive }: Props) => {
|
||||
const [showCredential, toggleShowCredential] = useReducer((prev) => !prev, !isSensitive);
|
||||
|
||||
const [, isCopyingCredential, setCopyCredential] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
return (
|
||||
<GenericFieldLabel label={label}>
|
||||
{children ? (
|
||||
<div className="flex w-full items-center gap-1 text-mineshaft-100">
|
||||
{showCredential ? children : "****************************"}
|
||||
<Tooltip content={`Copy ${label}`}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setCopyCredential(children);
|
||||
navigator.clipboard.writeText(children);
|
||||
}}
|
||||
ariaLabel="Copy credential"
|
||||
variant="plain"
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingCredential ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{isSensitive && (
|
||||
<Tooltip content={`Show ${label}`}>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
size="xs"
|
||||
onClick={toggleShowCredential}
|
||||
ariaLabel={`${showCredential ? "Hide" : "Show"} credential`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</GenericFieldLabel>
|
||||
);
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { faCheck, faClockRotateLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
|
||||
import { TViewSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
type Props = {
|
||||
generatedCredentialsResponse: TViewSecretRotationGeneratedCredentialsResponse;
|
||||
};
|
||||
|
||||
export const ViewSqlRotationGeneratedCredentials = ({
|
||||
generatedCredentialsResponse: { generatedCredentials, activeIndex }
|
||||
}: Props) => {
|
||||
const inactiveIndex = activeIndex === 0 ? 1 : 0;
|
||||
|
||||
const activeCredentials = generatedCredentials[activeIndex];
|
||||
const inactiveCredentials = generatedCredentials[inactiveIndex];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-1.5 text-green" />
|
||||
Current Credentials
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
The active credential set currently mapped to the rotation secrets.
|
||||
</p>
|
||||
<div className="flex flex-col gap-x-8 gap-y-2 rounded border border-mineshaft-600 bg-mineshaft-700 p-2">
|
||||
<CredentialDisplay label="Username">{activeCredentials?.username}</CredentialDisplay>
|
||||
<CredentialDisplay isSensitive label="Password">
|
||||
{activeCredentials?.password}
|
||||
</CredentialDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faClockRotateLeft} className="mr-1.5 text-yellow" />
|
||||
Retired Credentials
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
The retired credential set that will be revoked during the next rotation cycle.
|
||||
</p>
|
||||
<div className="flex flex-col gap-x-8 gap-y-2 rounded border border-mineshaft-600 bg-mineshaft-700 p-2">
|
||||
<CredentialDisplay label="Username">{inactiveCredentials?.username}</CredentialDisplay>
|
||||
<CredentialDisplay isSensitive label="Password">
|
||||
{inactiveCredentials?.password}
|
||||
</CredentialDisplay>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./ViewSqlRotationGeneratedCredentials";
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { format, setHours, setMinutes } from "date-fns";
|
||||
|
||||
import { FilterableSelect, FormControl, Input, Switch } from "@app/components/v2";
|
||||
import { getRotateAtLocal } from "@app/helpers/secretRotationsV2";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { TSecretRotationV2Form } from "./schemas";
|
||||
import { SecretRotationV2ConnectionField } from "./SecretRotationV2ConnectionField";
|
||||
|
||||
type Props = {
|
||||
isUpdate: boolean;
|
||||
environments?: WorkspaceEnv[];
|
||||
};
|
||||
|
||||
export const SecretRotationV2ConfigurationFields = ({ isUpdate, environments }: Props) => {
|
||||
const { control, watch } = useFormContext<TSecretRotationV2Form>();
|
||||
|
||||
console.log(watch("rotateAtUtc"));
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Configure the connection rotation strategy for this Secret Rotation.
|
||||
</p>
|
||||
{!isUpdate && environments && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl label="Environment" isError={Boolean(error)} errorText={error?.message}>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={environments}
|
||||
placeholder="Select an environment..."
|
||||
getOptionLabel={(option) => option?.name}
|
||||
getOptionValue={(option) => option?.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SecretRotationV2ConnectionField isUpdate={isUpdate} />
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Rotation Interval (In Days)"
|
||||
>
|
||||
<Input
|
||||
value={value}
|
||||
type="number"
|
||||
onChange={onChange}
|
||||
min={1}
|
||||
placeholder="my-secret-rotation"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="rotationInterval"
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Rotate At (Local Time)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
type="time"
|
||||
value={format(getRotateAtLocal(value), "HH:mm")}
|
||||
onChange={(e) => {
|
||||
const time = e.target.value;
|
||||
if (time) {
|
||||
const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10));
|
||||
const newSelectedDate = setHours(setMinutes(new Date(), minutes), hours);
|
||||
onChange({
|
||||
hours: newSelectedDate.getUTCHours(),
|
||||
minutes: newSelectedDate.getUTCMinutes()
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="bg-mineshaft-700 text-white [color-scheme:dark]"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
control={control}
|
||||
name="rotateAtUtc"
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="isAutoRotationEnabled"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
helperText={
|
||||
value
|
||||
? "Secrets will automatically be rotated when the rotation interval specified above as elapsed."
|
||||
: "Secrets will not be rotated automatically. You can still rotate secrets manually."
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="auto-rotation-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p className="w-[9.6rem]">Auto-Rotation {value ? "Enabled" : "Disabled"}</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
|
||||
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
||||
|
||||
import { TSecretRotationV2Form } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
onChange?: VoidFunction;
|
||||
isUpdate: boolean;
|
||||
};
|
||||
|
||||
export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }: Props) => {
|
||||
const { permission } = useOrgPermission();
|
||||
const { control, watch } = useFormContext<TSecretRotationV2Form>();
|
||||
|
||||
const rotationType = watch("type");
|
||||
const app = SECRET_ROTATION_CONNECTION_MAP[rotationType];
|
||||
|
||||
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
|
||||
|
||||
const connectionName = APP_CONNECTION_MAP[app].name;
|
||||
|
||||
const canCreateConnection = permission.can(
|
||||
OrgPermissionAppConnectionActions.Create,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
|
||||
const appName = APP_CONNECTION_MAP[app].name;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="App Connections can be created from the Organization Settings page."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label={`${connectionName} Connection`}
|
||||
helperText={isUpdate ? "Cannot be updated" : undefined}
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
if (callback) callback();
|
||||
}}
|
||||
isLoading={isPending}
|
||||
options={availableConnections}
|
||||
isDisabled={isUpdate}
|
||||
placeholder="Select connection..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="connection"
|
||||
/>
|
||||
{!isUpdate && availableConnections?.length === 0 && (
|
||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
|
||||
{canCreateConnection ? (
|
||||
<>
|
||||
You do not have access to any {appName} Connections. Create one from the{" "}
|
||||
<Link to="/organization/app-connections" className="underline">
|
||||
App Connections
|
||||
</Link>{" "}
|
||||
page.
|
||||
</>
|
||||
) : (
|
||||
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input, TextArea } from "@app/components/v2";
|
||||
|
||||
import { TSecretRotationV2Form } from "./schemas";
|
||||
|
||||
export const SecretRotationV2DetailsFields = () => {
|
||||
const { control } = useFormContext<TSecretRotationV2Form>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Provide a name and description for this Secret Rotation.
|
||||
</p>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
helperText="Must be slug-friendly"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Name"
|
||||
>
|
||||
<Input autoFocus value={value} onChange={onChange} placeholder="my-secret-rotation" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
label="Description"
|
||||
>
|
||||
<TextArea
|
||||
value={value ?? ""}
|
||||
onChange={onChange}
|
||||
placeholder="Describe the purpose of this rotation..."
|
||||
className="!resize-none"
|
||||
rows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="description"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,231 @@
|
||||
import { useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretRotationV2ConfigurationFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2ConfigurationFields";
|
||||
import { SecretRotationV2DetailsFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2DetailsFields";
|
||||
import { SecretRotationV2ParametersFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields";
|
||||
import { SecretRotationV2ReviewFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields";
|
||||
import { SecretRotationV2SecretsMappingFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import {
|
||||
SecretRotation,
|
||||
TSecretRotationV2,
|
||||
useSecretRotationV2Option
|
||||
} from "@app/hooks/api/secretRotationsV2";
|
||||
import {
|
||||
useCreateSecretRotationV2,
|
||||
useUpdateSecretRotationV2
|
||||
} from "@app/hooks/api/secretRotationsV2/mutations";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { SecretRotationV2FormSchema, TSecretRotationV2Form } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
onComplete: (secretRotation: TSecretRotationV2) => void;
|
||||
type: SecretRotation;
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
environment?: string;
|
||||
environments?: WorkspaceEnv[];
|
||||
secretRotation?: TSecretRotationV2;
|
||||
};
|
||||
|
||||
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretRotationV2Form)[] }[] = [
|
||||
{
|
||||
name: "Configuration",
|
||||
key: "configuration",
|
||||
fields: [
|
||||
"isAutoRotationEnabled",
|
||||
"environment",
|
||||
"rotationInterval",
|
||||
"connection",
|
||||
"rotateAtUtc"
|
||||
]
|
||||
},
|
||||
{ name: "Parameters", key: "parameters", fields: ["parameters"] },
|
||||
{ name: "Mappings", key: "secretsMapping", fields: ["secretsMapping"] },
|
||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||
{ name: "Review", key: "review", fields: [] }
|
||||
];
|
||||
|
||||
const DEFAULT_ROTATION_INTERVAL = 30;
|
||||
|
||||
export const SecretRotationV2Form = ({
|
||||
type,
|
||||
onComplete,
|
||||
onCancel,
|
||||
environment: envSlug,
|
||||
secretPath,
|
||||
secretRotation,
|
||||
environments
|
||||
}: Props) => {
|
||||
const createSecretRotation = useCreateSecretRotationV2();
|
||||
const updateSecretRotation = useUpdateSecretRotationV2();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { name: rotationType } = SECRET_ROTATION_MAP[type];
|
||||
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const { rotationOption } = useSecretRotationV2Option(type);
|
||||
|
||||
const formMethods = useForm<TSecretRotationV2Form>({
|
||||
resolver: zodResolver(SecretRotationV2FormSchema),
|
||||
defaultValues: secretRotation
|
||||
? {
|
||||
...secretRotation,
|
||||
environment: currentWorkspace?.environments.find((env) => env.slug === envSlug),
|
||||
secretPath
|
||||
}
|
||||
: {
|
||||
type,
|
||||
isAutoRotationEnabled: true,
|
||||
rotationInterval: DEFAULT_ROTATION_INTERVAL,
|
||||
rotateAtUtc: {
|
||||
hours: 0,
|
||||
minutes: 0
|
||||
},
|
||||
environment: currentWorkspace?.environments.find((env) => env.slug === envSlug),
|
||||
secretPath,
|
||||
...rotationOption!.template
|
||||
},
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({
|
||||
environment,
|
||||
connection,
|
||||
|
||||
...formData
|
||||
}: TSecretRotationV2Form) => {
|
||||
const mutation = secretRotation
|
||||
? updateSecretRotation.mutateAsync({
|
||||
rotationId: secretRotation.id,
|
||||
projectId: secretRotation.projectId,
|
||||
...formData
|
||||
})
|
||||
: createSecretRotation.mutateAsync({
|
||||
...formData,
|
||||
|
||||
connectionId: connection.id,
|
||||
environment: environment.slug,
|
||||
projectId: currentWorkspace.id
|
||||
});
|
||||
try {
|
||||
const rotation = await mutation;
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${secretRotation ? "updated" : "created"} ${rotationType} Rotation`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(rotation);
|
||||
} catch (err: any) {
|
||||
createNotification({
|
||||
title: `Failed to ${secretRotation ? "update" : "create"} ${rotationType} Rotation`,
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (selectedTabIndex === 0) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedTabIndex((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const { handleSubmit, trigger } = formMethods;
|
||||
|
||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||
|
||||
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
|
||||
|
||||
const handleNext = async () => {
|
||||
if (isFinalStep) {
|
||||
handleSubmit(onSubmit)();
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = await isStepValid(selectedTabIndex);
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
setSelectedTabIndex((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const isTabEnabled = async (index: number) => {
|
||||
let isEnabled = true;
|
||||
for (let i = index - 1; i >= 0; i -= 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
isEnabled = isEnabled && (await isStepValid(i));
|
||||
}
|
||||
|
||||
return isEnabled;
|
||||
};
|
||||
|
||||
return (
|
||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||
<FormProvider {...formMethods}>
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||
{FORM_TABS.map((tab, index) => (
|
||||
<Tab
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const isEnabled = await isTabEnabled(index);
|
||||
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
|
||||
}}
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
key={tab.key}
|
||||
>
|
||||
{index + 1}. {tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<SecretRotationV2ConfigurationFields
|
||||
isUpdate={Boolean(secretRotation)}
|
||||
environments={environments}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretRotationV2ParametersFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretRotationV2SecretsMappingFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretRotationV2DetailsFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretRotationV2ReviewFields />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</FormProvider>
|
||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||
<Button onClick={handleNext} colorSchema="secondary">
|
||||
{isFinalStep ? `${secretRotation ? "Update" : "Create"} Secret Rotation` : "Next"}
|
||||
</Button>
|
||||
<Button onClick={handlePrev} colorSchema="secondary">
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { TSecretRotationV2Form } from "../schemas";
|
||||
import { SqlRotationParametersFields } from "./shared";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlRotationParametersFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlRotationParametersFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2ParametersFields = () => {
|
||||
const { watch } = useFormContext<TSecretRotationV2Form>();
|
||||
|
||||
const rotationType = watch("type");
|
||||
|
||||
const Component = COMPONENT_MAP[rotationType];
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Configure the required parameters for this Secret Rotation.
|
||||
</p>
|
||||
<Component />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretRotationV2ParametersFields";
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
||||
import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const SqlRotationParametersFields = () => {
|
||||
const { control, watch } = useFormContext<
|
||||
TSecretRotationV2Form & {
|
||||
type: SecretRotation.PostgresCredentials; // all sql rotations share these fields
|
||||
}
|
||||
>();
|
||||
|
||||
const type = watch("type");
|
||||
|
||||
const { rotationOption } = useSecretRotationV2Option(type);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Database Username 1"
|
||||
>
|
||||
<Input value={value} onChange={onChange} placeholder="infiscal_user_1" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="parameters.username1"
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Database Username 2"
|
||||
>
|
||||
<Input value={value} onChange={onChange} placeholder="infiscal_user_2" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="parameters.username2"
|
||||
/>
|
||||
<NoticeBannerV2 title="Example Create User Statement">
|
||||
<p className="mb-3 text-sm text-mineshaft-300">
|
||||
Infisical requires two database users to be created for rotation. Below is an example
|
||||
statement for creating the required users. You may need to modify it to suit your needs.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
<pre className="whitespace-pre-wrap rounded border border-mineshaft-700 bg-mineshaft-800 p-2 text-mineshaft-300">
|
||||
{rotationOption!.template.createUserStatement}
|
||||
</pre>
|
||||
</p>
|
||||
</NoticeBannerV2>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SqlRotationParametersFields";
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
|
||||
import { SqlRotationReviewFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/shared";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { getRotateAtLocal } from "@app/helpers/secretRotationsV2";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlRotationReviewFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlRotationReviewFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2ReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretRotationV2Form>();
|
||||
|
||||
const {
|
||||
environment,
|
||||
secretPath,
|
||||
connection,
|
||||
type,
|
||||
name,
|
||||
description,
|
||||
rotationInterval,
|
||||
rotateAtUtc
|
||||
} = watch();
|
||||
|
||||
const Component = COMPONENT_MAP[type];
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Configuration</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Environment">{environment.name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Secret Path">{secretPath}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Rotation Interval">
|
||||
{rotationInterval} Day{rotationInterval > 1 ? "s" : ""}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Rotate At">
|
||||
{format(getRotateAtLocal(rotateAtUtc), "h:mm aa")}
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
<Component />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Details</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Description">{description}</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretRotationReviewFields";
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const SqlRotationReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretRotationV2Form & {
|
||||
type: SecretRotation.PostgresCredentials; // all sql rotations share these fields
|
||||
}
|
||||
>();
|
||||
|
||||
const [{ username1, username2 }, { username, password }] = watch([
|
||||
"parameters",
|
||||
"secretsMapping"
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Parameters</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Database Username 1">{username1}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Database Username 2">{username2}</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Secrets Mapping</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Username">{username}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Password">{password}</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SqlRotationReviewFields";
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { TSecretRotationV2Form } from "../schemas";
|
||||
import { SqlRotationSecretsMappingFields } from "./shared";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlRotationSecretsMappingFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlRotationSecretsMappingFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2SecretsMappingFields = () => {
|
||||
const { watch } = useFormContext<TSecretRotationV2Form>();
|
||||
|
||||
const rotationType = watch("type");
|
||||
|
||||
const Component = COMPONENT_MAP[rotationType];
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Map the rotated credentials to secrets in your Infisical project.
|
||||
</p>
|
||||
<Component />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretRotationV2SecretsMappingFields";
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faArrowRight, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
|
||||
import { Badge, FormControl, FormLabel, Input } from "@app/components/v2";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const SqlRotationSecretsMappingFields = () => {
|
||||
const { control } = useFormContext<
|
||||
TSecretRotationV2Form & {
|
||||
type: SecretRotation.PostgresCredentials; // all sql rotations share these fields
|
||||
}
|
||||
>();
|
||||
|
||||
const items = [
|
||||
{
|
||||
name: "Username",
|
||||
input: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input value={value} onChange={onChange} placeholder="DB_USERNAME" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretsMapping.username"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "Password",
|
||||
input: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input value={value} onChange={onChange} placeholder="DB_PASSWORD" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretsMapping.password"
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-hidden">
|
||||
<table className="w-full table-auto">
|
||||
<thead>
|
||||
<tr className="text-left">
|
||||
<th className="whitespace-nowrap">
|
||||
<FormLabel label="Rotated Credentials" />
|
||||
</th>
|
||||
<th />
|
||||
<th>
|
||||
<FormLabel
|
||||
tooltipClassName="max-w-sm"
|
||||
tooltipText="The name of the secret that the active credentials will be mapped to."
|
||||
label="Secret Name"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map(({ name, input }) => (
|
||||
<tr key={name}>
|
||||
<td className="whitespace-nowrap">
|
||||
<div className="mb-4 flex h-full items-start justify-center">
|
||||
<Badge className="pointer-events-none flex h-[36px] w-full items-center justify-center gap-1.5 whitespace-nowrap border border-mineshaft-600 bg-mineshaft-600 text-bunker-200">
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
<span>{name}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</td>
|
||||
<td className="whitespace-nowrap pl-5 pr-5">
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<FontAwesomeIcon className="text-mineshaft-400" icon={faArrowRight} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="w-full">{input}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SqlRotationSecretsMappingFields";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SecretRotationV2Form";
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
export const BaseSecretRotationSchema = z.object({
|
||||
name: slugSchema({ field: "Name" }),
|
||||
description: z.string().trim().max(256, "Cannot exceed 256 characters").nullish(),
|
||||
connection: z.object({ name: z.string(), id: z.string().uuid() }),
|
||||
environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }),
|
||||
secretPath: z.string().min(1, "Secret path required"),
|
||||
isAutoRotationEnabled: z.boolean(),
|
||||
rotationInterval: z.coerce.number(),
|
||||
rotateAtUtc: z.object({
|
||||
hours: z.number(),
|
||||
minutes: z.number()
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema";
|
||||
|
||||
const SecretRotationUnionSchema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationSchema
|
||||
]);
|
||||
|
||||
export const SecretRotationV2FormSchema = SecretRotationUnionSchema;
|
||||
|
||||
export type TSecretRotationV2Form = z.infer<typeof SecretRotationV2FormSchema>;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
|
||||
import { SqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/shared";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const MsSqlCredentialsRotationSchema = z
|
||||
.object({
|
||||
type: z.literal(SecretRotation.MsSqlCredentials)
|
||||
})
|
||||
.merge(SqlCredentialsRotationSchema)
|
||||
.merge(BaseSecretRotationSchema);
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
|
||||
import { SqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/shared";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const PostgresCredentialsRotationSchema = z
|
||||
.object({
|
||||
type: z.literal(SecretRotation.PostgresCredentials)
|
||||
})
|
||||
.merge(SqlCredentialsRotationSchema)
|
||||
.merge(BaseSecretRotationSchema);
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./sql-credentials-rotation-schema";
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretNameSchema } from "@app/lib/schemas";
|
||||
|
||||
export const SqlCredentialsRotationSchema = z.object({
|
||||
parameters: z.object({
|
||||
username1: z.string().trim().min(1, "Database Username 1 Required"),
|
||||
username2: z.string().trim().min(1, "Database Username 2 Required")
|
||||
}),
|
||||
secretsMapping: z.object({
|
||||
username: SecretNameSchema,
|
||||
password: SecretNameSchema
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./CreateSecretRotationV2Modal";
|
||||
+9
-9
@@ -2,7 +2,7 @@ import { useFormContext } from "react-hook-form";
|
||||
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
@@ -17,9 +17,9 @@ export const AwsParameterStoreSyncOptionsReviewFields = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||
{keyId && <GenericFieldLabel label="KMS Key">{keyId}</GenericFieldLabel>}
|
||||
{tags && tags.length > 0 && (
|
||||
<SecretSyncLabel label="Resource Tags">
|
||||
<GenericFieldLabel label="Resource Tags">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-1"
|
||||
@@ -49,12 +49,12 @@ export const AwsParameterStoreSyncOptionsReviewFields = () => {
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{syncSecretMetadataAsTags && (
|
||||
<SecretSyncLabel label="Sync Secret Metadata as Resource Tags">
|
||||
<GenericFieldLabel label="Sync Secret Metadata as Resource Tags">
|
||||
<Badge variant="success">Enabled</Badge>
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -71,13 +71,13 @@ export const AwsParameterStoreDestinationReviewFields = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncLabel label="Region">
|
||||
<GenericFieldLabel label="Region">
|
||||
{awsRegion?.name}
|
||||
<Badge className="ml-1" variant="success">
|
||||
{awsRegion?.slug}{" "}
|
||||
</Badge>
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Path">{path}</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Path">{path}</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+11
-11
@@ -2,7 +2,7 @@ import { useFormContext } from "react-hook-form";
|
||||
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
@@ -24,17 +24,17 @@ export const AwsSecretsManagerSyncReviewFields = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncLabel label="Region">
|
||||
<GenericFieldLabel label="Region">
|
||||
{awsRegion?.name}
|
||||
<Badge className="ml-1" variant="success">
|
||||
{awsRegion?.slug}{" "}
|
||||
</Badge>
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel className="capitalize" label="Mapping Behavior">
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel className="capitalize" label="Mapping Behavior">
|
||||
{mappingBehavior}
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && (
|
||||
<SecretSyncLabel label="Secret Name">{secretName}</SecretSyncLabel>
|
||||
<GenericFieldLabel label="Secret Name">{secretName}</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -49,9 +49,9 @@ export const AwsSecretsManagerSyncOptionsReviewFields = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||
{keyId && <GenericFieldLabel label="KMS Key">{keyId}</GenericFieldLabel>}
|
||||
{tags && tags.length > 0 && (
|
||||
<SecretSyncLabel label="Tags">
|
||||
<GenericFieldLabel label="Tags">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-1"
|
||||
@@ -81,12 +81,12 @@ export const AwsSecretsManagerSyncOptionsReviewFields = () => {
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{syncSecretMetadataAsTags && (
|
||||
<SecretSyncLabel label="Sync Secret Metadata as Resource Tags">
|
||||
<GenericFieldLabel label="Sync Secret Metadata as Resource Tags">
|
||||
<Badge variant="success">Enabled</Badge>
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
@@ -13,8 +13,8 @@ export const AzureAppConfigurationSyncReviewFields = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncLabel label="Configuration URL">{vaultBaseUrl}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Label">{label}</SecretSyncLabel>
|
||||
<GenericFieldLabel label="Configuration URL">{vaultBaseUrl}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Label">{label}</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
@@ -8,5 +8,5 @@ export const AzureKeyVaultSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.AzureKeyVault }>();
|
||||
const vaultBaseUrl = watch("destinationConfig.vaultBaseUrl");
|
||||
|
||||
return <SecretSyncLabel label="Vault URL">{vaultBaseUrl}</SecretSyncLabel>;
|
||||
return <GenericFieldLabel label="Vault URL">{vaultBaseUrl}</GenericFieldLabel>;
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
@@ -8,5 +8,5 @@ export const DatabricksSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Databricks }>();
|
||||
const scope = watch("destinationConfig.scope");
|
||||
|
||||
return <SecretSyncLabel label="Secret Scope">{scope}</SecretSyncLabel>;
|
||||
return <GenericFieldLabel label="Secret Scope">{scope}</GenericFieldLabel>;
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
@@ -10,5 +10,5 @@ export const GcpSyncReviewFields = () => {
|
||||
>();
|
||||
const projectId = watch("destinationConfig.projectId");
|
||||
|
||||
return <SecretSyncLabel label="Project ID">{projectId}</SecretSyncLabel>;
|
||||
return <GenericFieldLabel label="Project ID">{projectId}</GenericFieldLabel>;
|
||||
};
|
||||
|
||||
+12
-12
@@ -1,7 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { GitHubSyncScope, TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||
@@ -16,30 +16,30 @@ export const GitHubSyncReviewFields = () => {
|
||||
switch (config.scope) {
|
||||
case GitHubSyncScope.Repository:
|
||||
ScopeComponents = (
|
||||
<SecretSyncLabel label="Repository">
|
||||
<GenericFieldLabel label="Repository">
|
||||
{config.owner}/{config.repo}
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
);
|
||||
break;
|
||||
case GitHubSyncScope.Organization:
|
||||
ScopeComponents = (
|
||||
<>
|
||||
<SecretSyncLabel label="Organization">{config.org}</SecretSyncLabel>
|
||||
<SecretSyncLabel className="capitalize" label="Visibility">
|
||||
<GenericFieldLabel label="Organization">{config.org}</GenericFieldLabel>
|
||||
<GenericFieldLabel className="capitalize" label="Visibility">
|
||||
{config.visibility}
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case GitHubSyncScope.RepositoryEnvironment:
|
||||
ScopeComponents = (
|
||||
<>
|
||||
<SecretSyncLabel label="Repository">
|
||||
<GenericFieldLabel label="Repository">
|
||||
{config.owner}/{config.repo}
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel className="capitalize" label="Environment">
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel className="capitalize" label="Environment">
|
||||
{config.env}
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -52,9 +52,9 @@ export const GitHubSyncReviewFields = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncLabel className="capitalize" label="Scope">
|
||||
<GenericFieldLabel className="capitalize" label="Scope">
|
||||
{config.scope.replace("-", " ")}
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
{ScopeComponents}
|
||||
</>
|
||||
);
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
|
||||
@@ -14,10 +14,10 @@ export const HumanitecSyncReviewFields = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncLabel label="Organization">{orgId}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Application">{appId}</SecretSyncLabel>
|
||||
<GenericFieldLabel label="Organization">{orgId}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Application">{appId}</GenericFieldLabel>
|
||||
{scope === HumanitecSyncScope.Environment && (
|
||||
<SecretSyncLabel label="Environment">{envId}</SecretSyncLabel>
|
||||
<GenericFieldLabel label="Environment">{envId}</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
+12
-12
@@ -1,7 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
@@ -83,8 +83,8 @@ export const SecretSyncReviewFields = () => {
|
||||
<span className="text-sm text-mineshaft-300">Source</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<SecretSyncLabel label="Environment">{environment.name}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Secret Path">{secretPath}</SecretSyncLabel>
|
||||
<GenericFieldLabel label="Environment">{environment.name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Secret Path">{secretPath}</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -92,7 +92,7 @@ export const SecretSyncReviewFields = () => {
|
||||
<span className="text-sm text-mineshaft-300">Destination</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<SecretSyncLabel label="Connection">{connection.name}</SecretSyncLabel>
|
||||
<GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>
|
||||
{DestinationFieldsComponent}
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,21 +101,21 @@ export const SecretSyncReviewFields = () => {
|
||||
<span className="text-sm text-mineshaft-300">Sync Options</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<SecretSyncLabel label="Auto-Sync">
|
||||
<GenericFieldLabel label="Auto-Sync">
|
||||
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
|
||||
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Initial Sync Behavior">
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Initial Sync Behavior">
|
||||
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destinationName).name}
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||
{AdditionalSyncOptionsFieldsComponent}
|
||||
{disableSecretDeletion && (
|
||||
<SecretSyncLabel label="Secret Deletion">
|
||||
<GenericFieldLabel label="Secret Deletion">
|
||||
<Badge variant="primary">Disabled</Badge>
|
||||
</SecretSyncLabel>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,8 +124,8 @@ export const SecretSyncReviewFields = () => {
|
||||
<span className="text-sm text-mineshaft-300">Details</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<SecretSyncLabel label="Name">{name}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Description">{description}</SecretSyncLabel>
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Description">{description}</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export * from "../v2/GenericFieldLabel";
|
||||
export * from "./CreateSecretSyncModal";
|
||||
export * from "./DeleteSecretSyncModal";
|
||||
export * from "./EditSecretSyncModal";
|
||||
export * from "./SecretSyncImportSecretsModal";
|
||||
export * from "./SecretSyncImportStatusBadge";
|
||||
export * from "./SecretSyncLabel";
|
||||
export * from "./SecretSyncRemoveSecretsModal";
|
||||
export * from "./SecretSyncRemoveStatusBadge";
|
||||
export * from "./SecretSyncStatusBadge";
|
||||
|
||||
@@ -9,7 +9,7 @@ interface IProps {
|
||||
|
||||
export const Blur = ({ className, tooltipText }: IProps) => {
|
||||
return (
|
||||
<Tooltip content={tooltipText} isDisabled={!tooltipText}>
|
||||
<Tooltip content={tooltipText} className="max-w-md" isDisabled={!tooltipText}>
|
||||
<div
|
||||
className={twMerge("flex w-80 flex-grow items-center py-1 pl-4 pr-2", className)}
|
||||
tabIndex={0}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { ChangeEventHandler, useState } from "react";
|
||||
import { DayPicker, DayPickerProps } from "react-day-picker";
|
||||
import { DayPicker, DayPickerProps, getDefaultClassNames, UI } from "react-day-picker";
|
||||
import { faCalendar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverContentProps, PopoverProps } from "@radix-ui/react-popover";
|
||||
import { format, setHours, setMinutes } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../Popoverv2";
|
||||
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
|
||||
value?: Date;
|
||||
onChange: (date?: Date) => void;
|
||||
@@ -58,7 +61,30 @@ export const DatePicker = ({
|
||||
{value ? format(value, dateFormat) : "Pick a date and time"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit p-2" {...popUpContentProps}>
|
||||
<PopoverContent
|
||||
{...popUpContentProps}
|
||||
className={twMerge(
|
||||
"w-fit border border-mineshaft-600 bg-mineshaft-800 p-2 font-inter",
|
||||
popUpContentProps.className
|
||||
)}
|
||||
>
|
||||
<div className="px-2 pt-4">
|
||||
<DayPicker
|
||||
{...props}
|
||||
mode="single"
|
||||
selected={value}
|
||||
onSelect={handleDaySelect}
|
||||
className="font-inter text-mineshaft-200"
|
||||
classNames={{
|
||||
today: "text-primary border-primary",
|
||||
selected: " text-mineshaft-100 bg-mineshaft-500",
|
||||
root: `text-mineshaft-300 ${defaultClassNames}`,
|
||||
[UI.DayButton]: "p-3 rounded hover:text-mineshaft-100",
|
||||
[UI.Weekday]: "px-3 pt-3",
|
||||
[UI.Chevron]: "fill-mineshaft-300"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4 my-4">
|
||||
<Input
|
||||
type="time"
|
||||
@@ -67,17 +93,6 @@ export const DatePicker = ({
|
||||
className="bg-mineshaft-700 text-white [color-scheme:dark]"
|
||||
/>
|
||||
</div>
|
||||
<DayPicker
|
||||
{...props}
|
||||
mode="single"
|
||||
selected={value}
|
||||
onSelect={handleDaySelect}
|
||||
modifiersStyles={{
|
||||
selected: {
|
||||
background: "#cad62d"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ type Props = {
|
||||
labelClassName?: string;
|
||||
};
|
||||
|
||||
export const SecretSyncLabel = ({ label, children, className, labelClassName }: Props) => {
|
||||
export const GenericFieldLabel = ({ label, children, className, labelClassName }: Props) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<p className={twMerge("text-xs font-medium text-mineshaft-400", labelClassName)}>{label}</p>
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./GenericFieldLabel";
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ReactNode } from "react";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const NoticeBannerV2 = ({ title, children }: Props) => {
|
||||
return (
|
||||
<div className="flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
|
||||
<div className="mb-1 flex items-center text-sm">
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
|
||||
{title}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -16,12 +16,14 @@ export * from "./EmptyState";
|
||||
export * from "./FilterableSelect";
|
||||
export * from "./FontAwesomeSymbol";
|
||||
export * from "./FormControl";
|
||||
export * from "./GenericFieldLabel";
|
||||
export * from "./HoverCardv2";
|
||||
export * from "./IconButton";
|
||||
export * from "./Input";
|
||||
export * from "./Menu";
|
||||
export * from "./Modal";
|
||||
export * from "./NoticeBanner";
|
||||
export * from "./NoticeBannerV2";
|
||||
export * from "./PageHeader";
|
||||
export * from "./Pagination";
|
||||
export * from "./Popoverv2";
|
||||
|
||||
@@ -75,6 +75,15 @@ export enum ProjectPermissionGroupActions {
|
||||
GrantPrivileges = "grant-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretRotationActions {
|
||||
Read = "read",
|
||||
ReadGeneratedCredentials = "read-generated-credentials",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
RotateSecrets = "rotate-secrets"
|
||||
}
|
||||
|
||||
export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$ALL = "$all",
|
||||
@@ -169,6 +178,11 @@ export type SecretImportSubjectFields = {
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type SecretRotationSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionSecretActions,
|
||||
@@ -198,6 +212,13 @@ export type ProjectPermissionSet =
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SecretImportSubjectFields)
|
||||
)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionSecretRotationActions,
|
||||
(
|
||||
| ProjectPermissionSub.SecretRotation
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretRotation> & SecretRotationSubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||
@@ -210,7 +231,6 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
(
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faKey, faLock, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
AwsConnectionMethod,
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
AzureKeyVaultConnectionMethod,
|
||||
DatabricksConnectionMethod,
|
||||
GcpConnectionMethod,
|
||||
GitHubConnectionMethod,
|
||||
HumanitecConnectionMethod,
|
||||
MsSqlConnectionMethod,
|
||||
PostgresConnectionMethod,
|
||||
TAppConnection
|
||||
} from "@app/hooks/api/appConnections/types";
|
||||
import { DatabricksConnectionMethod } from "@app/hooks/api/appConnections/types/databricks-connection";
|
||||
import { HumanitecConnectionMethod } from "@app/hooks/api/appConnections/types/humanitec-connection";
|
||||
|
||||
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
|
||||
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
|
||||
@@ -26,7 +28,9 @@ export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: st
|
||||
image: "Microsoft Azure.png"
|
||||
},
|
||||
[AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" },
|
||||
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" }
|
||||
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" },
|
||||
[AppConnection.Postgres]: { name: "PostgreSQL", image: "Postgres.png" },
|
||||
[AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" }
|
||||
};
|
||||
|
||||
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||
@@ -45,8 +49,11 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
return { name: "Service Account Impersonation", icon: faUser };
|
||||
case DatabricksConnectionMethod.ServicePrincipal:
|
||||
return { name: "Service Principal", icon: faUser };
|
||||
case HumanitecConnectionMethod.API_TOKEN:
|
||||
case HumanitecConnectionMethod.ApiToken:
|
||||
return { name: "API Token", icon: faKey };
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
return { name: "Username & Password", icon: faLock };
|
||||
default:
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretRotation, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const SECRET_ROTATION_MAP: Record<SecretRotation, { name: string; image: string }> = {
|
||||
[SecretRotation.PostgresCredentials]: { name: "PostgreSQL Credentials", image: "Postgres.png" },
|
||||
[SecretRotation.MsSqlCredentials]: {
|
||||
name: "Microsoft SQL Server Credentials",
|
||||
image: "MsSql.png"
|
||||
}
|
||||
};
|
||||
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql
|
||||
};
|
||||
|
||||
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) =>
|
||||
new Date(
|
||||
Date.UTC(
|
||||
new Date().getUTCFullYear(),
|
||||
new Date().getUTCMonth(),
|
||||
new Date().getUTCDate(),
|
||||
hours,
|
||||
minutes,
|
||||
0,
|
||||
0
|
||||
)
|
||||
);
|
||||
@@ -5,5 +5,7 @@ export enum AppConnection {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
Databricks = "databricks",
|
||||
Humanitec = "humanitec"
|
||||
Humanitec = "humanitec",
|
||||
Postgres = "postgres",
|
||||
MsSql = "mssql"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
export type TAppConnectionOptionBase = {
|
||||
name: string;
|
||||
methods: string[];
|
||||
supportsPlatformManagement?: boolean;
|
||||
};
|
||||
|
||||
export type TAwsConnectionOption = TAppConnectionOptionBase & {
|
||||
@@ -38,6 +39,14 @@ export type THumanitecConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Humanitec;
|
||||
};
|
||||
|
||||
export type TPostgresConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Postgres;
|
||||
};
|
||||
|
||||
export type TMsSqlConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.MsSql;
|
||||
};
|
||||
|
||||
export type TAppConnectionOption =
|
||||
| TAwsConnectionOption
|
||||
| TGitHubConnectionOption
|
||||
@@ -45,7 +54,9 @@ export type TAppConnectionOption =
|
||||
| TAzureAppConfigurationConnectionOption
|
||||
| TAzureKeyVaultConnectionOption
|
||||
| TDatabricksConnectionOption
|
||||
| THumanitecConnectionOption;
|
||||
| THumanitecConnectionOption
|
||||
| TPostgresConnectionOption
|
||||
| TMsSqlConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
@@ -55,4 +66,6 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
|
||||
[AppConnection.Databricks]: TDatabricksConnectionOption;
|
||||
[AppConnection.Humanitec]: THumanitecConnectionOption;
|
||||
[AppConnection.Postgres]: TPostgresConnectionOption;
|
||||
[AppConnection.MsSql]: TMsSqlConnectionOption;
|
||||
};
|
||||
|
||||
@@ -2,11 +2,11 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum HumanitecConnectionMethod {
|
||||
API_TOKEN = "api-token"
|
||||
ApiToken = "api-token"
|
||||
}
|
||||
|
||||
export type THumanitecConnection = TRootAppConnection & { app: AppConnection.Humanitec } & {
|
||||
method: HumanitecConnectionMethod.API_TOKEN;
|
||||
method: HumanitecConnectionMethod.ApiToken;
|
||||
credentials: {
|
||||
apiToken: string;
|
||||
};
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-options";
|
||||
import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection";
|
||||
import { TDatabricksConnection } from "@app/hooks/api/appConnections/types/databricks-connection";
|
||||
import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
|
||||
import { THumanitecConnection } from "@app/hooks/api/appConnections/types/humanitec-connection";
|
||||
|
||||
import { AppConnection } from "../enums";
|
||||
import { TAppConnectionOption } from "./app-options";
|
||||
import { TAwsConnection } from "./aws-connection";
|
||||
import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection";
|
||||
import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
|
||||
import { TDatabricksConnection } from "./databricks-connection";
|
||||
import { TGcpConnection } from "./gcp-connection";
|
||||
import { TGitHubConnection } from "./github-connection";
|
||||
import { THumanitecConnection } from "./humanitec-connection";
|
||||
import { TMsSqlConnection } from "./mssql-connection";
|
||||
import { TPostgresConnection } from "./postgres-connection";
|
||||
|
||||
export * from "./aws-connection";
|
||||
export * from "./azure-app-configuration-connection";
|
||||
export * from "./azure-key-vault-connection";
|
||||
export * from "./databricks-connection";
|
||||
export * from "./gcp-connection";
|
||||
export * from "./github-connection";
|
||||
export * from "./humanitec-connection";
|
||||
export * from "./mssql-connection";
|
||||
export * from "./postgres-connection";
|
||||
|
||||
export type TAppConnection =
|
||||
| TAwsConnection
|
||||
@@ -23,7 +27,9 @@ export type TAppConnection =
|
||||
| TAzureKeyVaultConnection
|
||||
| TAzureAppConfigurationConnection
|
||||
| TDatabricksConnection
|
||||
| THumanitecConnection;
|
||||
| THumanitecConnection
|
||||
| TPostgresConnection
|
||||
| TMsSqlConnection;
|
||||
|
||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
|
||||
|
||||
@@ -35,11 +41,11 @@ export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppCo
|
||||
|
||||
export type TCreateAppConnectionDTO = Pick<
|
||||
TAppConnection,
|
||||
"name" | "credentials" | "method" | "app" | "description"
|
||||
"name" | "credentials" | "method" | "app" | "description" | "isPlatformManagedCredentials"
|
||||
>;
|
||||
|
||||
export type TUpdateAppConnectionDTO = Partial<
|
||||
Pick<TAppConnection, "name" | "credentials" | "description">
|
||||
Pick<TAppConnection, "name" | "credentials" | "description" | "isPlatformManagedCredentials">
|
||||
> & {
|
||||
connectionId: string;
|
||||
app: AppConnection;
|
||||
@@ -58,4 +64,6 @@ export type TAppConnectionMap = {
|
||||
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
|
||||
[AppConnection.Databricks]: TDatabricksConnection;
|
||||
[AppConnection.Humanitec]: THumanitecConnection;
|
||||
[AppConnection.Postgres]: TPostgresConnection;
|
||||
[AppConnection.MsSql]: TMsSqlConnection;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
import { TBaseSqlConnectionCredentials } from "./shared";
|
||||
|
||||
export enum MsSqlConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
||||
|
||||
export type TMsSqlConnection = TRootAppConnection & { app: AppConnection.MsSql } & {
|
||||
method: MsSqlConnectionMethod.UsernameAndPassword;
|
||||
credentials: TBaseSqlConnectionCredentials;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
import { TBaseSqlConnectionCredentials } from "./shared";
|
||||
|
||||
export enum PostgresConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
||||
|
||||
export type TPostgresConnection = TRootAppConnection & { app: AppConnection.Postgres } & {
|
||||
method: PostgresConnectionMethod.UsernameAndPassword;
|
||||
credentials: TBaseSqlConnectionCredentials;
|
||||
};
|
||||
@@ -6,4 +6,5 @@ export type TRootAppConnection = {
|
||||
orgId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isPlatformManagedCredentials?: boolean;
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./sql-connection";
|
||||
@@ -0,0 +1,7 @@
|
||||
export type TBaseSqlConnectionCredentials = {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
};
|
||||
@@ -110,7 +110,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.CREATE_APP_CONNECTION]: "Create App Connection",
|
||||
[EventType.UPDATE_APP_CONNECTION]: "Update App Connection",
|
||||
[EventType.DELETE_APP_CONNECTION]: "Delete App Connection",
|
||||
[EventType.GET_SECRET_SYNCS]: "List Secret Syncs",
|
||||
[EventType.GET_SECRET_SYNCS]: "List secret syncs",
|
||||
[EventType.GET_SECRET_SYNC]: "Get Secret Sync",
|
||||
[EventType.CREATE_SECRET_SYNC]: "Create Secret Sync",
|
||||
[EventType.UPDATE_SECRET_SYNC]: "Update Secret Sync",
|
||||
@@ -139,7 +139,15 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.KMIP_OPERATION_ACTIVATE]: "KMIP operation activate",
|
||||
[EventType.KMIP_OPERATION_REVOKE]: "KMIP operation revoke",
|
||||
[EventType.KMIP_OPERATION_LOCATE]: "KMIP operation locate",
|
||||
[EventType.KMIP_OPERATION_REGISTER]: "KMIP operation register"
|
||||
[EventType.KMIP_OPERATION_REGISTER]: "KMIP operation register",
|
||||
[EventType.GET_SECRET_ROTATIONS]: "List Secret Rotations",
|
||||
[EventType.GET_SECRET_ROTATION]: "Get Secret Rotation",
|
||||
[EventType.GET_SECRET_ROTATION_GENERATED_CREDENTIALS]:
|
||||
"Get Secret Rotation generated credentials",
|
||||
[EventType.CREATE_SECRET_ROTATION]: "Create Secret Rotation",
|
||||
[EventType.UPDATE_SECRET_ROTATION]: "Update Secret Rotation",
|
||||
[EventType.DELETE_SECRET_ROTATION]: "Delete Secret Rotation",
|
||||
[EventType.SECRET_ROTATION_ROTATE_SECRETS]: "Secret Rotation secrets rotated"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
||||
@@ -151,5 +151,12 @@ export enum EventType {
|
||||
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
||||
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
||||
KMIP_OPERATION_REGISTER = "kmip-operation-register",
|
||||
SECRET_APPROVAL_REQUEST_REVIEW = "secret-approval-request-review"
|
||||
SECRET_APPROVAL_REQUEST_REVIEW = "secret-approval-request-review",
|
||||
GET_SECRET_ROTATIONS = "get-secret-rotations",
|
||||
GET_SECRET_ROTATION = "get-secret-rotation",
|
||||
GET_SECRET_ROTATION_GENERATED_CREDENTIALS = "get-secret-rotation-generated-credentials",
|
||||
CREATE_SECRET_ROTATION = "create-secret-rotation",
|
||||
UPDATE_SECRET_ROTATION = "update-secret-rotation",
|
||||
DELETE_SECRET_ROTATION = "delete-secret-rotation",
|
||||
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets"
|
||||
}
|
||||
|
||||
@@ -132,6 +132,21 @@ export const fetchDashboardProjectSecretsByKeys = async ({
|
||||
return data;
|
||||
};
|
||||
|
||||
const mergePersonalRotationSecrets = (secrets: (SecretV3Raw | null)[]) => {
|
||||
const actualSecrets: SecretV3Raw[] = [];
|
||||
const dummySecrets: null[] = [];
|
||||
|
||||
secrets.forEach((secret) => {
|
||||
if (secret !== null) {
|
||||
actualSecrets.push(secret);
|
||||
} else {
|
||||
dummySecrets.push(secret);
|
||||
}
|
||||
});
|
||||
|
||||
return [...mergePersonalSecrets(actualSecrets), ...dummySecrets];
|
||||
};
|
||||
|
||||
export const useGetProjectSecretsOverview = (
|
||||
{
|
||||
projectId,
|
||||
@@ -145,6 +160,7 @@ export const useGetProjectSecretsOverview = (
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
includeSecretRotations,
|
||||
environments
|
||||
}: TGetDashboardProjectSecretsOverviewDTO,
|
||||
options?: Omit<
|
||||
@@ -173,6 +189,7 @@ export const useGetProjectSecretsOverview = (
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
includeSecretRotations,
|
||||
environments
|
||||
}),
|
||||
queryFn: () =>
|
||||
@@ -188,10 +205,11 @@ export const useGetProjectSecretsOverview = (
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
includeSecretRotations,
|
||||
environments
|
||||
}),
|
||||
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsOverview>>) => {
|
||||
const { secrets, ...select } = data;
|
||||
const { secrets, secretRotations, ...select } = data;
|
||||
const uniqueSecrets = secrets ? unique(secrets, (i) => i.secretKey) : [];
|
||||
|
||||
const uniqueFolders = select.folders ? unique(select.folders, (i) => i.name) : [];
|
||||
@@ -201,14 +219,22 @@ export const useGetProjectSecretsOverview = (
|
||||
: [];
|
||||
|
||||
const uniqueSecretImports = select.imports ? unique(select.imports, (i) => i.id) : [];
|
||||
const uniqueSecretRotations = secretRotations ? unique(secretRotations, (i) => i.name) : [];
|
||||
|
||||
return {
|
||||
...select,
|
||||
secrets: secrets ? mergePersonalSecrets(secrets) : undefined,
|
||||
secretRotations: secretRotations?.map((rotation) => {
|
||||
return {
|
||||
...rotation,
|
||||
secrets: mergePersonalRotationSecrets(rotation.secrets)
|
||||
};
|
||||
}),
|
||||
totalUniqueSecretsInPage: uniqueSecrets.length,
|
||||
totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length,
|
||||
totalUniqueFoldersInPage: uniqueFolders.length,
|
||||
totalUniqueSecretImportsInPage: uniqueSecretImports.length
|
||||
totalUniqueSecretImportsInPage: uniqueSecretImports.length,
|
||||
totalUniqueSecretRotationsInPage: uniqueSecretRotations.length
|
||||
};
|
||||
}, []),
|
||||
placeholderData: (previousData) => previousData
|
||||
@@ -230,6 +256,7 @@ export const useGetProjectSecretsDetails = (
|
||||
viewSecretValue,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
includeSecretRotations,
|
||||
tags
|
||||
}: TGetDashboardProjectSecretsDetailsDTO,
|
||||
options?: Omit<
|
||||
@@ -260,6 +287,7 @@ export const useGetProjectSecretsDetails = (
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
includeSecretRotations,
|
||||
tags
|
||||
}),
|
||||
queryFn: () =>
|
||||
@@ -277,12 +305,17 @@ export const useGetProjectSecretsDetails = (
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
includeSecretRotations,
|
||||
tags
|
||||
}),
|
||||
select: useCallback(
|
||||
(data: Awaited<ReturnType<typeof fetchProjectSecretsDetails>>) => ({
|
||||
...data,
|
||||
secrets: data.secrets ? mergePersonalSecrets(data.secrets) : undefined
|
||||
secrets: data.secrets ? mergePersonalSecrets(data.secrets) : undefined,
|
||||
secretRotations: data.secretRotations?.map((rotation) => ({
|
||||
...rotation,
|
||||
secrets: mergePersonalRotationSecrets(rotation.secrets)
|
||||
}))
|
||||
}),
|
||||
[]
|
||||
),
|
||||
@@ -371,7 +404,7 @@ export const useGetProjectSecretsQuickSearch = (
|
||||
tags
|
||||
}),
|
||||
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsQuickSearch>>) => {
|
||||
const { secrets, folders, dynamicSecrets } = data;
|
||||
const { secrets, folders, dynamicSecrets, secretRotations } = data;
|
||||
|
||||
const groupedFolders = groupBy(folders, (folder) => folder.path);
|
||||
const groupedSecrets = groupBy(
|
||||
@@ -383,11 +416,16 @@ export const useGetProjectSecretsQuickSearch = (
|
||||
(dynamicSecret) =>
|
||||
`${dynamicSecret.path === "/" ? "" : dynamicSecret.path}/${dynamicSecret.name}`
|
||||
);
|
||||
const groupedRotations = groupBy(
|
||||
secretRotations,
|
||||
(rotation) => `${rotation.folder.path === "/" ? "" : rotation.folder.path}/${rotation.name}`
|
||||
);
|
||||
|
||||
return {
|
||||
folders: groupedFolders,
|
||||
secrets: groupedSecrets,
|
||||
dynamicSecrets: groupedDynamicSecrets
|
||||
dynamicSecrets: groupedDynamicSecrets,
|
||||
secretRotations: groupedRotations
|
||||
};
|
||||
}, []),
|
||||
placeholderData: (previousData) => previousData
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||
import { TSecretImport } from "@app/hooks/api/secretImports/types";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { SecretV3Raw, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
|
||||
export type DashboardProjectSecretsOverviewResponse = {
|
||||
@@ -14,11 +15,16 @@ export type DashboardProjectSecretsOverviewResponse = {
|
||||
totalFolderCount?: number;
|
||||
totalDynamicSecretCount?: number;
|
||||
totalImportCount?: number;
|
||||
secretRotations?: (TSecretRotationV2 & {
|
||||
secrets: (SecretV3Raw | null)[];
|
||||
})[];
|
||||
totalSecretRotationCount?: number;
|
||||
totalCount: number;
|
||||
totalUniqueSecretsInPage: number;
|
||||
totalUniqueDynamicSecretsInPage: number;
|
||||
totalUniqueFoldersInPage: number;
|
||||
totalUniqueSecretImportsInPage: number;
|
||||
totalUniqueSecretRotationsInPage: number;
|
||||
};
|
||||
|
||||
export type DashboardProjectSecretsDetailsResponse = {
|
||||
@@ -26,10 +32,14 @@ export type DashboardProjectSecretsDetailsResponse = {
|
||||
folders?: TSecretFolder[];
|
||||
dynamicSecrets?: TDynamicSecret[];
|
||||
secrets?: SecretV3Raw[];
|
||||
secretRotations?: (TSecretRotationV2 & {
|
||||
secrets: (SecretV3Raw | null)[];
|
||||
})[];
|
||||
totalImportCount?: number;
|
||||
totalFolderCount?: number;
|
||||
totalDynamicSecretCount?: number;
|
||||
totalSecretCount?: number;
|
||||
totalSecretRotationCount?: number;
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
@@ -39,9 +49,12 @@ export type DashboardProjectSecretsByKeys = {
|
||||
|
||||
export type DashboardProjectSecretsOverview = Omit<
|
||||
DashboardProjectSecretsOverviewResponse,
|
||||
"secrets"
|
||||
"secrets" | "secretRotations"
|
||||
> & {
|
||||
secrets?: SecretV3RawSanitized[];
|
||||
secretRotations?: (TSecretRotationV2 & {
|
||||
secrets: (SecretV3RawSanitized | null)[];
|
||||
})[];
|
||||
};
|
||||
|
||||
export type DashboardProjectSecretsDetails = Omit<
|
||||
@@ -49,6 +62,9 @@ export type DashboardProjectSecretsDetails = Omit<
|
||||
"secrets"
|
||||
> & {
|
||||
secrets?: SecretV3RawSanitized[];
|
||||
secretRotations?: (TSecretRotationV2 & {
|
||||
secrets: (SecretV3RawSanitized | null)[];
|
||||
})[];
|
||||
};
|
||||
|
||||
export enum DashboardSecretsOrderBy {
|
||||
@@ -67,6 +83,7 @@ export type TGetDashboardProjectSecretsOverviewDTO = {
|
||||
includeFolders?: boolean;
|
||||
includeDynamicSecrets?: boolean;
|
||||
includeImports?: boolean;
|
||||
includeSecretRotations?: boolean;
|
||||
environments: string[];
|
||||
};
|
||||
|
||||
@@ -83,6 +100,7 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
|
||||
export type TDashboardProjectSecretsQuickSearchResponse = {
|
||||
folders: (TSecretFolder & { envId: string; path: string })[];
|
||||
dynamicSecrets: (TDynamicSecret & { environment: string; path: string })[];
|
||||
secretRotations: TSecretRotationV2[];
|
||||
secrets: SecretV3Raw[];
|
||||
};
|
||||
|
||||
@@ -90,6 +108,7 @@ export type TDashboardProjectSecretsQuickSearch = {
|
||||
folders: Record<string, TDashboardProjectSecretsQuickSearchResponse["folders"]>;
|
||||
secrets: Record<string, SecretV3RawSanitized[]>;
|
||||
dynamicSecrets: Record<string, TDashboardProjectSecretsQuickSearchResponse["dynamicSecrets"]>;
|
||||
secretRotations: Record<string, TDashboardProjectSecretsQuickSearchResponse["secretRotations"]>;
|
||||
};
|
||||
|
||||
export type TGetDashboardProjectSecretsQuickSearchDTO = {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-credentials"
|
||||
}
|
||||
|
||||
export enum SecretRotationStatus {
|
||||
Success = "success",
|
||||
Failed = "failed"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./enums";
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import {
|
||||
TCreateSecretRotationV2DTO,
|
||||
TDeleteSecretRotationV2DTO,
|
||||
TRotateSecretRotationV2DTO,
|
||||
TSecretRotationV2Response,
|
||||
TUpdateSecretRotationV2DTO
|
||||
} from "@app/hooks/api/secretRotationsV2/types";
|
||||
|
||||
export const useCreateSecretRotationV2 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ type, ...params }: TCreateSecretRotationV2DTO) => {
|
||||
const { data } = await apiRequest.post<TSecretRotationV2Response>(
|
||||
`/api/v2/secret-rotations/${type}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.secretRotation;
|
||||
},
|
||||
onSuccess: (_, { projectId, secretPath }) =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath })
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSecretRotationV2 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ type, rotationId, ...params }: TUpdateSecretRotationV2DTO) => {
|
||||
const { data } = await apiRequest.patch<TSecretRotationV2Response>(
|
||||
`/api/v2/secret-rotations/${type}/${rotationId}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.secretRotation;
|
||||
},
|
||||
onSuccess: (_, { projectId, secretPath }) =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath })
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export const useRotateSecretRotationV2 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ type, rotationId }: TRotateSecretRotationV2DTO) => {
|
||||
const { data } = await apiRequest.post<TSecretRotationV2Response>(
|
||||
`/api/v2/secret-rotations/${type}/${rotationId}/rotate-secrets`
|
||||
);
|
||||
|
||||
return data.secretRotation;
|
||||
},
|
||||
onSuccess: (_, { projectId, secretPath }) =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath })
|
||||
}),
|
||||
onError: (_, { projectId, secretPath }) =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath })
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSecretRotationV2 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
type,
|
||||
rotationId,
|
||||
deleteSecrets,
|
||||
revokeGeneratedCredentials
|
||||
}: TDeleteSecretRotationV2DTO) => {
|
||||
const { data } = await apiRequest.delete<TSecretRotationV2Response>(
|
||||
`/api/v2/secret-rotations/${type}/${rotationId}`,
|
||||
{ params: { deleteSecrets, revokeGeneratedCredentials } }
|
||||
);
|
||||
|
||||
return data.secretRotation;
|
||||
},
|
||||
onSuccess: (_, { projectId, secretPath }) =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath })
|
||||
})
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2/enums";
|
||||
import {
|
||||
TListSecretRotationV2Options,
|
||||
TSecretRotationV2Option,
|
||||
TViewSecretRotationGeneratedCredentialsResponse,
|
||||
TViewSecretRotationV2GeneratedCredentialsDTO
|
||||
} from "@app/hooks/api/secretRotationsV2/types";
|
||||
|
||||
export const secretRotationV2Keys = {
|
||||
all: ["secret-rotations-v2"] as const,
|
||||
options: () => [...secretRotationV2Keys.all, "options"] as const,
|
||||
viewGeneratedCredentials: ({ type, rotationId }: TViewSecretRotationV2GeneratedCredentialsDTO) =>
|
||||
[...secretRotationV2Keys.all, type, rotationId] as const
|
||||
};
|
||||
|
||||
export const useSecretRotationV2Options = (
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSecretRotationV2Option[],
|
||||
unknown,
|
||||
TSecretRotationV2Option[],
|
||||
ReturnType<typeof secretRotationV2Keys.options>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretRotationV2Keys.options(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TListSecretRotationV2Options>(
|
||||
"/api/v2/secret-rotations/options"
|
||||
);
|
||||
|
||||
return data.secretRotationOptions;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useSecretRotationV2Option = (type: SecretRotation) => {
|
||||
const { data: rotationOptions, isPending } = useSecretRotationV2Options();
|
||||
const rotationOption = rotationOptions?.find((option) => option.type === type);
|
||||
|
||||
return { rotationOption, isPending };
|
||||
};
|
||||
|
||||
export const useViewSecretRotationV2GeneratedCredentials = (
|
||||
{ rotationId, type }: TViewSecretRotationV2GeneratedCredentialsDTO,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TViewSecretRotationGeneratedCredentialsResponse,
|
||||
unknown,
|
||||
TViewSecretRotationGeneratedCredentialsResponse,
|
||||
ReturnType<typeof secretRotationV2Keys.viewGeneratedCredentials>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretRotationV2Keys.viewGeneratedCredentials({ rotationId, type }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TViewSecretRotationGeneratedCredentialsResponse>(
|
||||
`/api/v2/secret-rotations/${type}/${rotationId}/generated-credentials`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import {
|
||||
TMsSqlCredentialsRotation,
|
||||
TMsSqlCredentialsRotationGeneratedCredentialsResponse
|
||||
} from "@app/hooks/api/secretRotationsV2/types/mssql-credentials-rotation";
|
||||
import {
|
||||
TPostgresCredentialsRotation,
|
||||
TPostgresCredentialsRotationGeneratedCredentialsResponse
|
||||
} from "@app/hooks/api/secretRotationsV2/types/postgres-credentials-rotation";
|
||||
import { TSqlOptionTemplate } from "@app/hooks/api/secretRotationsV2/types/shared";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
export type TSecretRotationV2 = (TPostgresCredentialsRotation | TMsSqlCredentialsRotation) & {
|
||||
secrets: (SecretV3RawSanitized | null)[];
|
||||
};
|
||||
|
||||
export type TSecretRotationV2Option = {
|
||||
name: string;
|
||||
type: SecretRotation;
|
||||
connection: AppConnection;
|
||||
template: TSqlOptionTemplate;
|
||||
};
|
||||
|
||||
export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] };
|
||||
|
||||
export type TSecretRotationV2Response = { secretRotation: TSecretRotationV2 };
|
||||
|
||||
export type TViewSecretRotationGeneratedCredentialsResponse =
|
||||
| TPostgresCredentialsRotationGeneratedCredentialsResponse
|
||||
| TMsSqlCredentialsRotationGeneratedCredentialsResponse;
|
||||
|
||||
export type TCreateSecretRotationV2DTO = DiscriminativePick<
|
||||
TSecretRotationV2,
|
||||
| "name"
|
||||
| "parameters"
|
||||
| "secretsMapping"
|
||||
| "description"
|
||||
| "connectionId"
|
||||
| "type"
|
||||
| "isAutoRotationEnabled"
|
||||
| "rotationInterval"
|
||||
| "rotateAtUtc"
|
||||
> & { environment: string; secretPath: string; projectId: string };
|
||||
|
||||
export type TUpdateSecretRotationV2DTO = Partial<
|
||||
Omit<TCreateSecretRotationV2DTO, "type" | "secretPath" | "projectId">
|
||||
> & {
|
||||
type: SecretRotation;
|
||||
rotationId: string;
|
||||
// required for query invalidation
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type TRotateSecretRotationV2DTO = {
|
||||
rotationId: string;
|
||||
type: SecretRotation;
|
||||
// required for query invalidation
|
||||
secretPath: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TDeleteSecretRotationV2DTO = TRotateSecretRotationV2DTO & {
|
||||
revokeGeneratedCredentials: boolean;
|
||||
deleteSecrets: boolean;
|
||||
};
|
||||
|
||||
export type TViewSecretRotationV2GeneratedCredentialsDTO = {
|
||||
rotationId: string;
|
||||
type: SecretRotation;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import {
|
||||
TSecretRotationV2Base,
|
||||
TSecretRotationV2GeneratedCredentialsResponseBase,
|
||||
TSqlCredentialsRotationGeneratedCredentials,
|
||||
TSqlCredentialsRotationProperties
|
||||
} from "@app/hooks/api/secretRotationsV2/types/shared";
|
||||
|
||||
export type TMsSqlCredentialsRotation = TSecretRotationV2Base & {
|
||||
type: SecretRotation.MsSqlCredentials;
|
||||
} & TSqlCredentialsRotationProperties;
|
||||
|
||||
export type TMsSqlCredentialsRotationGeneratedCredentialsResponse =
|
||||
TSecretRotationV2GeneratedCredentialsResponseBase<
|
||||
SecretRotation.MsSqlCredentials,
|
||||
TSqlCredentialsRotationGeneratedCredentials
|
||||
>;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import {
|
||||
TSecretRotationV2Base,
|
||||
TSecretRotationV2GeneratedCredentialsResponseBase,
|
||||
TSqlCredentialsRotationGeneratedCredentials,
|
||||
TSqlCredentialsRotationProperties
|
||||
} from "@app/hooks/api/secretRotationsV2/types/shared";
|
||||
|
||||
export type TPostgresCredentialsRotation = TSecretRotationV2Base & {
|
||||
type: SecretRotation.PostgresCredentials;
|
||||
} & TSqlCredentialsRotationProperties;
|
||||
|
||||
export type TPostgresCredentialsRotationGeneratedCredentialsResponse =
|
||||
TSecretRotationV2GeneratedCredentialsResponseBase<
|
||||
SecretRotation.PostgresCredentials,
|
||||
TSqlCredentialsRotationGeneratedCredentials
|
||||
>;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./secret-rotation-base";
|
||||
export * from "./sql-credentials-rotation";
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretRotationStatus } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export type TSecretRotationV2Base = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
folderId: string;
|
||||
connectionId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
rotationInterval: number;
|
||||
rotateAtUtc: {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
};
|
||||
projectId: string;
|
||||
rotationStatus: SecretRotationStatus | null;
|
||||
lastRotationJobId: string | null;
|
||||
lastRotatedAt: string;
|
||||
lastRotationAttemptedAt: string;
|
||||
lastRotationMessage?: string | null;
|
||||
connection: {
|
||||
app: AppConnection;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
folder: {
|
||||
id: string;
|
||||
path: string;
|
||||
};
|
||||
} & (
|
||||
| {
|
||||
nextRotationAt: string;
|
||||
isAutoRotationEnabled: true;
|
||||
}
|
||||
| {
|
||||
nextRotationAt?: null;
|
||||
isAutoRotationEnabled: false;
|
||||
}
|
||||
);
|
||||
|
||||
export type TSecretRotationV2GeneratedCredentialsResponseBase<U, T> = {
|
||||
activeIndex: 0 | 1;
|
||||
generatedCredentials: [T, T | undefined];
|
||||
type: U;
|
||||
rotationId: string;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
export type TSqlCredentialsRotationProperties = {
|
||||
parameters: {
|
||||
username1: string;
|
||||
username2: string;
|
||||
};
|
||||
secretsMapping: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TSqlOptionTemplate = {
|
||||
secretsMapping: TSqlCredentialsRotationProperties["secretsMapping"];
|
||||
createUserStatement: string;
|
||||
};
|
||||
|
||||
export type TSqlCredentialsRotationGeneratedCredentials = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
@@ -85,7 +85,9 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
|
||||
version: el.version,
|
||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||
path: el.secretPath,
|
||||
secretMetadata: el.secretMetadata
|
||||
secretMetadata: el.secretMetadata,
|
||||
isRotatedSecret: el.isRotatedSecret,
|
||||
rotationId: el.rotationId
|
||||
};
|
||||
|
||||
if (el.type === SecretType.Personal) {
|
||||
|
||||
@@ -54,6 +54,8 @@ export type SecretV3RawSanitized = {
|
||||
skipMultilineEncoding?: boolean;
|
||||
secretMetadata?: { key: string; value: string }[];
|
||||
isReminderEvent?: boolean;
|
||||
isRotatedSecret?: boolean;
|
||||
rotationId?: string;
|
||||
};
|
||||
|
||||
export type SecretV3Raw = {
|
||||
@@ -76,6 +78,8 @@ export type SecretV3Raw = {
|
||||
tags?: WsTag[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isRotatedSecret?: boolean;
|
||||
rotationId?: string;
|
||||
};
|
||||
|
||||
export type SecretV3RawResponse = {
|
||||
@@ -166,7 +170,7 @@ export type TUpdateSecretsV3DTO = {
|
||||
skipMultilineEncoding?: boolean;
|
||||
newSecretName?: string;
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
secretValue?: string;
|
||||
secretComment?: string;
|
||||
secretReminderRepeatDays?: number | null;
|
||||
secretReminderNote?: string | null;
|
||||
|
||||
@@ -72,6 +72,55 @@ export const useDynamicSecretOverview = (
|
||||
return { dynamicSecretNames, isDynamicSecretPresentInEnv };
|
||||
};
|
||||
|
||||
export const useSecretRotationOverview = (
|
||||
secretRotations: DashboardProjectSecretsOverview["secretRotations"]
|
||||
) => {
|
||||
const secretRotationNames = useMemo(() => {
|
||||
const names = new Set<string>();
|
||||
secretRotations?.forEach((secretRotation) => {
|
||||
names.add(secretRotation.name);
|
||||
});
|
||||
return [...names];
|
||||
}, [secretRotations]);
|
||||
|
||||
const isSecretRotationPresentInEnv = useCallback(
|
||||
(name: string, env: string) => {
|
||||
return Boolean(
|
||||
secretRotations?.find(
|
||||
({ name: secretRotationName, environment }) =>
|
||||
secretRotationName === name && environment.slug === env
|
||||
)
|
||||
);
|
||||
},
|
||||
[secretRotations]
|
||||
);
|
||||
|
||||
const getSecretRotationByName = useCallback(
|
||||
(env: string, name: string) => {
|
||||
const secretRotation = secretRotations?.find(
|
||||
(rotation) => rotation.environment.slug === env && rotation.name === name
|
||||
);
|
||||
return secretRotation;
|
||||
},
|
||||
[secretRotations]
|
||||
);
|
||||
|
||||
const getSecretRotationStatusesByName = useCallback(
|
||||
(name: string) =>
|
||||
secretRotations
|
||||
?.filter((rotation) => rotation.name === name)
|
||||
.map((rotation) => rotation.rotationStatus),
|
||||
[secretRotations]
|
||||
);
|
||||
|
||||
return {
|
||||
secretRotationNames,
|
||||
isSecretRotationPresentInEnv,
|
||||
getSecretRotationByName,
|
||||
getSecretRotationStatusesByName
|
||||
};
|
||||
};
|
||||
|
||||
export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secrets"]) => {
|
||||
const secKeys = useMemo(() => {
|
||||
const keys = new Set<string>();
|
||||
|
||||
@@ -11,8 +11,12 @@ import {
|
||||
MenuItem,
|
||||
TBreadcrumbFormat
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
|
||||
import { useSubscription, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useGetAccessRequestsCount,
|
||||
useGetSecretApprovalRequestCount,
|
||||
useGetSecretRotations
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { ProjectSelect } from "./components/ProjectSelect";
|
||||
@@ -42,6 +46,16 @@ export const ProjectLayout = () => {
|
||||
options: { enabled: isSecretManager }
|
||||
});
|
||||
|
||||
// we only show the secret rotations v1 tab if they have existing rotations
|
||||
const { subscription } = useSubscription();
|
||||
const { data: secretRotations } = useGetSecretRotations({
|
||||
workspaceId,
|
||||
options: {
|
||||
enabled: isSecretManager && Boolean(subscription.secretRotation),
|
||||
refetchOnMount: false
|
||||
}
|
||||
});
|
||||
|
||||
const pendingRequestsCount =
|
||||
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
|
||||
@@ -147,7 +161,7 @@ export const ProjectLayout = () => {
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
{isSecretManager && (
|
||||
{isSecretManager && Boolean(secretRotations?.length) && (
|
||||
<Link
|
||||
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
|
||||
params={{
|
||||
|
||||
@@ -11,3 +11,10 @@ export const GenericResourceNameSchema = z
|
||||
/^[a-zA-Z0-9\-_\s]+$/,
|
||||
"Name can only contain alphanumeric characters, dashes, underscores, and spaces"
|
||||
);
|
||||
|
||||
export const BaseSecretNameSchema = z.string().trim().min(1);
|
||||
|
||||
export const SecretNameSchema = BaseSecretNameSchema.refine(
|
||||
(el) => !el.includes(" "),
|
||||
"Secret name cannot contain spaces."
|
||||
).refine((el) => !el.includes(":"), "Secret name cannot contain colon.");
|
||||
|
||||
@@ -91,7 +91,8 @@ export const AccessManagementPage = () => {
|
||||
<p className="mb-2 mt-1 text-sm text-bunker-300">
|
||||
We've developed an improved privilege management system to better serve your
|
||||
security needs. Upgrade to our new permission-based approach that allows you to
|
||||
explicitly designate who can modify specific access levels, rather than relying on hierarchy comparisons.
|
||||
explicitly designate who can modify specific access levels, rather than relying on
|
||||
hierarchy comparisons.
|
||||
</p>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
|
||||
+18
-2
@@ -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}`);
|
||||
}
|
||||
|
||||
+2
-1
@@ -32,7 +32,8 @@ export const GenericAppConnectionsFields = () => {
|
||||
isOptional
|
||||
>
|
||||
<TextArea
|
||||
className="h-20 !resize-none"
|
||||
className="!resize-none"
|
||||
rows={1}
|
||||
placeholder="Connection description..."
|
||||
{...register("description")}
|
||||
/>
|
||||
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+146
@@ -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>
|
||||
);
|
||||
};
|
||||
+143
@@ -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>
|
||||
);
|
||||
};
|
||||
+43
@@ -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>
|
||||
);
|
||||
};
|
||||
+9
@@ -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's credentials are managed by Infisical and cannot be updated.
|
||||
</p>
|
||||
</NoticeBannerV2>
|
||||
);
|
||||
+155
@@ -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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export * from "./PlatformManagedNoticeBanner";
|
||||
export * from "./sql-connection-schemas";
|
||||
export * from "./SqlConnectionFields";
|
||||
+10
@@ -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()
|
||||
});
|
||||
+79
-65
@@ -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>
|
||||
);
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@ type Props = {
|
||||
type:
|
||||
| ProjectPermissionSub.DynamicSecrets
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| ProjectPermissionSub.SecretImports;
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.SecretRotation;
|
||||
};
|
||||
|
||||
export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => {
|
||||
|
||||
+51
-5
@@ -17,6 +17,7 @@ import {
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
TPermissionCondition,
|
||||
TPermissionConditionOperators
|
||||
@@ -66,6 +67,15 @@ const SecretSyncPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretSyncActions.RemoveSecrets]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretRotationPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretRotationActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretRotationActions.ReadGeneratedCredentials]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretRotationActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretRotationActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretRotationActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretRotationActions.RotateSecrets]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const KmipPolicyActionSchema = z.object({
|
||||
[ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(),
|
||||
[ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(),
|
||||
@@ -209,7 +219,12 @@ export const projectRoleFormSchema = z.object({
|
||||
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretRotation]: SecretRotationPolicyActionSchema.extend({
|
||||
inverted: z.boolean().optional(),
|
||||
conditions: ConditionSchema
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([]),
|
||||
@@ -226,6 +241,7 @@ type TConditionalFields =
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.DynamicSecrets
|
||||
| ProjectPermissionSub.SecretRotation
|
||||
| ProjectPermissionSub.Identity;
|
||||
|
||||
export const isConditionalSubjects = (
|
||||
@@ -235,6 +251,7 @@ export const isConditionalSubjects = (
|
||||
subject === ProjectPermissionSub.DynamicSecrets ||
|
||||
subject === ProjectPermissionSub.SecretImports ||
|
||||
subject === ProjectPermissionSub.SecretFolders ||
|
||||
subject === ProjectPermissionSub.SecretRotation ||
|
||||
subject === ProjectPermissionSub.Identity;
|
||||
|
||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||
@@ -298,6 +315,30 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
if (isConditionalSubjects(subject)) {
|
||||
if (!formVal[subject]) formVal[subject] = [];
|
||||
|
||||
if (subject === ProjectPermissionSub.SecretRotation) {
|
||||
const canRead = action.includes(ProjectPermissionSecretRotationActions.Read);
|
||||
const canReadCredentials = action.includes(
|
||||
ProjectPermissionSecretRotationActions.ReadGeneratedCredentials
|
||||
);
|
||||
const canEdit = action.includes(ProjectPermissionSecretRotationActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionSecretRotationActions.Delete);
|
||||
const canCreate = action.includes(ProjectPermissionSecretRotationActions.Create);
|
||||
const canRotate = action.includes(ProjectPermissionSecretRotationActions.RotateSecrets);
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
formVal[subject]!.push({
|
||||
[ProjectPermissionSecretRotationActions.Read]: canRead,
|
||||
[ProjectPermissionSecretRotationActions.ReadGeneratedCredentials]: canReadCredentials,
|
||||
[ProjectPermissionSecretRotationActions.Create]: canCreate,
|
||||
[ProjectPermissionSecretRotationActions.Edit]: canEdit,
|
||||
[ProjectPermissionSecretRotationActions.Delete]: canDelete,
|
||||
[ProjectPermissionSecretRotationActions.RotateSecrets]: canRotate,
|
||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||
inverted
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.DynamicSecrets) {
|
||||
const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential);
|
||||
const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential);
|
||||
@@ -890,10 +931,15 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
[ProjectPermissionSub.SecretRotation]: {
|
||||
title: "Secret Rotation",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" }
|
||||
{ label: "Read Config", value: ProjectPermissionSecretRotationActions.Read },
|
||||
{
|
||||
label: "Read Generated Credentials",
|
||||
value: ProjectPermissionSecretRotationActions.ReadGeneratedCredentials
|
||||
},
|
||||
{ label: "Create Config", value: ProjectPermissionSecretRotationActions.Create },
|
||||
{ label: "Modify Config", value: ProjectPermissionSecretRotationActions.Edit },
|
||||
{ label: "Remove Config", value: ProjectPermissionSecretRotationActions.Delete },
|
||||
{ label: "Rotate Secrets", value: ProjectPermissionSecretRotationActions.RotateSecrets }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SecretRollback]: {
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
faFolderPlus,
|
||||
faKey,
|
||||
faList,
|
||||
faPlus
|
||||
faPlus,
|
||||
faRotate
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router";
|
||||
@@ -23,6 +24,11 @@ import { twMerge } from "tailwind-merge";
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { CreateSecretRotationV2Modal } from "@app/components/secret-rotations-v2";
|
||||
import { DeleteSecretRotationV2Modal } from "@app/components/secret-rotations-v2/DeleteSecretRotationV2Modal";
|
||||
import { EditSecretRotationV2Modal } from "@app/components/secret-rotations-v2/EditSecretRotationV2Modal";
|
||||
import { RotateSecretRotationV2Modal } from "@app/components/secret-rotations-v2/RotateSecretRotationV2Modal";
|
||||
import { ViewSecretRotationV2GeneratedCredentialsModal } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -57,6 +63,7 @@ import {
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useCreateFolder,
|
||||
@@ -71,9 +78,16 @@ import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
||||
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
|
||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
|
||||
import {
|
||||
useDynamicSecretOverview,
|
||||
useFolderOverview,
|
||||
useSecretOverview,
|
||||
useSecretRotationOverview
|
||||
} from "@app/hooks/utils";
|
||||
import { SecretOverviewSecretRotationRow } from "@app/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow";
|
||||
|
||||
import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
|
||||
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
|
||||
@@ -99,7 +113,8 @@ enum RowType {
|
||||
Folder = "folder",
|
||||
DynamicSecret = "dynamic",
|
||||
Secret = "secret",
|
||||
Import = "import"
|
||||
Import = "import",
|
||||
SecretRotation = "rotation"
|
||||
}
|
||||
|
||||
type Filter = {
|
||||
@@ -110,7 +125,8 @@ const DEFAULT_FILTER_STATE = {
|
||||
[RowType.Folder]: true,
|
||||
[RowType.DynamicSecret]: true,
|
||||
[RowType.Secret]: true,
|
||||
[RowType.Import]: true
|
||||
[RowType.Import]: true,
|
||||
[RowType.SecretRotation]: true
|
||||
};
|
||||
|
||||
export const OverviewPage = () => {
|
||||
@@ -195,6 +211,15 @@ export const OverviewPage = () => {
|
||||
})
|
||||
)
|
||||
);
|
||||
const userAvailableSecretRotationEnvs = userAvailableEnvs.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionSecretRotationActions.Create,
|
||||
subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment: env.slug,
|
||||
secretPath
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs);
|
||||
|
||||
@@ -224,6 +249,7 @@ export const OverviewPage = () => {
|
||||
includeDynamicSecrets: filter.dynamic,
|
||||
includeSecrets: filter.secret,
|
||||
includeImports: filter.import,
|
||||
includeSecretRotations: filter.rotation,
|
||||
search: debouncedSearchFilter,
|
||||
limit,
|
||||
offset
|
||||
@@ -235,15 +261,18 @@ export const OverviewPage = () => {
|
||||
secrets,
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
secretRotations,
|
||||
totalFolderCount,
|
||||
totalSecretCount,
|
||||
totalDynamicSecretCount,
|
||||
totalSecretRotationCount,
|
||||
totalImportCount,
|
||||
totalCount = 0,
|
||||
totalUniqueFoldersInPage,
|
||||
totalUniqueSecretsInPage,
|
||||
totalUniqueSecretImportsInPage,
|
||||
totalUniqueDynamicSecretsInPage
|
||||
totalUniqueDynamicSecretsInPage,
|
||||
totalUniqueSecretRotationsInPage
|
||||
} = overview ?? {};
|
||||
|
||||
const secretImportsShaped = secretImports
|
||||
@@ -273,6 +302,13 @@ export const OverviewPage = () => {
|
||||
const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
|
||||
useDynamicSecretOverview(dynamicSecrets);
|
||||
|
||||
const {
|
||||
secretRotationNames,
|
||||
isSecretRotationPresentInEnv,
|
||||
getSecretRotationByName,
|
||||
getSecretRotationStatusesByName
|
||||
} = useSecretRotationOverview(secretRotations);
|
||||
|
||||
const { secKeys, getEnvSecretKeyCount } = useSecretOverview(
|
||||
secrets?.concat(secretImportsShaped) || []
|
||||
);
|
||||
@@ -301,6 +337,11 @@ export const OverviewPage = () => {
|
||||
"misc",
|
||||
"updateFolder",
|
||||
"addDynamicSecret",
|
||||
"addSecretRotation",
|
||||
"editSecretRotation",
|
||||
"rotateSecretRotation",
|
||||
"viewSecretRotationGeneratedCredentials",
|
||||
"deleteSecretRotation",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
@@ -841,6 +882,21 @@ export const OverviewPage = () => {
|
||||
<span>Dynamic Secrets</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleToggleRowType(RowType.SecretRotation);
|
||||
}}
|
||||
icon={
|
||||
filter[RowType.SecretRotation] && <FontAwesomeIcon icon={faCheckCircle} />
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faRotate} className="text-mineshaft-400" />
|
||||
<span>Secret Rotations</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -952,6 +1008,29 @@ export const OverviewPage = () => {
|
||||
Add Dynamic Secret
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
userAvailableSecretRotationEnvs.length === 0 ? "Access restricted" : ""
|
||||
}
|
||||
>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faRotate} className="pr-2" />}
|
||||
onClick={() => {
|
||||
if (subscription?.secretRotation) {
|
||||
handlePopUpOpen("addSecretRotation");
|
||||
handlePopUpClose("misc");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
isDisabled={userAvailableSecretRotationEnvs.length === 0}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add Secret Rotation
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -1141,6 +1220,29 @@ export const OverviewPage = () => {
|
||||
key={`overview-${dynamicSecretName}-${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
{secretRotationNames.map((secretRotationName, index) => (
|
||||
<SecretOverviewSecretRotationRow
|
||||
secretRotationName={secretRotationName}
|
||||
isSecretRotationInEnv={isSecretRotationPresentInEnv}
|
||||
environments={visibleEnvs}
|
||||
getSecretRotationByName={getSecretRotationByName}
|
||||
getSecretRotationStatusesByName={getSecretRotationStatusesByName}
|
||||
key={`overview-${secretRotationName}-${index + 1}`}
|
||||
scrollOffset={scrollOffset}
|
||||
onEdit={(secretRotation) =>
|
||||
handlePopUpOpen("editSecretRotation", secretRotation)
|
||||
}
|
||||
onRotate={(secretRotation) =>
|
||||
handlePopUpOpen("rotateSecretRotation", secretRotation)
|
||||
}
|
||||
onViewGeneratedCredentials={(secretRotation) =>
|
||||
handlePopUpOpen("viewSecretRotationGeneratedCredentials", secretRotation)
|
||||
}
|
||||
onDelete={(secretRotation) =>
|
||||
handlePopUpOpen("deleteSecretRotation", secretRotation)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{secKeys.map((key, index) => (
|
||||
<SecretOverviewTableRow
|
||||
isSelected={Boolean(selectedEntries.secret[key])}
|
||||
@@ -1165,7 +1267,8 @@ export const OverviewPage = () => {
|
||||
(totalUniqueFoldersInPage || 0) -
|
||||
(totalUniqueDynamicSecretsInPage || 0) -
|
||||
(totalUniqueSecretsInPage || 0) -
|
||||
(totalUniqueSecretImportsInPage || 0),
|
||||
(totalUniqueSecretImportsInPage || 0) -
|
||||
(totalUniqueSecretRotationsInPage || 0),
|
||||
0
|
||||
)}
|
||||
/>
|
||||
@@ -1206,6 +1309,7 @@ export const OverviewPage = () => {
|
||||
secretCount={totalSecretCount}
|
||||
folderCount={totalFolderCount}
|
||||
importCount={totalImportCount}
|
||||
secretRotationCount={totalSecretRotationCount}
|
||||
/>
|
||||
}
|
||||
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
|
||||
@@ -1277,6 +1381,34 @@ export const OverviewPage = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<CreateSecretRotationV2Modal
|
||||
secretPath={secretPath}
|
||||
environments={userAvailableSecretRotationEnvs}
|
||||
isOpen={popUp.addSecretRotation.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretRotation", isOpen)}
|
||||
/>
|
||||
<EditSecretRotationV2Modal
|
||||
isOpen={popUp.editSecretRotation.isOpen}
|
||||
secretRotation={popUp.editSecretRotation.data as TSecretRotationV2}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("editSecretRotation", isOpen)}
|
||||
/>
|
||||
<RotateSecretRotationV2Modal
|
||||
isOpen={popUp.rotateSecretRotation.isOpen}
|
||||
secretRotation={popUp.rotateSecretRotation.data as TSecretRotationV2}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("rotateSecretRotation", isOpen)}
|
||||
/>
|
||||
<ViewSecretRotationV2GeneratedCredentialsModal
|
||||
isOpen={popUp.viewSecretRotationGeneratedCredentials.isOpen}
|
||||
secretRotation={popUp.viewSecretRotationGeneratedCredentials.data as TSecretRotationV2}
|
||||
onOpenChange={(isOpen) =>
|
||||
handlePopUpToggle("viewSecretRotationGeneratedCredentials", isOpen)
|
||||
}
|
||||
/>
|
||||
<DeleteSecretRotationV2Modal
|
||||
isOpen={popUp.deleteSecretRotation.isOpen}
|
||||
secretRotation={popUp.deleteSecretRotation.data as TSecretRotationV2}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deleteSecretRotation", isOpen)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faAsterisk,
|
||||
faCheck,
|
||||
faClose,
|
||||
faEdit,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faInfoCircle,
|
||||
faRotate,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { SecretRotationV2StatusBadge } from "@app/components/secret-rotations-v2/SecretRotationV2StatusBadge";
|
||||
import { Badge, IconButton, TableContainer, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import {
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { getExpandedRowStyle } from "@app/pages/secret-manager/OverviewPage/components/utils";
|
||||
|
||||
type Props = {
|
||||
secretRotationName: string;
|
||||
environments: { name: string; slug: string }[];
|
||||
isSecretRotationInEnv: (name: string, env: string) => boolean;
|
||||
getSecretRotationByName: (slug: string, name: string) => TSecretRotationV2 | undefined;
|
||||
getSecretRotationStatusesByName: (name: string) => (SecretRotationStatus | null)[] | undefined;
|
||||
scrollOffset: number;
|
||||
onEdit: (secretRotation: TSecretRotationV2) => void;
|
||||
onRotate: (secretRotation: TSecretRotationV2) => void;
|
||||
onViewGeneratedCredentials: (secretRotation: TSecretRotationV2) => void;
|
||||
onDelete: (secretRotation: TSecretRotationV2) => void;
|
||||
};
|
||||
|
||||
export const SecretOverviewSecretRotationRow = ({
|
||||
secretRotationName,
|
||||
environments = [],
|
||||
isSecretRotationInEnv,
|
||||
scrollOffset,
|
||||
getSecretRotationByName,
|
||||
getSecretRotationStatusesByName,
|
||||
onEdit,
|
||||
onRotate,
|
||||
onViewGeneratedCredentials,
|
||||
onDelete
|
||||
}: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useToggle(false);
|
||||
const [isSecretVisible, setIsSecretVisible] = useToggle();
|
||||
|
||||
const totalCols = environments.length + 1; // secret key row
|
||||
|
||||
const statuses = getSecretRotationStatusesByName(secretRotationName);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr isHoverable isSelectable onClick={setIsExpanded.toggle} className="group">
|
||||
<Td className="sticky left-0 z-10 border-0 bg-mineshaft-800 bg-clip-padding p-0 group-hover:bg-mineshaft-700">
|
||||
<div className="flex w-full items-center space-x-5 border-r border-mineshaft-600 py-2.5 pl-5 pr-2">
|
||||
<div className="text-mineshaft-400">
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</div>
|
||||
<div className="flex-1">{secretRotationName}</div>
|
||||
{statuses?.some((status) => status === SecretRotationStatus.Failed) && (
|
||||
<Tooltip className="max-w-sm" content="One or more secrets failed to rotate.">
|
||||
<div>
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="flex h-5 w-min items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
<span>Rotation Failed</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
{environments.map(({ slug }, i) => {
|
||||
const isPresent = isSecretRotationInEnv(secretRotationName, slug);
|
||||
|
||||
return (
|
||||
<Td
|
||||
key={`sec-overview-${slug}-${i + 1}-folder`}
|
||||
className={twMerge(
|
||||
"border-r border-mineshaft-600 py-3 group-hover:bg-mineshaft-700",
|
||||
isPresent ? "text-green-600" : "text-red-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-center">
|
||||
<FontAwesomeIcon
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
icon={isPresent ? faCheck : faXmark}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
{isExpanded &&
|
||||
environments.map(({ name: envName, slug }) => {
|
||||
const secretRotation = getSecretRotationByName(slug, secretRotationName);
|
||||
|
||||
if (!secretRotation) return null;
|
||||
|
||||
const { type, secrets, environment, folder, description } = secretRotation;
|
||||
|
||||
const { name: rotationType, image } = SECRET_ROTATION_MAP[type];
|
||||
|
||||
return (
|
||||
<Tr key={`secret-rotation-${slug}-${secretRotationName}`}>
|
||||
<Td
|
||||
colSpan={totalCols}
|
||||
className={`bg-bunker-600 px-0 py-0 ${isExpanded && "border-b-2 border-mineshaft-500"}`}
|
||||
>
|
||||
<div style={getExpandedRowStyle(scrollOffset)} className="ml-2 p-2">
|
||||
<TableContainer>
|
||||
<table className="secret-table">
|
||||
<thead className="!border-b">
|
||||
<tr className="h-10">
|
||||
<th
|
||||
style={{ padding: "0.5rem 1rem" }}
|
||||
className="min-table-row min-w-[30vw] !border-r-0"
|
||||
>
|
||||
<div className="flex w-full flex-wrap items-center">
|
||||
<span>{envName}</span>
|
||||
<Tag className="mx-2.5 flex items-center gap-1 px-1.5 py-0 text-xs normal-case">
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
style={{
|
||||
height: "11px",
|
||||
width: "11px"
|
||||
}}
|
||||
alt={`${rotationType} logo`}
|
||||
/>
|
||||
{rotationType}
|
||||
</Tag>
|
||||
{description && (
|
||||
<Tooltip content={description}>
|
||||
<FontAwesomeIcon
|
||||
icon={faInfoCircle}
|
||||
className="text-mineshaft-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<div className="absolute right-1 top-0.5 ml-auto mr-1 mt-1 w-min">
|
||||
<div className="flex items-center gap-2">
|
||||
<SecretRotationV2StatusBadge secretRotation={secretRotation} />
|
||||
<Tooltip content={isSecretVisible ? "Hide Values" : "Reveal Values"}>
|
||||
<IconButton
|
||||
ariaLabel={isSecretVisible ? "Hide Values" : "Reveal Values"}
|
||||
variant="plain"
|
||||
size="md"
|
||||
onClick={() => setIsSecretVisible.toggle()}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretRotationActions.ReadGeneratedCredentials}
|
||||
a={subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="View Generated Credentials"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="View generated credentials"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => onViewGeneratedCredentials(secretRotation)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAsterisk} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretRotationActions.RotateSecrets}
|
||||
a={subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Rotate Secrets"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="Rotate secrets"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => onRotate(secretRotation)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretRotationActions.Edit}
|
||||
a={subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="Edit rotation"
|
||||
variant="plain"
|
||||
size="md"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => onEdit(secretRotation)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretRotationActions.Delete}
|
||||
a={subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="Delete rotation"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="md"
|
||||
onClick={() => onDelete(secretRotation)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="border-t-2 border-mineshaft-600">
|
||||
{secrets.map((secret, index) => {
|
||||
return (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
secret
|
||||
? undefined
|
||||
: "You do not have permission to view this secret."
|
||||
}
|
||||
>
|
||||
<tr
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||
className="!last:border-b-0 h-full hover:bg-mineshaft-700"
|
||||
>
|
||||
<td
|
||||
className="flex h-full items-center"
|
||||
style={{ padding: "0.5rem 1rem" }}
|
||||
>
|
||||
<span className={twMerge(!secret && "blur")}>
|
||||
{secret?.key ?? "********"}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="col-span-2 h-full w-full"
|
||||
style={{ padding: "0.5rem 1rem" }}
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly
|
||||
value={secret.value}
|
||||
isVisible={isSecretVisible}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user