feature: secret sync base + aws parameter store & github

This commit is contained in:
Scott Wilson
2025-01-15 21:52:50 -08:00
parent b440e918ac
commit 473efa91f0
214 changed files with 10212 additions and 623 deletions
@@ -0,0 +1,254 @@
import { useCallback } from "react";
import {
faCheck,
faCopy,
faDownload,
faEllipsisV,
faEraser,
faInfoCircle,
faRotate,
faToggleOff,
faToggleOn,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DeleteSecretSyncModal,
SecretSyncImportSecretsModal,
SecretSyncImportStatusBadge,
SecretSyncRemoveSecretsModal,
SecretSyncRemoveStatusBadge
} from "@app/components/secret-syncs";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { usePopUp, useToggle } from "@app/hooks";
import {
TSecretSync,
useSecretSyncOption,
useTriggerSecretSyncSyncSecrets,
useUpdateSecretSync
} from "@app/hooks/api/secretSyncs";
import { IntegrationsListPageTabs } from "@app/types/integrations";
type Props = {
secretSync: TSecretSync;
};
export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"importSecrets",
"removeSecrets",
"deleteSync"
] as const);
const navigate = useNavigate();
const triggerSyncSecrets = useTriggerSecretSyncSyncSecrets();
const updateSync = useUpdateSecretSync();
const { destination } = secretSync;
const destinationName = SECRET_SYNC_MAP[destination].name;
const { syncOption } = useSecretSyncOption(destination);
const [isIdCopied, setIsIdCopied] = useToggle(false);
const handleCopyId = useCallback(() => {
setIsIdCopied.on();
navigator.clipboard.writeText(secretSync.id);
createNotification({
text: "Secret Sync ID copied to clipboard",
type: "info"
});
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
// eslint-disable-next-line consistent-return
return () => clearTimeout(timer);
}, [isIdCopied]);
const handleToggleEnableSync = async () => {
const isEnabled = !secretSync.isEnabled;
try {
await updateSync.mutateAsync({
syncId: secretSync.id,
destination: secretSync.destination,
isEnabled
});
createNotification({
text: `Successfully ${isEnabled ? "enabled" : "disabled"} ${destinationName} Sync`,
type: "success"
});
} catch {
createNotification({
text: `Failed to ${isEnabled ? "enable" : "disable"} ${destinationName} Sync`,
type: "error"
});
}
};
const handleTriggerSync = async () => {
try {
await triggerSyncSecrets.mutateAsync({
syncId: secretSync.id,
destination: secretSync.destination
});
createNotification({
text: `Successfully triggered ${destinationName} Sync`,
type: "success"
});
} catch {
createNotification({
text: `Failed to trigger ${destinationName} Sync`,
type: "error"
});
}
};
return (
<>
<div className="ml-auto mt-4 flex flex-wrap items-center justify-end gap-2">
<SecretSyncImportStatusBadge secretSync={secretSync} />
<SecretSyncRemoveStatusBadge secretSync={secretSync} />
<div>
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faRotate} />}
onClick={handleTriggerSync}
className="h-9 rounded-r-none bg-mineshaft-500"
>
Trigger Sync
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add-folder-or-import"
variant="outline_bg"
className="h-9 w-10 rounded-l-none border-l-2 border-mineshaft border-l-mineshaft-700 bg-mineshaft-500"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={(e) => {
e.stopPropagation();
handleCopyId();
}}
>
Copy Sync ID
</DropdownMenuItem>
{syncOption?.canImportSecrets && (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faDownload} />}
onClick={() => handlePopUpOpen("importSecrets")}
>
<Tooltip
position="left"
sideOffset={42}
content={`Import secrets from this ${destinationName} destination into Infisical.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Import Secrets</span>
<FontAwesomeIcon className="text-bunker-300" size="sm" icon={faInfoCircle} />
</div>
</Tooltip>
</DropdownMenuItem>
)}
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEraser} />}
onClick={() => handlePopUpOpen("removeSecrets")}
>
<Tooltip
position="left"
sideOffset={42}
content={`Remove secrets synced by Infisical from this ${destinationName} destination.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Remove Secrets</span>
<FontAwesomeIcon className="text-bunker-300" size="sm" icon={faInfoCircle} />
</div>
</Tooltip>
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={
<FontAwesomeIcon icon={secretSync.isEnabled ? faToggleOff : faToggleOn} />
}
onClick={handleToggleEnableSync}
>
{secretSync.isEnabled ? "Disable" : "Enable"} Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => handlePopUpOpen("deleteSync")}
>
Delete Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<SecretSyncImportSecretsModal
onOpenChange={(isOpen) => handlePopUpToggle("importSecrets", isOpen)}
isOpen={popUp.importSecrets.isOpen}
secretSync={secretSync}
/>
<SecretSyncRemoveSecretsModal
onOpenChange={(isOpen) => handlePopUpToggle("removeSecrets", isOpen)}
isOpen={popUp.removeSecrets.isOpen}
secretSync={secretSync}
/>
<DeleteSecretSyncModal
onOpenChange={(isOpen) => handlePopUpToggle("deleteSync", isOpen)}
isOpen={popUp.deleteSync.isOpen}
secretSync={secretSync}
onComplete={() =>
navigate({
to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
params: {
projectId: secretSync.projectId
},
search: {
selectedTab: IntegrationsListPageTabs.SecretSyncs
}
})
}
/>
</>
);
};
@@ -0,0 +1,86 @@
import { faFingerprint } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { useSubscription } from "@app/context";
import { EventType } from "@app/hooks/api/auditLogs/enums";
import { TSecretSync } from "@app/hooks/api/secretSyncs";
import { LogsSection } from "@app/pages/organization/AuditLogsPage/components/LogsSection";
// Add more events if needed
const INTEGRATION_EVENTS = [
EventType.SECRET_SYNC_SYNC_SECRETS,
EventType.SECRET_SYNC_REMOVE_SECRETS,
EventType.SECRET_SYNC_IMPORT_SECRETS
];
type Props = {
secretSync: TSecretSync;
};
export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
const { subscription } = useSubscription();
const auditLogsRetentionDays = subscription?.auditLogsRetentionDays ?? 30;
return (
<div className="flex max-h-full w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Sync Logs</h3>
{subscription.auditLogs && (
<p className="text-xs text-bunker-300">
Displaying audit logs from the last {auditLogsRetentionDays} days
</p>
)}
</div>
{subscription.auditLogs ? (
<LogsSection
refetchInterval={4000}
remappedHeaders={{
Metadata: "Sync Status"
}}
showFilters={false}
presets={{
eventMetadata: { syncId: secretSync.id },
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
eventType: INTEGRATION_EVENTS
}}
filterClassName="bg-mineshaft-900 static"
/>
) : (
<div className="flex h-full items-center justify-center rounded-lg bg-mineshaft-800 text-sm text-mineshaft-200">
<div className="flex flex-col items-center gap-4 py-20">
<FontAwesomeIcon size="2x" icon={faFingerprint} />
<p>
Please{" "}
{subscription && subscription.slug !== null ? (
<Link to="/organization/billing" target="_blank" rel="noopener noreferrer">
<a
className="cursor-pointer underline transition-all hover:text-white"
target="_blank"
>
upgrade your subscription
</a>
</Link>
) : (
<a
href="https://infisical.com/scheduledemo"
target="_blank"
rel="noopener noreferrer"
>
<a
className="cursor-pointer underline transition-all hover:text-white"
target="_blank"
>
upgrade your subscription
</a>
</a>
)}{" "}
to view sync logs.
</p>
</div>
</div>
)}
</div>
);
};
@@ -0,0 +1,28 @@
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { Badge } from "@app/components/v2";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-parameter-store-sync";
type Props = {
secretSync: TAwsParameterStoreSync;
};
export const AwsParameterStoreSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { path, region }
} = secretSync;
const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
return (
<>
<SecretSyncLabel label="Region">
{awsRegion?.name}
<Badge className="ml-1" variant="success">
{awsRegion?.slug}{" "}
</Badge>
</SecretSyncLabel>
<SecretSyncLabel label="Path">{path}</SecretSyncLabel>
</>
);
};
@@ -0,0 +1,75 @@
import { ReactNode } from "react";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { GitHubSyncSelectedRepositoriesTooltipContent } from "@app/components/secret-syncs/github/GitHubSyncSelectedRepositoriesTooltipContent";
import { Tooltip } from "@app/components/v2";
import {
GitHubSyncScope,
GitHubSyncVisibility,
TGitHubSync
} from "@app/hooks/api/secretSyncs/types/github-sync";
type Props = {
secretSync: TGitHubSync;
};
export const GitHubSyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
let Components: ReactNode;
switch (destinationConfig.scope) {
case GitHubSyncScope.Organization:
Components = (
<>
<SecretSyncLabel label="Organization">{destinationConfig.org}</SecretSyncLabel>
<SecretSyncLabel label="Visibility" className="capitalize">
{destinationConfig.visibility} Repositories
</SecretSyncLabel>
{destinationConfig.visibility === GitHubSyncVisibility.Selected && (
<SecretSyncLabel label="Selected Repositories">
{destinationConfig.selectedRepositoryIds?.length ?? 0} Repositories
<Tooltip
side="bottom"
content={<GitHubSyncSelectedRepositoriesTooltipContent secretSync={secretSync} />}
>
<FontAwesomeIcon size="xs" className="ml-1 text-bunker-300" icon={faInfoCircle} />
</Tooltip>
</SecretSyncLabel>
)}
</>
);
break;
case GitHubSyncScope.Repository:
Components = (
<SecretSyncLabel label="Repository">
{destinationConfig.owner}/{destinationConfig.repo}
</SecretSyncLabel>
);
break;
case GitHubSyncScope.RepositoryEnvironment:
Components = (
<>
<SecretSyncLabel label="Repository">
{destinationConfig.owner}/{destinationConfig.repo}
</SecretSyncLabel>
<SecretSyncLabel label="Environment">{destinationConfig.env}</SecretSyncLabel>
</>
);
break;
default:
throw new Error(
`Uhandled GitHub Sync Destination Section Scope ${secretSync.destinationConfig.scope}`
);
}
return (
<>
<SecretSyncLabel className="capitalize" label="Scope">
{destinationConfig.scope.replace("-", " ")}
</SecretSyncLabel>
{Components}
</>
);
};
@@ -0,0 +1,63 @@
import { ReactNode } from "react";
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { IconButton } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsParameterStoreSyncDestinationSection";
import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection";
type Props = {
secretSync: TSecretSync;
onEditDestination: VoidFunction;
};
export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: Props) => {
const { destination, connection } = secretSync;
const app = APP_CONNECTION_MAP[connection.app].name;
let DestinationComponents: ReactNode;
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
DestinationComponents = <AwsParameterStoreSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.GitHub:
DestinationComponents = <GitHubSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Destination Configuration</h3>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync destination"
onClick={onEditDestination}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div className="flex w-full flex-wrap gap-8">
<SecretSyncLabel label={`${app} Connection`}>{connection.name}</SecretSyncLabel>
{DestinationComponents}
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./SecretSyncDestinatonSection";
@@ -0,0 +1,68 @@
import { faBan, faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import { SecretSyncLabel, SecretSyncStatusBadge } from "@app/components/secret-syncs";
import { Badge, IconButton } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = {
secretSync: TSecretSync;
onEditDetails: VoidFunction;
};
export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) => {
const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, isEnabled } = secretSync;
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Details</h3>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync details"
onClick={onEditDetails}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div>
<div className="space-y-3">
<SecretSyncLabel label="Name">{name}</SecretSyncLabel>
<SecretSyncLabel label="Description">{description}</SecretSyncLabel>
<SecretSyncLabel label="Status">
{isEnabled ? (
syncStatus && <SecretSyncStatusBadge status={syncStatus} />
) : (
<Badge className="flex w-min items-center gap-1.5 bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faBan} />
<span>Disabled</span>
</Badge>
)}
</SecretSyncLabel>
{lastSyncedAt && (
<SecretSyncLabel label="Last Synced">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
</SecretSyncLabel>
)}
{syncStatus === SecretSyncStatus.Failed && lastSyncMessage && (
<SecretSyncLabel labelClassName="text-red" label="Last Sync Error">
<p className="rounded bg-mineshaft-600 p-2 text-xs">{lastSyncMessage}</p>
</SecretSyncLabel>
)}
</div>
</div>
</div>
);
};
@@ -0,0 +1,59 @@
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { IconButton } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs";
import { TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = {
secretSync: TSecretSync;
onEditOptions: VoidFunction;
};
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => {
const {
destination,
syncOptions: { appendSuffix, prependPrefix, initialSyncBehavior },
lastSyncedAt
} = secretSync;
return (
<div>
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync options"
onClick={onEditOptions}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div>
<div className="space-y-3">
{!lastSyncedAt && (
<SecretSyncLabel label="Initial Sync Behavior">
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
</SecretSyncLabel>
)}
<SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel>
</div>
</div>
</div>
</div>
);
};
@@ -0,0 +1,49 @@
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { IconButton } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = {
secretSync: TSecretSync;
onEditSource: VoidFunction;
};
export const SecretSyncSourceSection = ({ secretSync, onEditSource }: Props) => {
const { folder, environment } = secretSync;
return (
<div>
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Source</h3>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync source"
onClick={onEditSource}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div>
<div className="space-y-3">
<SecretSyncLabel label="Environment">{environment.name}</SecretSyncLabel>
<SecretSyncLabel label="Path">{folder.path}</SecretSyncLabel>
</div>
</div>
</div>
</div>
);
};
@@ -0,0 +1,6 @@
export * from "./SecretSyncActionTriggers";
export * from "./SecretSyncAuditLogsSection";
export * from "./SecretSyncDestinationSection";
export * from "./SecretSyncDetailsSection";
export * from "./SecretSyncOptionsSection";
export * from "./SecretSyncSourceSection";