Merge pull request #2199 from Infisical/secret-engine-v2-bridge

Secret engine v2 bridge
This commit is contained in:
Maidul Islam
2024-07-30 21:41:21 -04:00
committed by GitHub
192 changed files with 13069 additions and 2766 deletions
@@ -1,50 +1,39 @@
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { createNotification } from "@app/components/notifications";
import { Button, Modal, ModalContent } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import {
useDeleteIntegration,
useDeleteIntegrationAuths,
useGetCloudIntegrations,
useGetUserWsKey,
useGetWorkspaceAuthorizations,
useGetWorkspaceBot,
useGetWorkspaceIntegrations,
useUpdateBotActiveStatus
useGetWorkspaceIntegrations
} from "@app/hooks/api";
import { IntegrationAuth } from "@app/hooks/api/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
import { InfrastructureIntegrationSection } from "./components/InfrastructureIntegrationSection/InfrastructureIntegrationSection";
import { IntegrationsSection } from "./components/IntegrationsSection";
import { generateBotKey, redirectForProviderAuth } from "./IntegrationPage.utils";
import { redirectForProviderAuth } from "./IntegrationPage.utils";
type Props = {
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
infrastructureIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
infrastructureIntegrations: Array<{
name: string;
slug: string;
image: string;
docsLink: string;
}>;
};
export const IntegrationsPage = withProjectPermission(
({ frameworkIntegrations, infrastructureIntegrations }: Props) => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const environments = currentWorkspace?.environments || [];
const { data: latestWsKey } = useGetUserWsKey(workspaceId);
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"activeBot"
] as const);
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
useGetCloudIntegrations();
@@ -70,11 +59,6 @@ export const IntegrationsPage = withProjectPermission(
isFetching: isIntegrationFetching
} = useGetWorkspaceIntegrations(workspaceId);
const { data: bot } = useGetWorkspaceBot(workspaceId);
// mutation
const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
useUpdateBotActiveStatus();
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const {
mutateAsync: deleteIntegrationAuths,
@@ -95,12 +79,6 @@ export const IntegrationsPage = withProjectPermission(
isIntegrationsAuthorizedEmpty &&
isIntegrationsEmpty
) {
if (bot?.id && currentWorkspace?.version === ProjectVersion.V1)
updateBotActiveStatusSync({
isActive: false,
botId: bot.id,
workspaceId
});
resetDeleteIntegrationAuths();
}
}, [
@@ -116,16 +94,6 @@ export const IntegrationsPage = withProjectPermission(
if (!selectedCloudIntegration) return;
try {
if (bot && !bot.isActive && currentWorkspace?.version === ProjectVersion.V1) {
const botKey = generateBotKey(bot.publicKey, latestWsKey!);
await updateBotActiveStatus({
workspaceId,
botKey,
isActive: true,
botId: bot.id
});
}
redirectForProviderAuth(selectedCloudIntegration);
} catch (error) {
console.error(error);
@@ -135,19 +103,9 @@ export const IntegrationsPage = withProjectPermission(
// function to strat integration for a provider
// confirmation to user passing the bot key for provider to get secret access
const handleProviderIntegrationStart = (provider: string) => {
if (!bot?.isActive) {
handlePopUpOpen("activeBot", { provider });
return;
}
handleProviderIntegration(provider);
};
const handleUserAcceptBotCondition = () => {
const { provider } = popUp.activeBot?.data as { provider: string };
handleProviderIntegration(provider);
handlePopUpClose("activeBot");
};
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
try {
await deleteIntegration({ id: integrationId, workspaceId });
@@ -195,7 +153,6 @@ export const IntegrationsPage = withProjectPermission(
integrations={integrations}
environments={environments}
onIntegrationDelete={({ id }, cb) => handleIntegrationDelete(id, cb)}
isBotActive={bot?.isActive}
workspaceId={workspaceId}
/>
<CloudIntegrationSection
@@ -205,30 +162,6 @@ export const IntegrationsPage = withProjectPermission(
onIntegrationStart={handleProviderIntegrationStart}
onIntegrationRevoke={handleIntegrationAuthRevoke}
/>
<Modal
isOpen={popUp.activeBot?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
>
<ModalContent
title={t("integrations.grant-access-to-secrets") as string}
footerContent={
<div className="flex items-center space-x-2">
<Button onClick={() => handleUserAcceptBotCondition()}>
{t("integrations.grant-access-button") as string}
</Button>
<Button
onClick={() => handlePopUpClose("activeBot")}
variant="outline_bg"
colorSchema="secondary"
>
Cancel
</Button>
</div>
}
>
{t("integrations.why-infisical-needs-access")}
</ModalContent>
</Modal>
<FrameworkIntegrationSection frameworks={frameworkIntegrations} />
<InfrastructureIntegrationSection integrations={infrastructureIntegrations} />
</div>
@@ -1,4 +1,3 @@
import Link from "next/link";
import { faCalendarCheck } from "@fortawesome/free-regular-svg-icons";
import { faArrowRight, faRefresh, faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -7,8 +6,6 @@ import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Alert,
AlertDescription,
Button,
DeleteActionModal,
EmptyState,
@@ -29,7 +26,6 @@ type Props = {
integrations?: TIntegration[];
isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
isBotActive: boolean | undefined;
workspaceId: string;
};
@@ -38,7 +34,6 @@ export const IntegrationsSection = ({
environments = [],
isLoading,
onIntegrationDelete,
isBotActive,
workspaceId
}: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -59,21 +54,7 @@ export const IntegrationsSection = ({
</div>
)}
{!isBotActive && Boolean(integrations.length) && (
<div className="px-6 py-4">
<Alert hideTitle variant="warning">
<AlertDescription>
All the active integrations will be disabled. Disable End-to-End Encryption in{" "}
<Link href={`/project/${workspaceId}/settings`} passHref>
<a className="underline underline-offset-2">project settings </a>
</Link>
to re-enable it.
</AlertDescription>
</Alert>
</div>
)}
{!isLoading && !integrations.length && isBotActive && (
{!isLoading && !integrations.length && (
<div className="mx-6">
<EmptyState
className="rounded-md border border-mineshaft-700 pt-8 pb-4"
@@ -81,7 +62,7 @@ export const IntegrationsSection = ({
/>
</div>
)}
{!isLoading && isBotActive && (
{!isLoading && (
<div className="flex min-w-max flex-col space-y-4 p-6 pt-0">
{integrations?.map((integration) => (
<div
@@ -13,11 +13,11 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { CommitType, DecryptedSecret, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
export type Props = {
op: CommitType;
secretVersion?: DecryptedSecret;
secretVersion?: SecretV3Raw;
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & { tags?: WsTag[] };
presentSecretVersionNumber: number;
hasMerged?: Boolean;
@@ -91,11 +91,11 @@ export const SecretApprovalRequestChangeItem = ({
<TBody>
<Tr>
<Td className="text-red-600">OLD</Td>
<Td>{secretVersion?.key}</Td>
<Td>{secretVersion?.secretKey}</Td>
<Td>
<SecretInput isReadOnly value={secretVersion?.value} />
<SecretInput isReadOnly value={secretVersion?.secretValue} />
</Td>
<Td>{secretVersion?.comment}</Td>
<Td>{secretVersion?.secretComment}</Td>
<Td>
{secretVersion?.tags?.map(({ name, id: tagId, color }) => (
<Tag
@@ -137,17 +137,23 @@ export const SecretApprovalRequestChangeItem = ({
) : (
<TBody>
<Tr>
<Td>{op === CommitType.CREATE ? newVersion?.secretKey : secretVersion?.key}</Td>
<Td>
{op === CommitType.CREATE ? newVersion?.secretKey : secretVersion?.secretKey}
</Td>
<Td>
<SecretInput
isReadOnly
value={
op === CommitType.CREATE ? newVersion?.secretValue : secretVersion?.value
op === CommitType.CREATE
? newVersion?.secretValue
: secretVersion?.secretValue
}
/>
</Td>
<Td>
{op === CommitType.CREATE ? newVersion?.secretComment : secretVersion?.comment}
{op === CommitType.CREATE
? newVersion?.secretComment
: secretVersion?.secretComment}
</Td>
<Td>
{(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map(
@@ -230,7 +230,7 @@ export const SecretApprovalRequestChanges = ({
</div>
<div className="flex flex-col space-y-4">
{secretApprovalRequestDetails.commits.map(
({ op, secretVersion, secret, newVersion }, index) => (
({ op, secretVersion, secret, ...newVersion }, index) => (
<SecretApprovalRequestChangeItem
op={op}
conflicts={secretApprovalRequestDetails.conflicts}
@@ -269,8 +269,9 @@ export const SecretApprovalRequestChanges = ({
>
<div className="flex-grow text-sm">
<Tooltip
content={`${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""
}`}
content={`${requiredApprover.firstName || ""} ${
requiredApprover.lastName || ""
}`}
>
<span>{requiredApprover?.email} </span>
</Tooltip>
@@ -23,13 +23,12 @@ import {
useGetProjectSecrets,
useGetSecretApprovalPolicyOfABoard,
useGetSecretImports,
useGetUserWsKey,
useGetWorkspaceSnapshotList,
useGetWsSnapshotCount,
useGetWsTags
} from "@app/hooks/api";
import { ProjectIndexSecretsSection } from "../SecretOverviewPage/components/ProjectIndexSecretsSection";
import { SecretV2MigrationSection } from "../SecretOverviewPage/components/SecretV2MigrationSection";
import { ActionBar } from "./components/ActionBar";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { DynamicSecretListView } from "./components/DynamicSecretListView";
@@ -93,14 +92,11 @@ export const SecretMainPage = () => {
}
}, [isWorkspaceLoading, currentWorkspace, environment, router.isReady]);
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
// fetch secrets
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
environment,
workspaceId,
secretPath,
decryptFileKey: decryptFileKey!,
options: {
enabled: canReadSecret
}
@@ -131,7 +127,6 @@ export const SecretMainPage = () => {
const { data: importedSecrets } = useGetImportedSecretsSingleEnv({
projectId: workspaceId,
environment,
decryptFileKey: decryptFileKey!,
path: secretPath,
options: {
enabled: canReadSecret
@@ -237,6 +232,7 @@ export const SecretMainPage = () => {
return (
<StoreProvider>
<div className="container mx-auto flex h-full flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<SecretV2MigrationSection />
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
@@ -250,12 +246,10 @@ export const SecretMainPage = () => {
protectionPolicyName={boardPolicy?.name}
/>
</div>
<ProjectIndexSecretsSection decryptFileKey={decryptFileKey!} />
{!isRollbackMode ? (
<>
<ActionBar
secrets={secrets}
importedSecrets={importedSecrets}
environment={environment}
workspaceId={workspaceId}
projectSlug={projectSlug}
@@ -333,7 +327,6 @@ export const SecretMainPage = () => {
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
decryptFileKey={decryptFileKey!}
isProtectedBranch={isProtectedBranch}
/>
)}
@@ -343,7 +336,6 @@ export const SecretMainPage = () => {
<CreateSecretForm
environment={environment}
workspaceId={workspaceId}
decryptFileKey={decryptFileKey!}
secretPath={secretPath}
autoCapitalize={currentWorkspace?.autoCapitalization}
isProtectedBranch={isProtectedBranch}
@@ -352,7 +344,6 @@ export const SecretMainPage = () => {
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
decryptFileKey={decryptFileKey!}
secretPath={secretPath}
isSmaller={isNotEmtpy}
environments={currentWorkspace?.environments}
@@ -372,7 +363,6 @@ export const SecretMainPage = () => {
) : (
<SnapshotView
snapshotId={snapshotId || ""}
decryptFileKey={decryptFileKey!}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
@@ -25,7 +25,6 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { decryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
import {
Button,
DeleteActionModal,
@@ -46,15 +45,10 @@ import {
UpgradePlanModal
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
import { interpolateSecrets } from "@app/helpers/secret";
import { usePopUp } from "@app/hooks";
import {
useCreateFolder,
useDeleteSecretBatch,
useGetUserWsKey,
useMoveSecrets
} from "@app/hooks/api";
import { DecryptedSecret, SecretType, TImportedSecrets, WsTag } from "@app/hooks/api/types";
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
import { SecretType, SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { debounce } from "@app/lib/fn/debounce";
import {
@@ -70,9 +64,8 @@ import { FolderForm } from "./FolderForm";
import { MoveSecretsModal } from "./MoveSecretsModal";
type Props = {
secrets?: DecryptedSecret[];
secrets?: SecretV3RawSanitized[];
// swtich the secrets type as it gets decrypted after api call
importedSecrets?: Array<Omit<TImportedSecrets, "secrets"> & { secrets: DecryptedSecret[] }>;
environment: string;
// @depreciated will be moving all these details to zustand
workspaceId: string;
@@ -92,7 +85,6 @@ type Props = {
export const ActionBar = ({
secrets = [],
importedSecrets = [],
environment,
workspaceId,
projectSlug,
@@ -124,7 +116,6 @@ export const ActionBar = ({
const { mutateAsync: createFolder } = useCreateFolder();
const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch();
const { mutateAsync: moveSecrets } = useMoveSecrets();
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const selectedSecrets = useSelectedSecrets();
const { reset: resetSelectedSecret } = useSelectedSecretActions();
@@ -155,58 +146,46 @@ export const ActionBar = ({
};
const handleSecretDownload = async () => {
const secPriority: Record<string, boolean> = {};
const downloadedSecrets: Array<{ key: string; value: string; comment?: string }> = [];
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const workspaceKey = decryptAssymmetric({
ciphertext: decryptFileKey!.encryptedKey,
nonce: decryptFileKey!.nonce,
publicKey: decryptFileKey!.sender.publicKey,
privateKey: PRIVATE_KEY
const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({
workspaceId,
expandSecretReferences: true,
includeImports: true,
environment,
secretPath
});
const expandSecrets = interpolateSecrets({
projectId: workspaceId,
secretEncKey: workspaceKey
});
const secretRecord: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean }
> = {};
// load up secrets in dashboard
secrets?.forEach(({ key, value, valueOverride, comment }) => {
secPriority[key] = true;
downloadedSecrets.push({ key, value: valueOverride || value, comment });
});
// now load imported secrets with secPriority
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
importedSecrets[i].secrets.forEach(({ key, value, valueOverride, comment }) => {
if (secPriority?.[key]) return;
downloadedSecrets.unshift({ key, value: valueOverride || value, comment });
secPriority[key] = true;
const secretsPicked = new Set<string>();
const secretsToDownload: { key: string; value?: string; comment?: string }[] = [];
localSecrets.forEach((el) => {
secretsPicked.add(el.secretKey);
secretsToDownload.push({
key: el.secretKey,
value: el.secretValue,
comment: el.secretComment
});
});
for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) {
const secret = localImportedSecrets[i].secrets[j];
if (!secretsPicked.has(secret.secretKey)) {
secretsToDownload.push({
key: secret.secretKey,
value: secret.secretValue,
comment: secret.secretComment
});
}
secretsPicked.add(secret.secretKey);
}
}
downloadedSecrets.forEach((secret) => {
secretRecord[secret.key] = {
value: secret.value,
comment: secret.comment
};
});
await expandSecrets(secretRecord);
const file = downloadedSecrets
const file = secretsToDownload
.sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
.reduce(
(prev, { key, comment }, index) =>
(prev, { key, comment, value }, index) =>
prev +
(comment
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${secretRecord[key].value}\n`
: `${key}=${secretRecord[key].value}\n`),
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
: `${key}=${value}\n`),
""
);
@@ -221,7 +200,7 @@ export const ActionBar = ({
secretPath,
workspaceId,
environment,
secrets: bulkDeletedSecrets.map(({ key }) => ({ secretName: key, type: SecretType.Shared }))
secrets: bulkDeletedSecrets.map(({ key }) => ({ secretKey: key, type: SecretType.Shared }))
});
resetSelectedSecret();
handlePopUpClose("bulkDeleteSecrets");
@@ -6,7 +6,7 @@ import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { useCreateSecretV3 } from "@app/hooks/api";
import { SecretType, UserWsKeyPair } from "@app/hooks/api/types";
import { SecretType } from "@app/hooks/api/types";
import { PopUpNames, usePopUpAction, usePopUpState } from "../../SecretMainPage.store";
@@ -20,7 +20,6 @@ type TFormSchema = z.infer<typeof typeSchema>;
type Props = {
environment: string;
workspaceId: string;
decryptFileKey: UserWsKeyPair;
secretPath?: string;
// modal props
autoCapitalize?: boolean;
@@ -30,7 +29,6 @@ type Props = {
export const CreateSecretForm = ({
environment,
workspaceId,
decryptFileKey,
secretPath = "/",
autoCapitalize = true,
isProtectedBranch = false
@@ -53,11 +51,10 @@ export const CreateSecretForm = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretValue: value || "",
secretComment: "",
type: SecretType.Shared,
latestFileKey: decryptFileKey
type: SecretType.Shared
});
closePopUp(PopUpNames.CreateSecretForm);
reset();
@@ -32,7 +32,6 @@ import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api";
import { UserWsKeyPair } from "@app/hooks/api/types";
const formSchema = z.object({
environment: z.string().trim(),
@@ -54,7 +53,6 @@ type Props = {
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
environments?: { name: string; slug: string }[];
workspaceId: string;
decryptFileKey: UserWsKeyPair;
environment: string;
secretPath: string;
};
@@ -62,7 +60,6 @@ type Props = {
export const CopySecretsFromBoard = ({
environments = [],
workspaceId,
decryptFileKey,
environment,
secretPath,
isOpen,
@@ -93,7 +90,6 @@ export const CopySecretsFromBoard = ({
workspaceId,
environment: selectedEnvSlug,
secretPath: debouncedEnvCopySecretPath,
decryptFileKey,
options: {
enabled:
Boolean(workspaceId) &&
@@ -16,7 +16,7 @@ import { usePopUp, useToggle } from "@app/hooks";
import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { DecryptedSecret, SecretType, UserWsKeyPair } from "@app/hooks/api/types";
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types";
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
@@ -40,10 +40,9 @@ type Props = {
isSmaller: boolean;
environments?: { name: string; slug: string }[];
workspaceId: string;
decryptFileKey: UserWsKeyPair;
environment: string;
secretPath: string;
secrets?: DecryptedSecret[];
secrets?: SecretV3RawSanitized[];
isProtectedBranch?: boolean;
};
@@ -51,7 +50,6 @@ export const SecretDropzone = ({
isSmaller,
environments = [],
workspaceId,
decryptFileKey,
environment,
secretPath,
secrets = [],
@@ -165,29 +163,27 @@ export const SecretDropzone = ({
try {
if (Object.keys(create || {}).length) {
await createSecretBatch({
latestFileKey: decryptFileKey!,
secretPath,
workspaceId,
environment,
secrets: Object.entries(create).map(([secretName, secData]) => ({
secrets: Object.entries(create).map(([secretKey, secData]) => ({
type: SecretType.Shared,
secretComment: secData.comments.join("\n"),
secretValue: secData.value,
secretName
secretKey
}))
});
}
if (Object.keys(update || {}).length) {
await updateSecretBatch({
latestFileKey: decryptFileKey!,
secretPath,
workspaceId,
environment,
secrets: Object.entries(update).map(([secretName, secData]) => ({
secrets: Object.entries(update).map(([secretKey, secData]) => ({
type: SecretType.Shared,
secretComment: secData.comments.join("\n"),
secretValue: secData.value,
secretName
secretKey
}))
});
}
@@ -281,7 +277,6 @@ export const SecretDropzone = ({
environment={environment}
environments={environments}
workspaceId={workspaceId}
decryptFileKey={decryptFileKey}
secretPath={secretPath}
isSmaller={isSmaller}
/>
@@ -19,13 +19,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
SecretInput,
TableContainer,
Tooltip
} from "@app/components/v2";
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { useResyncSecretReplication } from "@app/hooks/api";
@@ -37,7 +31,11 @@ type Props = {
secretPath?: string;
secretImport?: TSecretImport;
isReplicationExpand?: boolean;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
importedSecrets: {
key: string;
value?: string;
overriden: { env: string; secretPath: string };
}[];
searchTerm: string;
onExpandReplicateSecrets: (id: string) => void;
};
@@ -45,9 +43,9 @@ type Props = {
// to show the environment and folder icon
export const EnvFolderIcon = ({
env,
secretPath,
// isReplication
}: {
secretPath
}: // isReplication
{
env: string;
secretPath: string;
// isReplication?: boolean;
@@ -18,7 +18,7 @@ import { usePopUp } from "@app/hooks";
import { useDeleteSecretImport, useUpdateSecretImport } from "@app/hooks/api";
import { ReservedFolders } from "@app/hooks/api/secretFolders/types";
import { TSecretImport } from "@app/hooks/api/secretImports/types";
import { DecryptedSecret, WorkspaceEnv } from "@app/hooks/api/types";
import { SecretV3RawSanitized, WorkspaceEnv } from "@app/hooks/api/types";
import { formatReservedPaths } from "@app/lib/fn/string";
import { SecretImportItem } from "./SecretImportItem";
@@ -29,14 +29,14 @@ type TImportedSecrets = Array<{
environmentInfo: WorkspaceEnv;
secretPath: string;
folderId: string;
secrets: DecryptedSecret[];
secrets: SecretV3RawSanitized[];
}>;
export const computeImportedSecretRows = (
importedSecEnv: string,
importedSecPath: string,
importSecrets: TImportedSecrets = [],
secrets: DecryptedSecret[] = []
secrets: SecretV3RawSanitized[] = []
) => {
const importedSecIndex = importSecrets.findIndex(
({ secretPath, environmentInfo }) =>
@@ -63,7 +63,7 @@ export const computeImportedSecretRows = (
const importedEntry: Record<string, boolean> = {};
const importedSecretEntries: {
key: string;
value: string;
value?: string;
overriden: {
env: string;
secretPath: string;
@@ -90,7 +90,7 @@ type Props = {
secretPath?: string;
secretImports?: TSecretImport[];
isFetching?: boolean;
secrets?: DecryptedSecret[];
secrets?: SecretV3RawSanitized[];
importedSecrets?: TImportedSecrets;
searchTerm: string;
};
@@ -37,7 +37,7 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api";
import { DecryptedSecret, UserWsKeyPair, WsTag } from "@app/hooks/api/types";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@@ -48,12 +48,11 @@ type Props = {
secretPath: string;
onToggle: (isOpen: boolean) => void;
onClose: () => void;
secret: DecryptedSecret;
decryptFileKey: UserWsKeyPair;
secret: SecretV3RawSanitized;
onDeleteSecret: () => void;
onSaveSecret: (
orgSec: DecryptedSecret,
modSec: Omit<DecryptedSecret, "tags"> & { tags: { id: string }[] },
orgSec: SecretV3RawSanitized,
modSec: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
cb?: () => void
) => Promise<void>;
tags: WsTag[];
@@ -64,7 +63,6 @@ type Props = {
export const SecretDetailSidebar = ({
isOpen,
onToggle,
decryptFileKey,
secret,
onDeleteSecret,
onSaveSecret,
@@ -101,7 +99,7 @@ export const SecretDetailSidebar = ({
control,
name: "tags"
});
const selectedTags = watch("tags", []);
const selectedTags = watch("tags", []) || [];
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.id]: true }),
{}
@@ -114,8 +112,7 @@ export const SecretDetailSidebar = ({
const { data: secretVersion } = useGetSecretVersion({
limit: 10,
offset: 0,
secretId: secret?.id,
decryptFileKey
secretId: secret?.id
});
const handleOverrideClick = () => {
@@ -420,7 +417,12 @@ export const SecretDetailSidebar = ({
className="px-2 py-1"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => handleSecretShare(secret.valueOverride ?? secret.value)}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share Secret
</Button>
@@ -428,7 +430,7 @@ export const SecretDetailSidebar = ({
<div className="dark mt-4 mb-4 flex-grow text-sm text-bunker-300">
<div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, value, id }, i) => (
{secretVersion?.map(({ createdAt, secretValue, id }, i) => (
<div key={id} className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<div>
@@ -438,7 +440,7 @@ export const SecretDetailSidebar = ({
</div>
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
<div className="break-all font-mono">{value}</div>
<div className="break-all font-mono">{secretValue}</div>
</div>
</div>
))}
@@ -28,7 +28,7 @@ import {
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WsTag } from "@app/hooks/api/types";
import { subject } from "@casl/ability";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -46,14 +46,14 @@ import {
} from "./SecretListView.utils";
type Props = {
secret: DecryptedSecret;
secret: SecretV3RawSanitized;
onSaveSecret: (
orgSec: DecryptedSecret,
modSec: Omit<DecryptedSecret, "tags"> & { tags: { id: string }[] },
orgSec: SecretV3RawSanitized,
modSec: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
cb?: () => void
) => Promise<void>;
onDeleteSecret: (sec: DecryptedSecret) => void;
onDetailViewSecret: (sec: DecryptedSecret) => void;
onDeleteSecret: (sec: SecretV3RawSanitized) => void;
onDetailViewSecret: (sec: SecretV3RawSanitized) => void;
isVisible?: boolean;
isSelected?: boolean;
onToggleSecretSelect: (id: string) => void;
@@ -113,7 +113,7 @@ export const SecretItem = memo(
const overrideAction = watch("overrideAction");
const hasComment = Boolean(watch("comment"));
const selectedTags = watch("tags", []);
const selectedTags = watch("tags", []) || [];
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.id]: true }),
{}
@@ -10,9 +10,9 @@ import { usePopUp } from "@app/hooks";
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { DecryptedSecret, SecretType } from "@app/hooks/api/secrets/types";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { secretSnapshotKeys } from "@app/hooks/api/secretSnapshots/queries";
import { UserWsKeyPair, WsTag } from "@app/hooks/api/types";
import { WsTag } from "@app/hooks/api/types";
import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal";
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
@@ -22,10 +22,9 @@ import { SecretItem } from "./SecretItem";
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
type Props = {
secrets?: DecryptedSecret[];
secrets?: SecretV3RawSanitized[];
environment: string;
workspaceId: string;
decryptFileKey: UserWsKeyPair;
secretPath?: string;
filter: Filter;
sortDir?: SortDir;
@@ -34,8 +33,8 @@ type Props = {
isProtectedBranch?: boolean;
};
const reorderSecretGroupByUnderscore = (secrets: DecryptedSecret[], sortDir: SortDir) => {
const groupedSecrets: Record<string, DecryptedSecret[]> = {};
const reorderSecretGroupByUnderscore = (secrets: SecretV3RawSanitized[], sortDir: SortDir) => {
const groupedSecrets: Record<string, SecretV3RawSanitized[]> = {};
secrets.forEach((secret) => {
const lastSeperatorIndex = secret.key.lastIndexOf("_");
const namespace =
@@ -53,7 +52,11 @@ const reorderSecretGroupByUnderscore = (secrets: DecryptedSecret[], sortDir: Sor
.map((namespace) => ({ namespace, secrets: groupedSecrets[namespace] }));
};
const reorderSecret = (secrets: DecryptedSecret[], sortDir: SortDir, filter?: GroupBy | null) => {
const reorderSecret = (
secrets: SecretV3RawSanitized[],
sortDir: SortDir,
filter?: GroupBy | null
) => {
if (filter === GroupBy.PREFIX) {
return reorderSecretGroupByUnderscore(secrets, sortDir);
}
@@ -70,13 +73,13 @@ const reorderSecret = (secrets: DecryptedSecret[], sortDir: SortDir, filter?: Gr
];
};
export const filterSecrets = (secrets: DecryptedSecret[], filter: Filter) =>
export const filterSecrets = (secrets: SecretV3RawSanitized[], filter: Filter) =>
secrets.filter(({ key, value, tags }) => {
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
const searchTerm = filter.searchFilter.toLowerCase();
return (
(!isTagFilterActive || tags.some(({ id }) => filter.tags?.[id])) &&
(key.toLowerCase().includes(searchTerm) || value.toLowerCase().includes(searchTerm))
(!isTagFilterActive || tags?.some(({ id }) => filter.tags?.[id])) &&
(key.toLowerCase().includes(searchTerm) || value?.toLowerCase().includes(searchTerm))
);
});
@@ -84,7 +87,6 @@ export const SecretListView = ({
secrets = [],
environment,
workspaceId,
decryptFileKey,
secretPath = "/",
filter,
sortDir = SortDir.ASC,
@@ -148,7 +150,7 @@ export const SecretListView = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
type,
secretId
});
@@ -160,12 +162,10 @@ export const SecretListView = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretId,
secretKey: key,
secretValue: value || "",
type,
latestFileKey: decryptFileKey,
tags,
tagIds: tags,
secretComment: comment,
secretReminderRepeatDays: reminderRepeatDays,
secretReminderNote: reminderNote,
@@ -180,12 +180,11 @@ export const SecretListView = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretValue: value || "",
secretComment: "",
skipMultilineEncoding,
type,
latestFileKey: decryptFileKey
type
},
{}
);
@@ -193,8 +192,8 @@ export const SecretListView = ({
const handleSaveSecret = useCallback(
async (
orgSecret: DecryptedSecret,
modSecret: Omit<DecryptedSecret, "tags"> & { tags: { id: string }[] },
orgSecret: SecretV3RawSanitized,
modSecret: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
cb?: () => void
) => {
const { key: oldKey } = orgSecret;
@@ -288,7 +287,7 @@ export const SecretListView = ({
);
const handleSecretDelete = useCallback(async () => {
const { key, id: secretId } = popUp.deleteSecret?.data as DecryptedSecret;
const { key, id: secretId } = popUp.deleteSecret?.data as SecretV3RawSanitized;
try {
await handleSecretOperation("delete", SecretType.Shared, key, { secretId });
// wrap this in another function and then reuse
@@ -317,16 +316,16 @@ export const SecretListView = ({
text: "Failed to delete secret"
});
}
}, [(popUp.deleteSecret?.data as DecryptedSecret)?.key, environment, secretPath]);
}, [(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key, environment, secretPath]);
// for optimization on minimise re-rendering of secret items
const onCreateTag = useCallback(() => handlePopUpOpen("createTag"), []);
const onDeleteSecret = useCallback(
(sec: DecryptedSecret) => handlePopUpOpen("deleteSecret", sec),
(sec: SecretV3RawSanitized) => handlePopUpOpen("deleteSecret", sec),
[]
);
const onDetailViewSecret = useCallback(
(sec: DecryptedSecret) => handlePopUpOpen("secretDetail", sec),
(sec: SecretV3RawSanitized) => handlePopUpOpen("secretDetail", sec),
[]
);
@@ -380,7 +379,7 @@ export const SecretListView = ({
)}
<DeleteActionModal
isOpen={popUp.deleteSecret.isOpen}
deleteKey={(popUp.deleteSecret?.data as DecryptedSecret)?.key}
deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
title="Do you want to delete this secret?"
onChange={(isOpen) => handlePopUpToggle("deleteSecret", isOpen)}
onDeleteApproved={handleSecretDelete}
@@ -391,8 +390,7 @@ export const SecretListView = ({
secretPath={secretPath}
isOpen={popUp.secretDetail.isOpen}
onToggle={(isOpen) => handlePopUpToggle("secretDetail", isOpen)}
decryptFileKey={decryptFileKey}
secret={popUp.secretDetail.data as DecryptedSecret}
secret={popUp.secretDetail.data as SecretV3RawSanitized}
onDeleteSecret={() => handlePopUpOpen("deleteSecret", popUp.secretDetail.data)}
onClose={() => handlePopUpClose("secretDetail")}
onSaveSecret={handleSaveSecret}
@@ -23,7 +23,10 @@ export enum SecretActionType {
export const formSchema = z.object({
key: z.string().trim().min(1, { message: "Secret key is required" }),
value: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
value: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.optional(),
idOverride: z.string().trim().optional(),
valueOverride: z
.string()
@@ -52,6 +55,7 @@ export const formSchema = z.object({
})
.array()
.default([])
.optional()
});
export type TFormSchema = z.infer<typeof formSchema>;
@@ -21,7 +21,7 @@ import {
Tr
} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { DecryptedSecret } from "@app/hooks/api/types";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
export enum TDiffModes {
NoChange = "no change",
@@ -32,8 +32,8 @@ export enum TDiffModes {
type Props = {
mode: TDiffModes;
preSecret?: DecryptedSecret;
postSecret: DecryptedSecret;
preSecret?: SecretV3RawSanitized;
postSecret: SecretV3RawSanitized;
};
export type TDiffView<T> = {
mode: TDiffModes;
@@ -120,9 +120,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<Td className="border-r border-mineshaft-600">Value</Td>
{isModified && (
<Td className="border-r border-mineshaft-600">
<SecretInput
value={preSecret?.value}
/>
<SecretInput value={preSecret?.value} />
</Td>
)}
<Td>
@@ -14,7 +14,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, ContentLoader, Input, Tag, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useGetSnapshotSecrets, usePerformSecretRollback } from "@app/hooks/api";
import { DecryptedSecret, TSecretFolder, UserWsKeyPair } from "@app/hooks/api/types";
import { SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
import { renderIcon, SecretItem, TDiffModes, TDiffView } from "./SecretItem";
@@ -23,8 +23,7 @@ type Props = {
environment: string;
workspaceId: string;
secretPath?: string;
decryptFileKey: UserWsKeyPair;
secrets?: DecryptedSecret[];
secrets?: SecretV3RawSanitized[];
folders?: TSecretFolder[];
snapshotCount?: number;
onGoBack: () => void;
@@ -33,7 +32,7 @@ type Props = {
const LOADER_TEXT = ["Fetching your snapshot", "Creating the difference view"];
const deepCompareSecrets = (lhs: DecryptedSecret, rhs: DecryptedSecret) =>
const deepCompareSecrets = (lhs: SecretV3RawSanitized, rhs: SecretV3RawSanitized) =>
lhs.key === rhs.key &&
lhs.value === rhs.value &&
lhs.comment === rhs.comment &&
@@ -45,21 +44,18 @@ export const SnapshotView = ({
environment,
workspaceId,
secretPath,
decryptFileKey,
secrets = [],
folders = [],
onGoBack,
snapshotCount,
onClickListSnapshot
}: Props) => {
const [search, setSearch] = useState("");
const { mutateAsync: performRollback, isLoading: isRollingBack } = usePerformSecretRollback();
const { data: snapshotData, isLoading: isSnapshotLoading } = useGetSnapshotSecrets({
snapshotId,
env: environment,
decryptFileKey
env: environment
});
const rollingFolder = snapshotData?.folders || [];
@@ -92,11 +88,11 @@ export const SnapshotView = ({
}, [folders, rollingFolder]);
const secretDiffView = useMemo(() => {
const secretGroupById = secrets.reduce<Record<string, DecryptedSecret>>(
const secretGroupById = secrets.reduce<Record<string, SecretV3RawSanitized>>(
(prev, curr) => ({ ...prev, [curr.id]: curr }),
{}
);
const diffView: Array<TDiffView<DecryptedSecret>> = [];
const diffView: Array<TDiffView<SecretV3RawSanitized>> = [];
rollingSecrets.forEach((rollSecret) => {
const { id } = rollSecret;
const doesExist = Boolean(secretGroupById?.[id]);
@@ -59,7 +59,6 @@ import {
useGetFoldersByEnv,
useGetImportedSecretsAllEnvs,
useGetProjectSecretsAllEnv,
useGetUserWsKey,
useUpdateSecretV3
} from "@app/hooks/api";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
@@ -70,10 +69,10 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
import { SecretV2MigrationSection } from "./components/SecretV2MigrationSection";
import { SelectionPanel } from "./components/SelectionPanel/SelectionPanel";
export enum EntryType {
@@ -103,7 +102,6 @@ export const SecretOverviewPage = () => {
const { currentOrg } = useOrganization();
const workspaceId = currentWorkspace?.id as string;
const projectSlug = currentWorkspace?.slug as string;
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
const [searchFilter, setSearchFilter] = useState("");
const secretPath = (router.query?.secretPath as string) || "/";
@@ -178,8 +176,7 @@ export const SecretOverviewPage = () => {
} = useGetProjectSecretsAllEnv({
workspaceId,
envs: userAvailableEnvs.map(({ slug }) => slug),
secretPath,
decryptFileKey: latestFileKey!
secretPath
});
const { folders, folderNames, isFolderPresentInEnv, getFolderByNameAndEnv } = useGetFoldersByEnv({
@@ -190,7 +187,6 @@ export const SecretOverviewPage = () => {
const { isImportedSecretPresentInEnv, getImportedSecretByKey } = useGetImportedSecretsAllEnvs({
projectId: workspaceId,
decryptFileKey: latestFileKey!,
path: secretPath,
environments: userAvailableEnvs.map(({ slug }) => slug)
});
@@ -317,11 +313,10 @@ export const SecretOverviewPage = () => {
environment: env,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretValue: value,
secretComment: "",
type: SecretType.Shared,
latestFileKey: latestFileKey!
type: SecretType.Shared
});
createNotification({
type: "success",
@@ -348,19 +343,16 @@ export const SecretOverviewPage = () => {
env: string,
key: string,
value: string,
type = SecretType.Shared,
secretId?: string
type = SecretType.Shared
) => {
try {
await updateSecretV3({
environment: env,
workspaceId,
secretPath,
secretId,
secretName: key,
secretKey: key,
secretValue: value,
type,
latestFileKey: latestFileKey!
type
});
createNotification({
type: "success",
@@ -381,7 +373,7 @@ export const SecretOverviewPage = () => {
environment: env,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretId,
type: SecretType.Shared
});
@@ -481,7 +473,7 @@ export const SecretOverviewPage = () => {
return (
<>
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
<SecretV2MigrationSection />
<div className="relative right-5 ml-4">
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
</div>
@@ -861,7 +853,6 @@ export const SecretOverviewPage = () => {
getSecretByKey={getSecretByKey}
onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
decryptFileKey={latestFileKey!}
/>
<Modal
isOpen={popUp.addFolder.isOpen}
@@ -18,7 +18,7 @@ import {
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { useWorkspace } from "@app/context";
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { DecryptedSecret, SecretType, UserWsKeyPair } from "@app/hooks/api/types";
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types";
const typeSchema = z
.object({
@@ -34,8 +34,7 @@ type TFormSchema = z.infer<typeof typeSchema>;
type Props = {
secretPath?: string;
decryptFileKey: UserWsKeyPair;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
// modal props
isOpen?: boolean;
onClose: () => void;
@@ -44,7 +43,6 @@ type Props = {
export const CreateSecretForm = ({
secretPath = "/",
decryptFileKey,
isOpen,
getSecretByKey,
onClose,
@@ -101,10 +99,9 @@ export const CreateSecretForm = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretValue: value || "",
type: SecretType.Shared,
latestFileKey: decryptFileKey
type: SecretType.Shared
});
}
@@ -112,11 +109,10 @@ export const CreateSecretForm = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretValue: value || "",
secretComment: "",
type: SecretType.Shared,
latestFileKey: decryptFileKey
type: SecretType.Shared
});
});
@@ -1,109 +0,0 @@
import { ProjectPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Button, Spinner } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetWorkspaceIndexStatus, useNameWorkspaceSecrets } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { UserWsKeyPair } from "@app/hooks/api/types";
import { fetchWorkspaceSecrets } from "@app/hooks/api/workspace/queries";
// TODO: add check so that this only shows up if user is
// an admin in the workspace
type Props = {
decryptFileKey: UserWsKeyPair;
};
export const ProjectIndexSecretsSection = ({ decryptFileKey }: Props) => {
const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission();
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
currentWorkspace?.id ?? ""
);
const [isIndexing, setIsIndexing] = useToggle();
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
const onEnableBlindIndices = async () => {
if (!currentWorkspace?.id) return;
setIsIndexing.on();
try {
const encryptedSecrets = await fetchWorkspaceSecrets(currentWorkspace.id);
const key = decryptAssymmetric({
ciphertext: decryptFileKey.encryptedKey,
nonce: decryptFileKey.nonce,
publicKey: decryptFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
const secretName = decryptSymmetric({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag,
key
});
return {
secretName,
secretId: encryptedSecret.id
};
});
await nameWorkspaceSecrets.mutateAsync({
workspaceId: currentWorkspace.id,
secretsToUpdate
});
} catch (err) {
console.log(err);
} finally {
setIsIndexing.off();
}
};
// for non admin this would throw an error
// so no need to render
return !isBlindIndexedLoading && typeof isBlindIndexed === "boolean" && !isBlindIndexed ? (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
{isIndexing && (
<div className="absolute top-0 left-0 z-50 flex h-screen w-screen items-center justify-center bg-bunker-500 bg-opacity-80">
<Spinner size="lg" className="text-primary" />
<div className="ml-4 flex flex-col space-y-1">
<div className="text-3xl font-medium">Please wait</div>
<span className="inline-block">Re-indexing your secrets...</span>
</div>
</div>
)}
<p className="mb-2 text-lg font-semibold">Action Required</p>
<p className="mb-4 leading-7 text-gray-400">
Your project was created before the introduction of blind indexing. To continue accessing
secrets by name through the SDK, public API and web dashboard, please enable blind indexing.{" "}
<b>
{membership.role !== ProjectMembershipRole.Admin && "This is an admin only operation."}
</b>
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Button
onClick={onEnableBlindIndices}
isDisabled={!isAllowed || membership.role !== ProjectMembershipRole.Admin}
color="mineshaft"
type="submit"
isLoading={isIndexing}
>
Enable Blind Indexing
</Button>
)}
</ProjectPermissionCan>
</div>
) : (
<div />
);
};
@@ -1 +0,0 @@
export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection";
@@ -13,7 +13,7 @@ import { twMerge } from "tailwind-merge";
import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { DecryptedSecret, SecretType } from "@app/hooks/api/secrets/types";
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
import { SecretEditRow } from "./SecretEditRow";
@@ -26,7 +26,7 @@ type Props = {
expandableColWidth: number;
isSelected: boolean;
onToggleSecretSelect: (key: string) => void;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (
env: string,
@@ -40,7 +40,7 @@ type Props = {
getImportedSecretByKey: (
env: string,
secretName: string
) => { secret?: DecryptedSecret; environmentInfo?: WorkspaceEnv } | undefined;
) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined;
};
export const SecretOverviewTableRow = ({
@@ -17,15 +17,15 @@ import {
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetUserWsKey, useUpdateSecretV3 } from "@app/hooks/api";
import { DecryptedSecret, SecretType } from "@app/hooks/api/types";
import { useUpdateSecretV3 } from "@app/hooks/api";
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types";
import { SecretActionType } from "@app/views/SecretMainPage/components/SecretListView/SecretListView.utils";
type Props = {
secretKey: string;
secretPath: string;
environments: { name: string; slug: string }[];
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
};
export const formSchema = z.object({
@@ -64,8 +64,6 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
);
const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const [isSecNameCopied, setIsSecNameCopied] = useToggle(false);
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
@@ -109,12 +107,10 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
environment: secret?.env,
workspaceId,
secretPath,
secretName: secret.key,
secretId: secret.id,
secretKey: secret.key,
secretValue: secret.value || "",
type: SecretType.Shared,
latestFileKey: decryptFileKey!,
tags: secret.tags.map((tag) => tag.id),
tagIds: secret.tags?.map((tag) => tag.id),
secretComment: secret.comment,
secretReminderRepeatDays: secret.reminderRepeatDays,
secretReminderNote: secret.reminderNote,
@@ -0,0 +1,183 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, Checkbox, Modal, ModalContent, Spinner } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useGetWorkspaceById, useMigrateProjectToV3 } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
enum ProjectUpgradeStatus {
InProgress = "IN_PROGRESS",
// Completed -> Will be null if completed. So a completed status is not needed
Failed = "FAILED"
}
const formSchema = z.object({
isCLIChecked: z.literal(true),
isOperatorChecked: z.literal(true),
shouldCloseOpenApprovals: z.literal(true)
});
export const SecretV2MigrationSection = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["migrationInfo"] as const);
const { currentWorkspace } = useWorkspace();
const { data: workspaceDetails, refetch } = useGetWorkspaceById(
// if v3 no need to fetch
currentWorkspace?.version === ProjectVersion.V3 ? "" : currentWorkspace?.id || "",
{
refetchInterval:
currentWorkspace?.upgradeStatus === ProjectUpgradeStatus.InProgress ? 3000 : false
}
);
const { membership } = useProjectPermission();
const migrateProjectToV3 = useMigrateProjectToV3();
const { handleSubmit, control, reset } = useForm({ resolver: zodResolver(formSchema) });
useEffect(() => {
if (!popUp.migrationInfo.isOpen) {
reset();
}
}, [popUp.migrationInfo.isOpen]);
const isProjectUpgraded = workspaceDetails?.version === ProjectVersion.V3;
useEffect(() => {
if (isProjectUpgraded && migrateProjectToV3.data) {
createNotification({ type: "success", text: "Project upgrade completed successfully" });
migrateProjectToV3.reset();
}
}, [isProjectUpgraded, Boolean(migrateProjectToV3.data)]);
if (isProjectUpgraded || currentWorkspace?.version === ProjectVersion.V3) return null;
const isUpgrading = workspaceDetails?.upgradeStatus === ProjectUpgradeStatus.InProgress;
const didProjectUpgradeFailed = workspaceDetails?.upgradeStatus === ProjectUpgradeStatus.Failed;
const handleMigrationSecretV2 = async () => {
try {
handlePopUpToggle("migrationInfo");
await migrateProjectToV3.mutateAsync({ workspaceId: currentWorkspace?.id || "" });
refetch();
createNotification({
text: "Project upgrade started",
type: "success"
});
} catch {
createNotification({
text: "Failed to upgrade project",
type: "error"
});
}
};
const isAdmin = membership?.roles.includes(ProjectMembershipRole.Admin);
return (
<div className="mt-4 rounded-lg border border-primary-600 bg-mineshaft-900 p-4">
{isUpgrading && (
<div className="absolute top-0 left-0 z-50 flex h-screen w-screen items-center justify-center bg-bunker-500 bg-opacity-80">
<Spinner size="lg" className="text-primary" />
<div className="ml-4 flex flex-col space-y-1">
<div className="text-3xl font-medium">Please wait</div>
<span className="inline-block">Upgrading your project...</span>
</div>
</div>
)}
<p className="mb-2 text-lg font-semibold">Action Required</p>
<p className="mb-4 leading-7 text-gray-400">
Infisical secrets engine is now 10x faster and allows you to encrypt secrets with your own
KMS. Upgrade your project to receive these improvements.
<b>{!isAdmin && "This is an admin only operation."}</b>
</p>
<Button
onClick={() => handlePopUpOpen("migrationInfo")}
isDisabled={!isAdmin || isUpgrading}
color="mineshaft"
isLoading={migrateProjectToV3.isLoading}
>
Upgrade Project
</Button>
{didProjectUpgradeFailed && (
<p className="mt-2 text-sm leading-7 text-red-400">
<FontAwesomeIcon icon={faTriangleExclamation} className="mr-2" />
Project upgrade unsuccessful. For assistance, please contact the Infisical support team.
</p>
)}
<Modal
isOpen={popUp.migrationInfo.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("migrationInfo", isOpen)}
>
<ModalContent
title="Upgrade Checklist"
subTitle="To ensure smooth transition, please ensure the following requirements are met before upgrading this project."
>
<div>
<form onSubmit={handleSubmit(handleMigrationSecretV2)}>
<div className="flex flex-col space-y-4">
<Controller
control={control}
name="isCLIChecked"
defaultValue={false}
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
<Checkbox
id="is-cli-checked"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
isError={Boolean(error?.message)}
>
Infisical CLI version is v0.25.0 or above.
</Checkbox>
)}
/>
<Controller
control={control}
name="isOperatorChecked"
defaultValue={false}
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
<Checkbox
id="is-operator-checked"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
isError={Boolean(error?.message)}
>
Infisical Kubernetes Operator version is v0.7.0 or above.
</Checkbox>
)}
/>
<Controller
control={control}
name="shouldCloseOpenApprovals"
defaultValue={false}
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
<Checkbox
id="is-approvals-checked"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
isError={Boolean(error?.message)}
>
Close/merge all open approval/access requests.
</Checkbox>
)}
/>
</div>
<div className="mt-8 flex space-x-4">
<Button type="submit">Confirm Upgrade</Button>
<Button variant="outline_bg" onClick={() => handlePopUpToggle("migrationInfo")}>
Cancel
</Button>
</div>
</form>
</div>
</ModalContent>
</Modal>
</div>
);
};
@@ -0,0 +1 @@
export { SecretV2MigrationSection } from "./SecretV2MigrationSection";
@@ -14,8 +14,8 @@ import {
import { usePopUp } from "@app/hooks";
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
import {
DecryptedSecret,
SecretType,
SecretV3RawSanitized,
TDeleteSecretBatchDTO,
TSecretFolder
} from "@app/hooks/api/types";
@@ -27,7 +27,7 @@ export enum EntryType {
type Props = {
secretPath: string;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
getFolderByNameAndEnv: (name: string, env: string) => TSecretFolder | undefined;
resetSelectedEntries: () => void;
selectedEntries: {
@@ -115,7 +115,7 @@ export const SelectionPanel = ({
return [
...accum,
{
secretName: entry.key,
secretKey: entry.key,
type: SecretType.Shared
}
];
@@ -1,4 +1,3 @@
import { useTranslation } from "react-i18next";
import Link from "next/link";
import {
faArrowsSpin,
@@ -16,12 +15,9 @@ import { formatDistance } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
IconButton,
Modal,
ModalContent,
Skeleton,
Spinner,
Table,
@@ -48,22 +44,17 @@ import {
useDeleteSecretRotation,
useGetSecretRotationProviders,
useGetSecretRotations,
useGetUserWsKey,
useGetWorkspaceBot,
useRestartSecretRotation,
useUpdateBotActiveStatus
useRestartSecretRotation
} from "@app/hooks/api";
import { TSecretRotationProviderTemplate } from "@app/hooks/api/types";
import { CreateRotationForm } from "./components/CreateRotationForm";
import { generateBotKey } from "./SecretRotationPage.utils";
export const SecretRotationPage = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
const { permission } = useProjectPermission();
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"createRotation",
"activeBot",
@@ -77,13 +68,10 @@ export const SecretRotationPage = withProjectPermission(
);
const { subscription } = useSubscription();
const { data: userWsKey } = useGetUserWsKey(workspaceId);
const { data: secretRotationProviders, isLoading: isRotationProviderLoading } =
useGetSecretRotationProviders({ workspaceId });
const { data: secretRotations, isLoading: isRotationLoading } = useGetSecretRotations({
workspaceId,
decryptFileKey: userWsKey!
workspaceId
});
const {
@@ -97,11 +85,6 @@ export const SecretRotationPage = withProjectPermission(
isLoading: isRestartingRotation
} = useRestartSecretRotation();
const { data: bot } = useGetWorkspaceBot(workspaceId);
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
const isBotActive = Boolean(bot?.isActive);
const handleDeleteRotation = async () => {
const { id } = popUp.deleteRotation.data as { id: string };
try {
@@ -142,29 +125,6 @@ export const SecretRotationPage = withProjectPermission(
}
};
const handleUserAcceptBotCondition = async () => {
const provider = popUp.activeBot?.data as TSecretRotationProviderTemplate;
try {
if (bot?.id) {
const botKey = generateBotKey(bot.publicKey, userWsKey!);
await updateBotActiveStatus({
isActive: true,
botId: bot.id,
workspaceId,
botKey
});
}
handlePopUpOpen("createRotation", provider);
handlePopUpClose("activeBot");
} catch (error) {
console.log(error);
createNotification({
type: "error",
text: "Failed to create bot"
});
}
};
const handleCreateRotation = async (provider: TSecretRotationProviderTemplate) => {
if (subscription && !subscription?.secretRotation) {
handlePopUpOpen("upgradePlan");
@@ -174,11 +134,7 @@ export const SecretRotationPage = withProjectPermission(
createNotification({ type: "error", text: "Access permission denied!!" });
return;
}
if (isBotActive) {
handlePopUpOpen("createRotation", provider);
} else {
handlePopUpOpen("activeBot", provider);
}
handlePopUpOpen("createRotation", provider);
};
return (
@@ -391,30 +347,6 @@ export const SecretRotationPage = withProjectPermission(
onToggle={(isOpen) => handlePopUpToggle("createRotation", isOpen)}
provider={(popUp.createRotation.data as TSecretRotationProviderTemplate) || {}}
/>
<Modal
isOpen={popUp.activeBot?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
>
<ModalContent
title={t("integrations.grant-access-to-secrets") as string}
footerContent={
<div className="flex items-center space-x-2">
<Button onClick={() => handleUserAcceptBotCondition()}>
{t("integrations.grant-access-button") as string}
</Button>
<Button
onClick={() => handlePopUpClose("activeBot")}
variant="outline_bg"
colorSchema="secondary"
>
Cancel
</Button>
</div>
}
>
{t("integrations.why-infisical-needs-access")}
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.deleteRotation.isOpen}
title="Are you sure want to delete this rotation?"
@@ -5,7 +5,7 @@ import { z } from "zod";
import { Button, FormControl, Input, Select, SelectItem, Spinner } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useWorkspace } from "@app/context";
import { useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
import { useGetProjectSecrets } from "@app/hooks/api";
const formSchema = z.object({
environment: z.string().trim(),
@@ -38,12 +38,10 @@ export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Pr
const secretPath = watch("secretPath");
const selectedSecrets = watch("secrets");
const { data: userWsKey } = useGetUserWsKey(workspaceId);
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
environment,
secretPath,
decryptFileKey: userWsKey!
secretPath
});
return (
@@ -0,0 +1,97 @@
import { useState } from "react";
import { faAws } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { Modal, ModalContent } from "@app/components/v2";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AwsKmsForm } from "./AwsKmsForm";
type Props = {
isOpen?: boolean;
onToggle: (isOpen: boolean) => void;
};
enum WizardSteps {
SelectProvider = "select-provider",
ProviderInputs = "provider-inputs"
}
const EXTERNAL_KMS_LIST = [
{
icon: faAws,
provider: ExternalKmsProvider.AWS,
title: "AWS KMS"
}
];
export const AddExternalKmsForm = ({ isOpen, onToggle }: Props) => {
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider);
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const handleFormReset = (state: boolean = false) => {
onToggle(state);
setWizardStep(WizardSteps.SelectProvider);
setSelectedProvider(null);
};
return (
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
<ModalContent
title="Add a Key Management System"
subTitle="Configure an external key management system (KMS)"
className="my-4"
>
<AnimatePresence exitBeforeEnter>
{wizardStep === WizardSteps.SelectProvider && (
<motion.div
key="select-type-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<div className="mb-4 text-mineshaft-300">Select a KMS Provider</div>
<div className="flex items-center space-x-4">
{EXTERNAL_KMS_LIST.map(({ icon, provider, title }) => (
<div
key={`kms-${provider}`}
className="flex h-28 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
role="button"
tabIndex={0}
onClick={() => {
setSelectedProvider(provider);
setWizardStep(WizardSteps.ProviderInputs);
}}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
setSelectedProvider(provider);
setWizardStep(WizardSteps.ProviderInputs);
}
}}
>
<FontAwesomeIcon icon={icon} size="lg" />
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
</div>
))}
</div>
</motion.div>
)}
{wizardStep === WizardSteps.ProviderInputs &&
selectedProvider === ExternalKmsProvider.AWS && (
<motion.div
key="kms-aws"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<AwsKmsForm onCancel={() => onToggle(false)} onCompleted={() => onToggle(false)} />
</motion.div>
)}
</AnimatePresence>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,273 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useAddExternalKms, useUpdateExternalKms } from "@app/hooks/api";
import {
AddExternalKmsSchema,
AddExternalKmsType,
ExternalKmsProvider,
Kms,
KmsAwsCredentialType
} from "@app/hooks/api/kms/types";
const AWS_REGIONS = [
{ name: "US East (Ohio)", slug: "us-east-2" },
{ name: "US East (N. Virginia)", slug: "us-east-1" },
{ name: "US West (N. California)", slug: "us-west-1" },
{ name: "US West (Oregon)", slug: "us-west-2" },
{ name: "Africa (Cape Town)", slug: "af-south-1" },
{ name: "Asia Pacific (Hong Kong)", slug: "ap-east-1" },
{ name: "Asia Pacific (Hyderabad)", slug: "ap-south-2" },
{ name: "Asia Pacific (Jakarta)", slug: "ap-southeast-3" },
{ name: "Asia Pacific (Melbourne)", slug: "ap-southeast-4" },
{ name: "Asia Pacific (Mumbai)", slug: "ap-south-1" },
{ name: "Asia Pacific (Osaka)", slug: "ap-northeast-3" },
{ name: "Asia Pacific (Seoul)", slug: "ap-northeast-2" },
{ name: "Asia Pacific (Singapore)", slug: "ap-southeast-1" },
{ name: "Asia Pacific (Sydney)", slug: "ap-southeast-2" },
{ name: "Asia Pacific (Tokyo)", slug: "ap-northeast-1" },
{ name: "Canada (Central)", slug: "ca-central-1" },
{ name: "Europe (Frankfurt)", slug: "eu-central-1" },
{ name: "Europe (Ireland)", slug: "eu-west-1" },
{ name: "Europe (London)", slug: "eu-west-2" },
{ name: "Europe (Milan)", slug: "eu-south-1" },
{ name: "Europe (Paris)", slug: "eu-west-3" },
{ name: "Europe (Spain)", slug: "eu-south-2" },
{ name: "Europe (Stockholm)", slug: "eu-north-1" },
{ name: "Europe (Zurich)", slug: "eu-central-2" },
{ name: "Middle East (Bahrain)", slug: "me-south-1" },
{ name: "Middle East (UAE)", slug: "me-central-1" },
{ name: "South America (Sao Paulo)", slug: "sa-east-1" },
{ name: "AWS GovCloud (US-East)", slug: "us-gov-east-1" },
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" }
];
type Props = {
onCompleted: () => void;
onCancel: () => void;
kms?: Kms;
};
export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
const {
control,
handleSubmit,
watch,
setValue,
formState: { isSubmitting }
} = useForm<AddExternalKmsType>({
resolver: zodResolver(AddExternalKmsSchema),
defaultValues: {
slug: kms?.slug,
description: kms?.description ?? "",
provider: {
type: ExternalKmsProvider.AWS,
inputs: {
credential: {
type: kms?.external?.providerInput?.credential?.type,
data: {
accessKey: kms?.external?.providerInput?.credential?.data?.accessKey,
secretKey: kms?.external?.providerInput?.credential?.data?.secretKey,
assumeRoleArn: kms?.external?.providerInput?.credential?.data?.assumeRoleArn,
externalId: kms?.external?.providerInput?.credential?.data?.externalId
}
},
awsRegion: kms?.external?.providerInput?.awsRegion,
kmsKeyId: kms?.external?.providerInput?.kmsKeyId
}
}
}
});
const { currentOrg } = useOrganization();
const { mutateAsync: addAwsExternalKms } = useAddExternalKms(currentOrg?.id!);
const { mutateAsync: updateAwsExternalKms } = useUpdateExternalKms(currentOrg?.id!);
const selectedAwsAuthType = watch("provider.inputs.credential.type");
const handleAddAwsKms = async (data: AddExternalKmsType) => {
const { slug, description, provider } = data;
try {
if (kms) {
await updateAwsExternalKms({
kmsId: kms.id,
slug,
description,
provider
});
createNotification({
text: "Successfully updated AWS External KMS",
type: "success"
});
} else {
await addAwsExternalKms({
slug,
description,
provider
});
createNotification({
text: "Successfully added AWS External KMS",
type: "success"
});
}
onCompleted();
} catch (err) {
console.error(err);
}
};
return (
<form onSubmit={handleSubmit(handleAddAwsKms)} autoComplete="off">
<Controller
control={control}
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.inputs.credential.type"
defaultValue={KmsAwsCredentialType.AssumeRole}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Authentication Mode"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue("provider.inputs.credential.data.accessKey", "");
setValue("provider.inputs.credential.data.secretKey", "");
setValue("provider.inputs.credential.data.assumeRoleArn", "");
setValue("provider.inputs.credential.data.externalId", "");
onChange(e);
}}
className="w-full"
>
<SelectItem value={KmsAwsCredentialType.AssumeRole}>AWS Assume Role</SelectItem>
<SelectItem value={KmsAwsCredentialType.AccessKey}>Access Key</SelectItem>
</Select>
</FormControl>
)}
/>
{selectedAwsAuthType === KmsAwsCredentialType.AccessKey ? (
<>
<Controller
control={control}
name="provider.inputs.credential.data.accessKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Key ID"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.inputs.credential.data.secretKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Access Key"
errorText={error?.message}
isError={Boolean(error)}
>
<Input type="password" autoComplete="new-password" placeholder="" {...field} />
</FormControl>
)}
/>
</>
) : (
<>
<Controller
control={control}
name="provider.inputs.credential.data.assumeRoleArn"
render={({ field, fieldState: { error } }) => (
<FormControl
label="IAM Role ARN For Role Assumption"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.inputs.credential.data.externalId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Assume Role External ID"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
name="provider.inputs.awsRegion"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="AWS Region" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500"
>
{AWS_REGIONS.map((awsRegion) => (
<SelectItem value={awsRegion.slug} key={`kms-aws-region-${awsRegion.slug}`}>
{awsRegion.name} ({awsRegion.slug})
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.inputs.kmsKeyId"
render={({ field, fieldState: { error } }) => (
<FormControl label="AWS KMS Key ID" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<div className="mt-6 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Save
</Button>
<Button variant="outline_bg" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
);
};
@@ -0,0 +1,218 @@
import { faAws } from "@fortawesome/free-brands-svg-icons";
import { faEllipsis, faLock, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
THead,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useGetExternalKmsList, useRemoveExternalKms } from "@app/hooks/api";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AddExternalKmsForm } from "./AddExternalKmsForm";
import { UpdateExternalKmsForm } from "./UpdateExternalKmsForm";
export const OrgEncryptionTab = withPermission(
() => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upgradePlan",
"addExternalKms",
"editExternalKms",
"removeExternalKms"
] as const);
const { data: externalKmsList, isLoading: isExternalKmsListLoading } =
useGetExternalKmsList(orgId);
const { mutateAsync: removeExternalKms } = useRemoveExternalKms(currentOrg?.id!);
const handleRemoveExternalKms = async () => {
const { kmsId } = popUp?.removeExternalKms?.data as {
kmsId: string;
};
try {
await removeExternalKms(kmsId);
createNotification({
text: "Successfully deleted external KMS",
type: "success"
});
handlePopUpToggle("removeExternalKms", false);
} catch (err) {
console.error(err);
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Key Management System (KMS)</p>
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Kms}>
{(isAllowed) => (
<Button
onClick={() => {
if (subscription && !subscription?.externalKms) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("addExternalKms");
}}
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add
</Button>
)}
</OrgPermissionCan>
</div>
<p className="mb-4 text-gray-400">
Encrypt your organization&apos;s data with external KMS.
</p>
<TableContainer>
<Table>
<THead>
<Tr>
<Td>Provider</Td>
<Td>Alias</Td>
</Tr>
</THead>
<TBody>
{isExternalKmsListLoading && <TableSkeleton columns={2} innerKey="kms-loading" />}
{!isExternalKmsListLoading && externalKmsList && externalKmsList?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No external KMS found" icon={faLock} />
</Td>
</Tr>
)}
{!isExternalKmsListLoading &&
externalKmsList?.map((kms) => (
<Tr key={kms.id}>
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{kms.externalKms.provider === ExternalKmsProvider.AWS && (
<FontAwesomeIcon icon={faAws} />
)}
<div className="ml-2">{kms.externalKms.provider.toUpperCase()}</div>
</Td>
<Td>{kms.slug}</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
an={OrgPermissionSubjects.Kms}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
if (subscription && !subscription?.externalKms) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("editExternalKms", {
kmsId: kms.id
});
}}
>
Edit
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
an={OrgPermissionSubjects.Kms}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeExternalKms", {
slug: kms.slug,
kmsId: kms.id,
provider: kms.externalKms.provider
});
}}
>
Delete
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can configure external KMS if you switch to Infisical's Enterprise plan."
/>
<AddExternalKmsForm
isOpen={popUp.addExternalKms.isOpen}
onToggle={(state) => handlePopUpToggle("addExternalKms", state)}
/>
<UpdateExternalKmsForm
isOpen={popUp.editExternalKms.isOpen}
kmsId={(popUp.editExternalKms.data as { kmsId: string })?.kmsId}
onOpenChange={(state) => handlePopUpToggle("editExternalKms", state)}
/>
<DeleteActionModal
isOpen={popUp.removeExternalKms.isOpen}
title={`Are you sure want to remove ${
(popUp?.removeExternalKms?.data as { slug: string })?.slug || ""
} from ${(popUp?.removeExternalKms?.data as { provider: string })?.provider || ""}?`}
onChange={(isOpen) => handlePopUpToggle("removeExternalKms", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleRemoveExternalKms}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Kms }
);
@@ -0,0 +1,29 @@
import { ContentLoader, Modal, ModalContent } from "@app/components/v2";
import { useGetExternalKmsById } from "@app/hooks/api";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AwsKmsForm } from "./AwsKmsForm";
type Props = {
isOpen: boolean;
kmsId: string;
onOpenChange: (state: boolean) => void;
};
export const UpdateExternalKmsForm = ({ isOpen, kmsId, onOpenChange }: Props) => {
const { data: externalKms, isLoading } = useGetExternalKmsById(kmsId);
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Edit configuration">
{isLoading && <ContentLoader />}
{externalKms?.external?.provider === ExternalKmsProvider.AWS && (
<AwsKmsForm
kms={externalKms}
onCancel={() => onOpenChange(false)}
onCompleted={() => onOpenChange(false)}
/>
)}
</ModalContent>
</Modal>
);
};
@@ -0,0 +1 @@
export { OrgEncryptionTab } from "./OrgEncryptionTab";
@@ -3,11 +3,13 @@ import { Tab } from "@headlessui/react";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
import { OrgAuthTab } from "../OrgAuthTab";
import { OrgEncryptionTab } from "../OrgEncryptionTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
const tabs = [
{ name: "General", key: "tab-org-general" },
{ name: "Security", key: "tab-org-security" },
{ name: "Encryption", key: "tab-org-encryption" },
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
];
export const OrgTabGroup = () => {
@@ -19,8 +21,9 @@ export const OrgTabGroup = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
@@ -35,6 +38,9 @@ export const OrgTabGroup = () => {
<Tab.Panel>
<OrgAuthTab />
</Tab.Panel>
<Tab.Panel>
<OrgEncryptionTab />
</Tab.Panel>
<Tab.Panel>
<AuditLogStreamsTab />
</Tab.Panel>
@@ -2,16 +2,26 @@ import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { Tab } from "@headlessui/react";
import { useWorkspace } from "@app/context";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { EncryptionTab } from "./components/EncryptionTab";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { WebhooksTab } from "./components/WebhooksTab";
const tabs = [
{ name: "General", key: "tab-project-general" },
{ name: "Webhooks", key: "tab-project-webhooks" },
];
export const ProjectSettingsPage = () => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const tabs = [
{ name: "General", key: "tab-project-general" },
{
name: "Encryption",
key: "tab-project-encryption",
isHidden: currentWorkspace?.version !== ProjectVersion.V3
},
{ name: "Webhooks", key: "tab-project-webhooks" }
];
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6">
@@ -20,24 +30,32 @@ export const ProjectSettingsPage = () => {
</div>
<Tab.Group>
<Tab.List className="mb-4 w-full border-b-2 border-mineshaft-800">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
{tabs
.filter((el) => !el.isHidden)
.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
)}
</Tab>
))}
>
{tab.name}
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<ProjectGeneralTab />
</Tab.Panel>
{currentWorkspace?.version === ProjectVersion.V3 && (
<Tab.Panel>
<EncryptionTab />
</Tab.Panel>
)}
<Tab.Panel>
<WebhooksTab />
</Tab.Panel>
@@ -1,38 +0,0 @@
import Link from "next/link";
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
import { useWorkspace } from "@app/context";
import { useGetWorkspaceBot } from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
export const E2EESection = () => {
const { currentWorkspace } = useWorkspace();
const { data: bot } = useGetWorkspaceBot(currentWorkspace?.id ?? "");
if (!currentWorkspace) return null;
return bot && currentWorkspace.version === ProjectVersion.V1 ? (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">End-to-End Encryption</p>
<UpgradeProjectAlert transparent project={currentWorkspace} />
</div>
<p className="mt-5 max-w-2xl text-sm text-gray-400">
We are updating our encryption logic to make sure that Infisical can be the most versatile
secret management platform. <br />
<br />
Upgrading the project version is required to continue receiving the latest improvements and
patches.
</p>
<Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
<a target="_blank" className="text-sm text-primary-400">
Learn more about project upgrades
</a>
</Link>
</div>
) : (
<div />
);
};
@@ -1 +0,0 @@
export { E2EESection } from "./E2EESection";
@@ -0,0 +1,352 @@
import { ChangeEvent, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import FileSaver from "file-saver";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
FormControl,
IconButton,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace
} from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useGetActiveProjectKms,
useGetExternalKmsList,
useLoadProjectKmsBackup,
useUpdateProjectKms
} from "@app/hooks/api";
import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries";
import { INTERNAL_KMS_KEY_ID, KmsType } from "@app/hooks/api/kms/types";
import { Organization, Workspace } from "@app/hooks/api/types";
const formSchema = z.object({
kmsKeyId: z.string()
});
type TForm = z.infer<typeof formSchema>;
const BackupConfirmationModal = ({
isOpen,
onOpenChange,
org,
workspace
}: {
isOpen: boolean;
onOpenChange: (state: boolean) => void;
org?: Organization;
workspace?: Workspace;
}) => {
const [isGeneratingBackup, setGeneratingBackup] = useToggle();
const downloadKmsBackup = async () => {
if (!workspace || !org) {
return;
}
setGeneratingBackup.on();
try {
const { secretManager } = await fetchProjectKmsBackup(workspace.id);
const [, , kmsFunction] = secretManager.split(".");
const file = secretManager;
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(
blob,
`kms-backup-${org.slug}-${workspace.slug}-${kmsFunction}.infisical.txt`
);
onOpenChange(false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create KMS backup",
type: "error"
});
}
setGeneratingBackup.off();
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Create KMS backup">
<p className="mb-6 text-bunker-300">
In case of interruptions with your configured external KMS, you can load a backup to set
the project&apos;s KMS back to the default Infisical KMS.
</p>
<Button onClick={downloadKmsBackup} isLoading={isGeneratingBackup}>
Generate
</Button>
<Button
onClick={() => onOpenChange(false)}
colorSchema="secondary"
variant="plain"
className="ml-4"
>
Cancel
</Button>
</ModalContent>
</Modal>
);
};
const LoadBackupModal = ({
isOpen,
onOpenChange,
org,
workspace
}: {
isOpen: boolean;
onOpenChange: (state: boolean) => void;
org?: Organization;
workspace?: Workspace;
}) => {
const fileUploadRef = useRef<HTMLInputElement>(null);
const { mutateAsync: loadKmsBackup, isLoading } = useLoadProjectKmsBackup(workspace?.id!);
const [backupContent, setBackupContent] = useState("");
const [backupFileName, setBackupFileName] = useState("");
const uploadKmsBackup = async () => {
if (!workspace || !org) {
return;
}
try {
await loadKmsBackup(backupContent);
createNotification({
text: "Successfully loaded KMS backup",
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
}
};
const parseFile = (file?: File) => {
if (!file) {
createNotification({
text: "Failed to parse uploaded file.",
type: "error"
});
return;
}
const reader = new FileReader();
reader.onload = (event) => {
if (!event?.target?.result) return;
const data = event.target.result.toString();
setBackupContent(data);
};
try {
reader.readAsText(file);
} catch (error) {
console.log(error);
}
};
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
parseFile(e.target?.files?.[0]);
setBackupFileName(e.target?.files?.[0]?.name || "");
};
return (
<Modal
isOpen={isOpen}
onOpenChange={(state: boolean) => {
setBackupContent("");
setBackupFileName("");
onOpenChange(state);
}}
>
<ModalContent title="Load KMS backup">
<p className="mb-6 text-bunker-300">
By loading a backup, the project&apos;s KMS will be switched to the default Infisical KMS.
</p>
<div className="flex justify-center">
<input
id="fileSelect"
className="hidden"
type="file"
accept=".txt"
onChange={handleFileUpload}
ref={fileUploadRef}
/>
<IconButton
className="p-10"
ariaLabel="upload-backup"
colorSchema="secondary"
onClick={() => {
fileUploadRef?.current?.click();
}}
>
<FontAwesomeIcon icon={faUpload} size="3x" />
</IconButton>
</div>
{backupFileName && (
<div className="mt-2 flex justify-center px-4 text-center">{backupFileName}</div>
)}
{backupContent && (
<Button
onClick={uploadKmsBackup}
className="mt-10 w-fit"
disabled={isLoading}
isLoading={isLoading}
>
Continue
</Button>
)}
</ModalContent>
</Modal>
);
};
export const EncryptionTab = () => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
const { data: activeKms } = useGetActiveProjectKms(currentWorkspace?.id!);
const { mutateAsync: updateProjectKms, isLoading: isUpdatingProjectKms } = useUpdateProjectKms(
currentWorkspace?.id!
);
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([
"createBackupConfirmation",
"loadBackup"
] as const);
const kmsKeyId = activeKms?.isExternal ? activeKms.id : INTERNAL_KMS_KEY_ID;
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = useForm<TForm>({
resolver: zodResolver(formSchema),
values: {
kmsKeyId
}
});
const onUpdateProjectKms = async (data: TForm) => {
try {
await updateProjectKms(
data.kmsKeyId === INTERNAL_KMS_KEY_ID
? { type: KmsType.Internal }
: { type: KmsType.External, kmsId: data.kmsKeyId }
);
createNotification({
text: "Successfully updated project KMS",
type: "success"
});
} catch (err) {
console.error(err);
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Key Management</h2>
{kmsKeyId !== INTERNAL_KMS_KEY_ID && (
<div className="space-x-2">
<Button colorSchema="secondary" onClick={() => handlePopUpOpen("loadBackup")}>
Load Backup
</Button>
<Button
colorSchema="secondary"
onClick={() => {
handlePopUpOpen("createBackupConfirmation");
}}
>
Create Backup
</Button>
</div>
)}
</div>
<p className="mb-4 text-gray-400">
Select which Key Management System to use for encrypting your project data
</p>
<div className="mb-6 max-w-md">
<form onSubmit={handleSubmit(onUpdateProjectKms)}>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Kms}>
{(isAllowed) => (
<Controller
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<Select
{...field}
isDisabled={!isAllowed || isUpdatingProjectKms}
onValueChange={onChange}
isLoading={isUpdatingProjectKms}
className="w-3/4 bg-mineshaft-600"
>
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
Default Infisical KMS
</SelectItem>
{externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="kmsKeyId"
/>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Workspace}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
isDisabled={!isAllowed || isSubmitting || !isDirty}
isLoading={isSubmitting}
>
Save
</Button>
)}
</ProjectPermissionCan>
</form>
</div>
<BackupConfirmationModal
isOpen={popUp.createBackupConfirmation.isOpen}
onOpenChange={(state: boolean) => handlePopUpToggle("createBackupConfirmation", state)}
org={currentOrg}
workspace={currentWorkspace}
/>
<LoadBackupModal
isOpen={popUp.loadBackup.isOpen}
onOpenChange={(state: boolean) => handlePopUpToggle("loadBackup", state)}
org={currentOrg}
workspace={currentWorkspace}
/>
</div>
);
};
@@ -0,0 +1 @@
export { EncryptionTab } from "./EncryptionTab";
@@ -2,7 +2,6 @@ import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { DeleteProjectSection } from "../DeleteProjectSection";
import { E2EESection } from "../E2EESection";
import { EnvironmentSection } from "../EnvironmentSection";
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
import { ProjectNameChangeSection } from "../ProjectNameChangeSection";
@@ -16,7 +15,6 @@ export const ProjectGeneralTab = () => {
<EnvironmentSection />
<SecretTagsSection />
<AutoCapitalizationSection />
<E2EESection />
<PointInTimeVersionLimitSection />
<AuditLogsRetentionSection />
<BackfillSecretReferenceSecretion />
@@ -1,7 +1,6 @@
export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
export { DeleteProjectSection } from "./DeleteProjectSection";
export { E2EESection } from "./E2EESection";
export { EnvironmentSection } from "./EnvironmentSection";
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
export { SecretTagsSection } from "./SecretTagsSection";