mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 03:31:27 +00:00
Merge pull request #2199 from Infisical/secret-engine-v2-bridge
Secret engine v2 bridge
This commit is contained in:
@@ -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>
|
||||
|
||||
+2
-21
@@ -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
|
||||
|
||||
+14
-8
@@ -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(
|
||||
|
||||
+4
-3
@@ -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}
|
||||
/>
|
||||
|
||||
+9
-11
@@ -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;
|
||||
|
||||
+5
-5
@@ -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;
|
||||
};
|
||||
|
||||
+14
-12
@@ -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}
|
||||
|
||||
+6
-10
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
-109
@@ -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";
|
||||
+3
-3
@@ -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 = ({
|
||||
|
||||
+5
-9
@@ -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,
|
||||
|
||||
+183
@@ -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?"
|
||||
|
||||
+2
-4
@@ -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 (
|
||||
|
||||
+97
@@ -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>
|
||||
);
|
||||
};
|
||||
+273
@@ -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>
|
||||
);
|
||||
};
|
||||
+218
@@ -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'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 }
|
||||
);
|
||||
+29
@@ -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>
|
||||
|
||||
-38
@@ -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";
|
||||
+352
@@ -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'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'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
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user