mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 08:14:06 +00:00
Merge branch 'main' into feat/showWarningOnImportedSecretDeletion
This commit is contained in:
@@ -25,7 +25,10 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import {
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useGetImportedSecretsSingleEnv,
|
||||
@@ -39,6 +42,7 @@ import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView";
|
||||
|
||||
import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount";
|
||||
import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection";
|
||||
@@ -137,6 +141,11 @@ const Page = () => {
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const canReadSecretRotations = permission.can(
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
subject(ProjectPermissionSub.SecretRotation, { environment, secretPath })
|
||||
);
|
||||
|
||||
const canDoReadRollback = permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
@@ -150,7 +159,8 @@ const Page = () => {
|
||||
[RowType.Folder]: true,
|
||||
[RowType.Import]: true,
|
||||
[RowType.DynamicSecret]: true,
|
||||
[RowType.Secret]: true
|
||||
[RowType.Secret]: true,
|
||||
[RowType.SecretRotation]: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -194,6 +204,7 @@ const Page = () => {
|
||||
viewSecretValue: canReadSecretValue,
|
||||
includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic,
|
||||
includeSecrets: canReadSecret && filter.include.secret,
|
||||
includeSecretRotations: canReadSecretRotations && filter.include.rotation,
|
||||
tags: filter.tags
|
||||
});
|
||||
|
||||
@@ -201,13 +212,15 @@ const Page = () => {
|
||||
imports,
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
secretRotations,
|
||||
secrets,
|
||||
totalImportCount = 0,
|
||||
totalFolderCount = 0,
|
||||
totalDynamicSecretCount = 0,
|
||||
totalSecretCount = 0,
|
||||
totalCount = 0,
|
||||
importedBy
|
||||
importedBy,
|
||||
totalSecretRotationCount = 0
|
||||
} = data ?? {};
|
||||
|
||||
useResetPageHelper({
|
||||
@@ -267,7 +280,8 @@ const Page = () => {
|
||||
(imports?.length || 0) -
|
||||
(folders?.length || 0) -
|
||||
(secrets?.length || 0) -
|
||||
(dynamicSecrets?.length || 0),
|
||||
(dynamicSecrets?.length || 0) -
|
||||
(secretRotations?.length || 0),
|
||||
0
|
||||
);
|
||||
const isNotEmpty = Boolean(
|
||||
@@ -275,6 +289,7 @@ const Page = () => {
|
||||
folders?.length ||
|
||||
imports?.length ||
|
||||
dynamicSecrets?.length ||
|
||||
secretRotations?.length ||
|
||||
noAccessSecretCount
|
||||
);
|
||||
|
||||
@@ -364,6 +379,8 @@ const Page = () => {
|
||||
const selectedSecretActions = useSelectedSecretActions();
|
||||
|
||||
const allRowsSelectedOnPage = useMemo(() => {
|
||||
if (!secrets?.length) return { isChecked: false, isIndeterminate: false };
|
||||
|
||||
if (secrets?.every((secret) => selectedSecrets[secret.id]))
|
||||
return { isChecked: true, isIndeterminate: false };
|
||||
|
||||
@@ -499,6 +516,9 @@ const Page = () => {
|
||||
dynamicSecrets={dynamicSecrets}
|
||||
/>
|
||||
)}
|
||||
{canReadSecretRotations && Boolean(secretRotations?.length) && (
|
||||
<SecretRotationListView secretRotations={secretRotations} />
|
||||
)}
|
||||
{canReadSecret && Boolean(secrets?.length) && (
|
||||
<SecretListView
|
||||
secrets={secrets}
|
||||
@@ -526,6 +546,7 @@ const Page = () => {
|
||||
importCount={totalImportCount}
|
||||
secretCount={totalSecretCount}
|
||||
folderCount={totalFolderCount}
|
||||
secretRotationCount={totalSecretRotationCount}
|
||||
/>
|
||||
}
|
||||
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
|
||||
|
||||
@@ -10,5 +10,6 @@ export enum RowType {
|
||||
Folder = "folder",
|
||||
Import = "import",
|
||||
DynamicSecret = "dynamic",
|
||||
Secret = "secret"
|
||||
Secret = "secret",
|
||||
SecretRotation = "rotation"
|
||||
}
|
||||
|
||||
+54
@@ -19,6 +19,7 @@ import {
|
||||
faMinusSquare,
|
||||
faPaste,
|
||||
faPlus,
|
||||
faRotate,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -30,6 +31,7 @@ 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 {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
@@ -54,6 +56,7 @@ import {
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateFolder,
|
||||
@@ -133,6 +136,7 @@ export const ActionBar = ({
|
||||
"addDynamicSecret",
|
||||
"addSecretImport",
|
||||
"bulkDeleteSecrets",
|
||||
"addSecretRotation",
|
||||
"moveSecrets",
|
||||
"misc",
|
||||
"upgradePlan",
|
||||
@@ -673,6 +677,23 @@ export const ActionBar = ({
|
||||
<span>Dynamic Secrets</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onToggleRowType(RowType.SecretRotation);
|
||||
}}
|
||||
icon={
|
||||
filter?.include[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();
|
||||
@@ -856,6 +877,33 @@ export const ActionBar = ({
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretRotationActions.Create}
|
||||
a={subject(ProjectPermissionSub.SecretRotation, {
|
||||
environment,
|
||||
secretPath
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faRotate} className="pr-2" />}
|
||||
onClick={() => {
|
||||
if (subscription && subscription.secretRotation) {
|
||||
handlePopUpOpen("addSecretRotation");
|
||||
handlePopUpClose("misc");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Secret Rotation
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={subject(ProjectPermissionSub.SecretImports, {
|
||||
@@ -991,6 +1039,12 @@ export const ActionBar = ({
|
||||
secretPath={secretPath}
|
||||
isSingleEnvironmentMode
|
||||
/>
|
||||
<CreateSecretRotationV2Modal
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
isOpen={popUp.addSecretRotation.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretRotation", isOpen)}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={popUp.addFolder.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
|
||||
|
||||
+10
-7
@@ -6,7 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { Button, FormControl, Input, PasswordGenerator } from "@app/components/v2";
|
||||
import { CreatableSelect } from "@app/components/v2/CreatableSelect";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
@@ -162,12 +162,15 @@ export const CreateSecretForm = ({
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
<PasswordGenerator onUsePassword={field.onChange} />
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
+18
-16
@@ -366,7 +366,7 @@ export const SecretDetailSidebar = ({
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly || !isAllowed}
|
||||
isReadOnly={isReadOnly || !isAllowed || secret?.isRotatedSecret}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
key="secret-value"
|
||||
@@ -881,21 +881,23 @@ export const SecretDetailSidebar = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
|
||||
>
|
||||
<Tooltip content="Restore Secret Value">
|
||||
<IconButton
|
||||
ariaLabel="Restore"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
className="h-8 w-8 rounded-md"
|
||||
onClick={() => setValue("value", secretValue)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateRight} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{!secret?.isRotatedSecret && (
|
||||
<div
|
||||
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
|
||||
>
|
||||
<Tooltip content="Restore Secret Value">
|
||||
<IconButton
|
||||
ariaLabel="Restore"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
className="h-8 w-8 rounded-md"
|
||||
onClick={() => setValue("value", secretValue)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateRight} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
+36
-20
@@ -48,6 +48,8 @@ import {
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
FontAwesomeSpriteName,
|
||||
formSchema,
|
||||
@@ -91,6 +93,7 @@ export const SecretItem = memo(
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { permission } = useProjectPermission();
|
||||
const { isRotatedSecret } = secret;
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -151,7 +154,6 @@ export const SecretItem = memo(
|
||||
secretTags: selectedTagSlugs
|
||||
})
|
||||
);
|
||||
|
||||
const { secretValueHidden } = secret;
|
||||
|
||||
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
|
||||
@@ -221,29 +223,43 @@ export const SecretItem = memo(
|
||||
<div
|
||||
className={twMerge(
|
||||
"border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700",
|
||||
isDirty && "border-primary-400/50"
|
||||
isDirty && "border-primary-400/50",
|
||||
isRotatedSecret && "bg-mineshaft-700/60"
|
||||
)}
|
||||
>
|
||||
<div className="group flex">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-11 w-11 items-center justify-center px-4 py-3",
|
||||
"flex h-11 w-11 items-center justify-center px-4 py-3 text-mineshaft-300",
|
||||
isDirty && "text-primary"
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={`checkbox-${secret.id}`}
|
||||
isChecked={isSelected}
|
||||
onCheckedChange={() => onToggleSecretSelect(secret)}
|
||||
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
|
||||
/>
|
||||
<FontAwesomeSymbol
|
||||
className={twMerge(
|
||||
"ml-3 block h-3.5 w-3.5 group-hover:hidden",
|
||||
isSelected && "hidden"
|
||||
)}
|
||||
symbolName={FontAwesomeSpriteName.SecretKey}
|
||||
/>
|
||||
{secret.isRotatedSecret ? (
|
||||
<div className="relative">
|
||||
<FontAwesomeIcon icon={faKey} size="xs" className={twMerge("ml-3 h-3.5 w-3.5")} />
|
||||
<FontAwesomeIcon
|
||||
icon={faRotate}
|
||||
size="xs"
|
||||
className="absolute -bottom-[0.05rem] -right-[0.2rem] text-mineshaft-400"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Checkbox
|
||||
id={`checkbox-${secret.id}`}
|
||||
isChecked={isSelected}
|
||||
onCheckedChange={() => onToggleSecretSelect(secret)}
|
||||
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
|
||||
/>
|
||||
<FontAwesomeSymbol
|
||||
className={twMerge(
|
||||
"ml-3 block h-3.5 w-3.5 group-hover:hidden",
|
||||
isSelected && "hidden"
|
||||
)}
|
||||
symbolName={FontAwesomeSpriteName.SecretKey}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
|
||||
<Controller
|
||||
@@ -252,7 +268,7 @@ export const SecretItem = memo(
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<Input
|
||||
autoComplete="off"
|
||||
isReadOnly={isReadOnly}
|
||||
isReadOnly={isReadOnly || isRotatedSecret}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
variant="plain"
|
||||
isDisabled={isOverriden}
|
||||
@@ -294,7 +310,7 @@ export const SecretItem = memo(
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
isReadOnly={isReadOnly || isRotatedSecret}
|
||||
key="secret-value"
|
||||
isVisible={isVisible}
|
||||
environment={environment}
|
||||
@@ -582,7 +598,7 @@ export const SecretItem = memo(
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
allowedLabel={isRotatedSecret ? "Cannot Delete Rotated Secret" : "Delete"} // just using label for isRotatedSecret, disabled below
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -592,7 +608,7 @@ export const SecretItem = memo(
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDeleteSecret(secret)}
|
||||
isDisabled={!isAllowed}
|
||||
isDisabled={!isAllowed || isRotatedSecret}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Close}
|
||||
|
||||
+9
-4
@@ -82,7 +82,8 @@ export const SecretListView = ({
|
||||
skipMultilineEncoding,
|
||||
newKey,
|
||||
secretId,
|
||||
secretMetadata
|
||||
secretMetadata,
|
||||
isRotatedSecret
|
||||
}: Partial<{
|
||||
value: string;
|
||||
comment: string;
|
||||
@@ -93,6 +94,7 @@ export const SecretListView = ({
|
||||
newKey: string;
|
||||
secretId: string;
|
||||
secretMetadata?: { key: string; value: string }[];
|
||||
isRotatedSecret?: boolean;
|
||||
}> = {}
|
||||
) => {
|
||||
if (operation === "delete") {
|
||||
@@ -113,14 +115,16 @@ export const SecretListView = ({
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
...(!isRotatedSecret && {
|
||||
newSecretName: newKey,
|
||||
secretValue: value || ""
|
||||
}),
|
||||
type,
|
||||
tagIds: tags,
|
||||
secretComment: comment,
|
||||
secretReminderRepeatDays: reminderRepeatDays,
|
||||
secretReminderNote: reminderNote,
|
||||
skipMultilineEncoding,
|
||||
newSecretName: newKey,
|
||||
secretMetadata
|
||||
});
|
||||
return;
|
||||
@@ -216,7 +220,8 @@ export const SecretListView = ({
|
||||
secretId: orgSecret.id,
|
||||
newKey: hasKeyChanged ? key : undefined,
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding,
|
||||
secretMetadata
|
||||
secretMetadata,
|
||||
isRotatedSecret: orgSecret.isRotatedSecret
|
||||
});
|
||||
if (cb) cb();
|
||||
}
|
||||
|
||||
+33
-9
@@ -1,3 +1,7 @@
|
||||
import { faLock, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { FontAwesomeSymbol, Input, Tooltip } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
|
||||
@@ -5,9 +9,10 @@ import { FontAwesomeSpriteName } from "./SecretListView.utils";
|
||||
|
||||
type Props = {
|
||||
count: number;
|
||||
isRotationView?: boolean;
|
||||
};
|
||||
|
||||
export const SecretNoAccessListView = ({ count }: Props) => {
|
||||
export const SecretNoAccessListView = ({ count, isRotationView }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{Array.from(Array(count)).map((_, i) => (
|
||||
@@ -17,15 +22,34 @@ export const SecretNoAccessListView = ({ count }: Props) => {
|
||||
content="You do not have permission to view this secret"
|
||||
key={`no-access-secret-${i + 1}`}
|
||||
>
|
||||
<div className="flex border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex border-b border-mineshaft-600 shadow-none hover:bg-mineshaft-700",
|
||||
isRotationView ? "bg-mineshaft-700/60" : "bg-mineshaft-800"
|
||||
)}
|
||||
>
|
||||
<div className="flex h-11 w-11 items-center justify-center px-4 py-3">
|
||||
<FontAwesomeSymbol
|
||||
className="ml-3 block h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.KeyLock}
|
||||
/>
|
||||
{isRotationView ? (
|
||||
<div className="relative">
|
||||
<FontAwesomeIcon
|
||||
icon={faLock}
|
||||
size="xs"
|
||||
className={twMerge("ml-3 h-3.5 w-3.5")}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
icon={faRotate}
|
||||
size="xs"
|
||||
className="absolute -bottom-[0.05rem] -right-[0.2rem] text-mineshaft-400"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<FontAwesomeSymbol
|
||||
className="ml-3 block h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.KeyLock}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
|
||||
<div className="ml-[0.05rem] flex h-11 w-80 flex-shrink-0 items-center border-r border-mineshaft-600 px-4 py-2">
|
||||
<Input
|
||||
autoComplete="off"
|
||||
isReadOnly
|
||||
@@ -35,7 +59,7 @@ export const SecretNoAccessListView = ({ count }: Props) => {
|
||||
className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
|
||||
/>
|
||||
</div>
|
||||
<Blur />
|
||||
<Blur className="pl-8" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
import { useState } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faAsterisk,
|
||||
faClose,
|
||||
faEdit,
|
||||
faInfoCircle,
|
||||
faKey,
|
||||
faRotate
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { SecretRotationV2StatusBadge } from "@app/components/secret-rotations-v2/SecretRotationV2StatusBadge";
|
||||
import { IconButton, Modal, ModalContent, TableContainer, Tag, Tooltip } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
type Props = {
|
||||
secretRotation: TSecretRotationV2;
|
||||
onEdit: () => void;
|
||||
onRotate: () => void;
|
||||
onViewGeneratedCredentials: () => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export const SecretRotationItem = ({
|
||||
secretRotation,
|
||||
onEdit,
|
||||
onRotate,
|
||||
onViewGeneratedCredentials,
|
||||
onDelete
|
||||
}: Props) => {
|
||||
const { name, type, environment, folder, secrets, description } = secretRotation;
|
||||
|
||||
const { name: rotationType, image } = SECRET_ROTATION_MAP[type];
|
||||
const [showSecrets, setShowSecrets] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={twMerge("group flex border-b border-mineshaft-600 hover:bg-mineshaft-700")}>
|
||||
<div className="text- flex w-11 items-center py-2 pl-5 text-mineshaft-400">
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</div>
|
||||
<div className="flex flex-grow items-center border-r border-mineshaft-600 py-2 pl-4 pr-2">
|
||||
<div className="flex w-full flex-wrap items-center">
|
||||
<span>{name}</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>
|
||||
<SecretRotationV2StatusBadge className="mx-2" secretRotation={secretRotation} />
|
||||
<div
|
||||
key="actions"
|
||||
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
|
||||
>
|
||||
<Tooltip content="View Rotation Secrets">
|
||||
<IconButton
|
||||
ariaLabel="View rotation secrets"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowSecrets(true);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
</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}
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewGeneratedCredentials();
|
||||
}}
|
||||
>
|
||||
<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}
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRotate();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="options"
|
||||
className="flex h-10 flex-shrink-0 items-center space-x-1 px-[0.665rem]"
|
||||
initial={{ x: 0, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 10, opacity: 0 }}
|
||||
>
|
||||
<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}
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
>
|
||||
<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"
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<Modal onOpenChange={setShowSecrets} isOpen={showSecrets}>
|
||||
<ModalContent
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
className="max-w-3xl"
|
||||
title="Rotation Secrets"
|
||||
>
|
||||
<TableContainer>
|
||||
<table className="secret-table">
|
||||
<tbody>
|
||||
{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="h-full last:!border-b-0 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}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
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 { usePopUp } from "@app/hooks";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { SecretRotationItem } from "./SecretRotationItem";
|
||||
|
||||
type Props = {
|
||||
secretRotations?: TSecretRotationV2[];
|
||||
};
|
||||
|
||||
export const SecretRotationListView = ({ secretRotations }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"editSecretRotation",
|
||||
"rotateSecretRotation",
|
||||
"viewSecretRotationGeneratedCredentials",
|
||||
"deleteSecretRotation"
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<>
|
||||
{secretRotations?.map((secretRotation) => (
|
||||
<SecretRotationItem
|
||||
key={secretRotation.id}
|
||||
secretRotation={secretRotation}
|
||||
onEdit={() => handlePopUpOpen("editSecretRotation", secretRotation)}
|
||||
onRotate={() => handlePopUpOpen("rotateSecretRotation", secretRotation)}
|
||||
onViewGeneratedCredentials={() =>
|
||||
handlePopUpOpen("viewSecretRotationGeneratedCredentials", secretRotation)
|
||||
}
|
||||
onDelete={() => handlePopUpOpen("deleteSecretRotation", secretRotation)}
|
||||
/>
|
||||
))}
|
||||
<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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { SecretRotationListView } from "./SecretRotationListView";
|
||||
+9
-2
@@ -76,7 +76,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
</div>
|
||||
<div className="flex flex-grow items-center space-x-4 px-4 py-3">
|
||||
{mode === "modified" ? (
|
||||
{mode === "modified" && !preSecret?.isRotatedSecret ? (
|
||||
<>
|
||||
<div>{preSecret?.key}</div>
|
||||
<div className="rounded-lg bg-primary px-1 py-0.5 text-xs font-bold text-black">
|
||||
@@ -90,7 +90,14 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
postSecret.key
|
||||
<>
|
||||
{postSecret.key}
|
||||
{postSecret.isRotatedSecret && (
|
||||
<span className="ml-2 text-mineshaft-400">
|
||||
Rotated Secrets are not affected by Rollback
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+6
-3
@@ -98,9 +98,12 @@ export const SnapshotView = ({
|
||||
const doesExist = Boolean(secretGroupById?.[id]);
|
||||
if (doesExist) {
|
||||
diffView.push({
|
||||
mode: deepCompareSecrets(rollSecret, secretGroupById[id])
|
||||
? TDiffModes.NoChange
|
||||
: TDiffModes.Modified,
|
||||
mode:
|
||||
rollSecret.isRotatedSecret ||
|
||||
secretGroupById[id]?.isRotatedSecret ||
|
||||
deepCompareSecrets(rollSecret, secretGroupById[id])
|
||||
? TDiffModes.NoChange
|
||||
: TDiffModes.Modified,
|
||||
pre: secretGroupById[id],
|
||||
post: rollSecret
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user