mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 16:27:46 +00:00
improvement: address feedback
This commit is contained in:
@@ -24,9 +24,25 @@ const listAwsKmsKeys = async (
|
|||||||
region
|
region
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Aliases = [] } = await awsKms.listAliases({ Limit: 100 }).promise();
|
const aliasEntries: AWS.KMS.AliasList = [];
|
||||||
|
let aliasMarker: string | undefined;
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const response = await awsKms.listAliases({ Limit: 100, Marker: aliasMarker }).promise();
|
||||||
|
aliasEntries.push(...(response.Aliases || []));
|
||||||
|
aliasMarker = response.NextMarker;
|
||||||
|
} while (aliasMarker);
|
||||||
|
|
||||||
const aliasEntries = Aliases.filter((aliasEntry) => {
|
const keyMetadataRecord: Record<string, AWS.KMS.KeyMetadata | undefined> = {};
|
||||||
|
for await (const aliasEntry of aliasEntries) {
|
||||||
|
if (aliasEntry.TargetKeyId) {
|
||||||
|
const keyDescription = await awsKms.describeKey({ KeyId: aliasEntry.TargetKeyId }).promise();
|
||||||
|
|
||||||
|
keyMetadataRecord[aliasEntry.TargetKeyId] = keyDescription.KeyMetadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validAliasEntries = aliasEntries.filter((aliasEntry) => {
|
||||||
if (!aliasEntry.TargetKeyId) return false;
|
if (!aliasEntry.TargetKeyId) return false;
|
||||||
|
|
||||||
if (destination === SecretSync.AWSParameterStore && aliasEntry.AliasName === "alias/aws/ssm") return true;
|
if (destination === SecretSync.AWSParameterStore && aliasEntry.AliasName === "alias/aws/ssm") return true;
|
||||||
@@ -36,13 +52,18 @@ const listAwsKmsKeys = async (
|
|||||||
|
|
||||||
if (aliasEntry.AliasName?.includes("alias/aws/")) return false;
|
if (aliasEntry.AliasName?.includes("alias/aws/")) return false;
|
||||||
|
|
||||||
|
const keyMetadata = keyMetadataRecord[aliasEntry.TargetKeyId];
|
||||||
|
|
||||||
|
if (!keyMetadata || keyMetadata.KeyUsage !== "ENCRYPT_DECRYPT" || keyMetadata.KeySpec !== "SYMMETRIC_DEFAULT")
|
||||||
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const kmsKeys = aliasEntries.map((alias) => {
|
const kmsKeys = validAliasEntries.map((aliasEntry) => {
|
||||||
return {
|
return {
|
||||||
id: alias.TargetKeyId!,
|
id: aliasEntry.TargetKeyId!,
|
||||||
alias: alias.AliasName!
|
alias: aliasEntry.AliasName!
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+52
-31
@@ -137,10 +137,9 @@ const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<T
|
|||||||
|
|
||||||
const getParameterStoreTagsRecord = async (
|
const getParameterStoreTagsRecord = async (
|
||||||
ssm: AWS.SSM,
|
ssm: AWS.SSM,
|
||||||
path: string,
|
|
||||||
awsParameterStoreSecretsRecord: TAWSParameterStoreRecord,
|
awsParameterStoreSecretsRecord: TAWSParameterStoreRecord,
|
||||||
areTagsPresent: boolean
|
needsTagsPermissions: boolean
|
||||||
): Promise<TAWSParameterStoreTagsRecord> => {
|
): Promise<{ shouldManageTags: boolean; awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord }> => {
|
||||||
const awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord = {};
|
const awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord = {};
|
||||||
|
|
||||||
for await (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
for await (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
||||||
@@ -163,15 +162,23 @@ const getParameterStoreTagsRecord = async (
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// users aren't required to provide tag permissions to use sync so we handle gracefully if unauthorized
|
// users aren't required to provide tag permissions to use sync so we handle gracefully if unauthorized
|
||||||
// and they aren't trying to configure tags
|
// and they aren't trying to configure tags
|
||||||
if ((e as AWSError).code === "AccessDeniedException" && !areTagsPresent) {
|
if ((e as AWSError).code === "AccessDeniedException") {
|
||||||
return {};
|
if (!needsTagsPermissions) {
|
||||||
|
return { shouldManageTags: false, awsParameterStoreTagsRecord: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SecretSyncError({
|
||||||
|
message:
|
||||||
|
"IAM role has inadequate permissions to manage resource tags. Ensure the following polices are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource",
|
||||||
|
shouldRetry: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return awsParameterStoreTagsRecord;
|
return { shouldManageTags: true, awsParameterStoreTagsRecord };
|
||||||
};
|
};
|
||||||
|
|
||||||
const processParameterTags = ({
|
const processParameterTags = ({
|
||||||
@@ -294,13 +301,11 @@ export const AwsParameterStoreSyncFns = {
|
|||||||
|
|
||||||
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
|
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
|
||||||
|
|
||||||
const awsParameterStoreTagsRecord = await getParameterStoreTagsRecord(
|
const { shouldManageTags, awsParameterStoreTagsRecord } = await getParameterStoreTagsRecord(
|
||||||
ssm,
|
ssm,
|
||||||
destinationConfig.path,
|
|
||||||
awsParameterStoreSecretsRecord,
|
awsParameterStoreSecretsRecord,
|
||||||
Boolean(syncOptions.tags || syncOptions.syncSecretMetadataAsTags)
|
Boolean(syncOptions.tags?.length || syncOptions.syncSecretMetadataAsTags)
|
||||||
);
|
);
|
||||||
|
|
||||||
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
|
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
|
||||||
|
|
||||||
for await (const entry of Object.entries(secretMap)) {
|
for await (const entry of Object.entries(secretMap)) {
|
||||||
@@ -316,7 +321,7 @@ export const AwsParameterStoreSyncFns = {
|
|||||||
if (
|
if (
|
||||||
!(key in awsParameterStoreSecretsRecord) ||
|
!(key in awsParameterStoreSecretsRecord) ||
|
||||||
value !== awsParameterStoreSecretsRecord[key].Value ||
|
value !== awsParameterStoreSecretsRecord[key].Value ||
|
||||||
syncOptions.keyId !== awsParameterStoreMetadataRecord[key]?.KeyId
|
(syncOptions.keyId ?? "alias/aws/ssm") !== awsParameterStoreMetadataRecord[key]?.KeyId
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await putParameter(ssm, {
|
await putParameter(ssm, {
|
||||||
@@ -334,28 +339,44 @@ export const AwsParameterStoreSyncFns = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tagsToAdd, tagKeysToRemove } = processParameterTags({
|
if (shouldManageTags) {
|
||||||
syncTagsRecord: {
|
const { tagsToAdd, tagKeysToRemove } = processParameterTags({
|
||||||
// configured sync tags take preference over secret metadata
|
syncTagsRecord: {
|
||||||
...(syncOptions.syncSecretMetadataAsTags &&
|
// configured sync tags take preference over secret metadata
|
||||||
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
...(syncOptions.syncSecretMetadataAsTags &&
|
||||||
...syncTagsRecord
|
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||||
},
|
...syncTagsRecord
|
||||||
awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {}
|
},
|
||||||
});
|
awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {}
|
||||||
|
|
||||||
if (tagsToAdd.length) {
|
|
||||||
await addTagsToParameter(ssm, {
|
|
||||||
ResourceId: `${destinationConfig.path}${key}`,
|
|
||||||
Tags: tagsToAdd
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (tagKeysToRemove.length) {
|
if (tagsToAdd.length) {
|
||||||
await removeTagsFromParameter(ssm, {
|
try {
|
||||||
ResourceId: `${destinationConfig.path}${key}`,
|
await addTagsToParameter(ssm, {
|
||||||
TagKeys: tagKeysToRemove
|
ResourceId: `${destinationConfig.path}${key}`,
|
||||||
});
|
Tags: tagsToAdd
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagKeysToRemove.length) {
|
||||||
|
try {
|
||||||
|
await removeTagsFromParameter(ssm, {
|
||||||
|
ResourceId: `${destinationConfig.path}${key}`,
|
||||||
|
TagKeys: tagKeysToRemove
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-5
@@ -35,14 +35,17 @@ const AwsParameterStoreSyncOptionsSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.regex(
|
.regex(
|
||||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||||
"Tag keys can only contain Unicode letters, digits, white space and any of the following: _."
|
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
)
|
)
|
||||||
.min(1, "AWS tag key required")
|
.min(1, "Resource tag key required")
|
||||||
.max(128, "AWS tag name cannot exceed 128 characters"),
|
.max(128, "Resource tag name cannot exceed 128 characters"),
|
||||||
value: z
|
value: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, "Invalid AWS tag value")
|
.regex(
|
||||||
.max(256, "Tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-")
|
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||||
|
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
|
)
|
||||||
|
.max(256, "Resource tag value cannot exceed 256 characters")
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.max(50)
|
.max(50)
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ via the UI or API for the third-party service you intend to sync secrets to.
|
|||||||
- <strong>Destination:</strong> The App Connection to utilize and the destination endpoint to deploy secrets to. These can vary between services.
|
- <strong>Destination:</strong> The App Connection to utilize and the destination endpoint to deploy secrets to. These can vary between services.
|
||||||
- <strong>Options:</strong> Customize how secrets should be synced. Examples include adding a suffix or prefix to your secrets, or importing secrets from the destination on the initial sync.
|
- <strong>Options:</strong> Customize how secrets should be synced. Examples include adding a suffix or prefix to your secrets, or importing secrets from the destination on the initial sync.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Secret Syncs are the source of truth for connected third-party services. Any secret,
|
||||||
|
including associated data, not present or imported in Infisical before syncing will be
|
||||||
|
overwritten, and changes directly in the connected service outside of infisical may also
|
||||||
|
be overwritten by future syncs.
|
||||||
|
</Note>
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
Some third-party services do not support importing secrets.
|
Some third-party services do not support importing secrets.
|
||||||
</Info>
|
</Info>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, Checkbox, FormControl, Switch } from "@app/components/v2";
|
import { Button, FormControl, Modal, ModalContent, Switch } from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||||
import {
|
import {
|
||||||
@@ -42,8 +44,9 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
||||||
|
|
||||||
|
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||||
|
|
||||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||||
const [confirmOverwrite, setConfirmOverwrite] = useState(false);
|
|
||||||
|
|
||||||
const { syncOption } = useSecretSyncOption(destination);
|
const { syncOption } = useSecretSyncOption(destination);
|
||||||
|
|
||||||
@@ -77,6 +80,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
onComplete(secretSync);
|
onComplete(secretSync);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
setShowConfirmation(false);
|
||||||
createNotification({
|
createNotification({
|
||||||
title: `Failed to add ${destinationName} Sync`,
|
title: `Failed to add ${destinationName} Sync`,
|
||||||
text: err.message,
|
text: err.message,
|
||||||
@@ -94,7 +98,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
setSelectedTabIndex((prev) => prev - 1);
|
setSelectedTabIndex((prev) => prev - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { handleSubmit, trigger, watch, control } = formMethods;
|
const { handleSubmit, trigger, control } = formMethods;
|
||||||
|
|
||||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||||
|
|
||||||
@@ -102,7 +106,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
if (isFinalStep) {
|
if (isFinalStep) {
|
||||||
handleSubmit(onSubmit)();
|
setShowConfirmation(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,113 +127,146 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
|||||||
return isEnabled;
|
return isEnabled;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialSyncBehavior = watch("syncOptions.initialSyncBehavior");
|
if (showConfirmation)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col rounded-sm border border-l-[2px] border-mineshaft-600 border-l-primary bg-mineshaft-700/80 px-4 py-3">
|
||||||
|
<div className="mb-1 flex items-center text-sm">
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
|
||||||
|
Secret Sync Behavior
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-bunker-200">
|
||||||
|
Secret Syncs are the source of truth for connected third-party services. Any secret,
|
||||||
|
including associated data, not present or imported in Infisical before syncing will be
|
||||||
|
overwritten, and changes directly in the connected service outside of infisical may also
|
||||||
|
be overwritten by future syncs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex gap-4">
|
||||||
|
<Button
|
||||||
|
isDisabled={createSecretSync.isPending}
|
||||||
|
isLoading={createSecretSync.isPending}
|
||||||
|
onClick={handleSubmit(onSubmit)}
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
I Understand
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
isDisabled={createSecretSync.isPending}
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => setShowConfirmation(false)}
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
<>
|
||||||
<FormProvider {...formMethods}>
|
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
<FormProvider {...formMethods}>
|
||||||
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||||
{FORM_TABS.map((tab, index) => (
|
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||||
<Tab
|
{FORM_TABS.map((tab, index) => (
|
||||||
onClick={async (e) => {
|
<Tab
|
||||||
e.preventDefault();
|
onClick={async (e) => {
|
||||||
const isEnabled = await isTabEnabled(index);
|
e.preventDefault();
|
||||||
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
|
const isEnabled = await isTabEnabled(index);
|
||||||
}}
|
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
|
||||||
className={({ selected }) =>
|
}}
|
||||||
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
className={({ selected }) =>
|
||||||
selected
|
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
selected
|
||||||
: "text-bunker-300"
|
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||||
}`
|
: "text-bunker-300"
|
||||||
}
|
}`
|
||||||
key={tab.key}
|
}
|
||||||
>
|
key={tab.key}
|
||||||
{index + 1}. {tab.name}
|
>
|
||||||
</Tab>
|
{index + 1}. {tab.name}
|
||||||
))}
|
</Tab>
|
||||||
</Tab.List>
|
))}
|
||||||
<Tab.Panels>
|
</Tab.List>
|
||||||
<Tab.Panel>
|
<Tab.Panels>
|
||||||
<SecretSyncSourceFields />
|
<Tab.Panel>
|
||||||
</Tab.Panel>
|
<SecretSyncSourceFields />
|
||||||
<Tab.Panel>
|
</Tab.Panel>
|
||||||
<SecretSyncDestinationFields />
|
<Tab.Panel>
|
||||||
</Tab.Panel>
|
<SecretSyncDestinationFields />
|
||||||
<Tab.Panel>
|
</Tab.Panel>
|
||||||
<SecretSyncOptionsFields />
|
<Tab.Panel>
|
||||||
<Controller
|
<SecretSyncOptionsFields />
|
||||||
control={control}
|
<Controller
|
||||||
name="isAutoSyncEnabled"
|
control={control}
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
name="isAutoSyncEnabled"
|
||||||
return (
|
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||||
<FormControl
|
return (
|
||||||
helperText={
|
<FormControl
|
||||||
value
|
helperText={
|
||||||
? "Secrets will automatically be synced when changes occur in the source location."
|
value
|
||||||
: "Secrets will not automatically be synced when changes occur in the source location. You can still trigger syncs manually."
|
? "Secrets will automatically be synced when changes occur in the source location."
|
||||||
}
|
: "Secrets will not automatically be synced when changes occur in the source location. You can still trigger syncs manually."
|
||||||
isError={Boolean(error)}
|
}
|
||||||
errorText={error?.message}
|
isError={Boolean(error)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<Switch
|
|
||||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
|
|
||||||
id="auto-sync-enabled"
|
|
||||||
thumbClassName="bg-mineshaft-800"
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
isChecked={value}
|
|
||||||
>
|
>
|
||||||
<p className="w-[8.4rem]">Auto-Sync {value ? "Enabled" : "Disabled"}</p>
|
<Switch
|
||||||
</Switch>
|
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
|
||||||
</FormControl>
|
id="auto-sync-enabled"
|
||||||
);
|
thumbClassName="bg-mineshaft-800"
|
||||||
}}
|
onCheckedChange={onChange}
|
||||||
/>
|
isChecked={value}
|
||||||
</Tab.Panel>
|
>
|
||||||
<Tab.Panel>
|
<p className="w-[8.4rem]">Auto-Sync {value ? "Enabled" : "Disabled"}</p>
|
||||||
<SecretSyncDetailsFields />
|
</Switch>
|
||||||
</Tab.Panel>
|
</FormControl>
|
||||||
<Tab.Panel>
|
);
|
||||||
<SecretSyncReviewFields />
|
}}
|
||||||
</Tab.Panel>
|
/>
|
||||||
</Tab.Panels>
|
</Tab.Panel>
|
||||||
</Tab.Group>
|
<Tab.Panel>
|
||||||
</FormProvider>
|
<SecretSyncDetailsFields />
|
||||||
{isFinalStep &&
|
</Tab.Panel>
|
||||||
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination && (
|
<Tab.Panel>
|
||||||
<Checkbox
|
<SecretSyncReviewFields />
|
||||||
id="confirm-overwrite"
|
</Tab.Panel>
|
||||||
isChecked={confirmOverwrite}
|
</Tab.Panels>
|
||||||
containerClassName="-mt-5"
|
</Tab.Group>
|
||||||
onCheckedChange={(isChecked) => setConfirmOverwrite(Boolean(isChecked))}
|
</FormProvider>
|
||||||
>
|
|
||||||
<p
|
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||||
className={`mt-5 text-wrap text-xs ${confirmOverwrite ? "text-mineshaft-200" : "text-red"}`}
|
<Button onClick={handleNext} colorSchema="secondary">
|
||||||
>
|
{isFinalStep ? "Create Sync" : "Next"}
|
||||||
I understand all secrets present in the configured {destinationName} destination will
|
|
||||||
be removed if they are not present within Infisical.
|
|
||||||
</p>
|
|
||||||
</Checkbox>
|
|
||||||
)}
|
|
||||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
|
||||||
<Button
|
|
||||||
isDisabled={
|
|
||||||
isFinalStep &&
|
|
||||||
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination &&
|
|
||||||
!confirmOverwrite
|
|
||||||
}
|
|
||||||
onClick={handleNext}
|
|
||||||
colorSchema="secondary"
|
|
||||||
>
|
|
||||||
{isFinalStep ? "Create Sync" : "Next"}
|
|
||||||
</Button>
|
|
||||||
{selectedTabIndex > 0 && (
|
|
||||||
<Button onClick={handlePrev} colorSchema="secondary">
|
|
||||||
Back
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
{selectedTabIndex > 0 && (
|
||||||
</div>
|
<Button onClick={handlePrev} colorSchema="secondary">
|
||||||
</form>
|
Back
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<Modal isOpen={showConfirmation} onOpenChange={setShowConfirmation}>
|
||||||
|
<ModalContent
|
||||||
|
title="Import Secrets"
|
||||||
|
subTitle={`Import secrets into Infisical from this ${destinationName} Sync destination.`}
|
||||||
|
>
|
||||||
|
<div className="mt-6 flex flex-col rounded-sm border border-l-[2px] border-mineshaft-600 border-l-primary bg-mineshaft-700/80 px-4 py-3">
|
||||||
|
<div className="mb-1 flex items-center text-sm">
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
|
||||||
|
Secret Sync Behavior
|
||||||
|
</div>
|
||||||
|
<p className="mb-2 mt-1 text-sm text-bunker-200">
|
||||||
|
Secret Syncs are the source of truth for connected third-party services. Any secret
|
||||||
|
not present or imported in Infisical before syncing will be overwritten, and changes
|
||||||
|
made directly in the connected service may also be overwritten by future syncs from
|
||||||
|
Infisical.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+25
@@ -63,6 +63,31 @@ export const AwsParameterStoreSyncOptionsFields = () => {
|
|||||||
onChange={(option) =>
|
onChange={(option) =>
|
||||||
onChange((option as SingleValue<TAwsConnectionKmsKey>)?.alias ?? null)
|
onChange((option as SingleValue<TAwsConnectionKmsKey>)?.alias ?? null)
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
|
noOptionsMessage={({ inputValue }) =>
|
||||||
|
inputValue ? undefined : (
|
||||||
|
<p>
|
||||||
|
To configure a KMS key, ensure the following permissions are present on the
|
||||||
|
selected IAM role:{" "}
|
||||||
|
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||||
|
"kms:ListKeys"
|
||||||
|
</span>
|
||||||
|
,{" "}
|
||||||
|
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||||
|
"kms:ListAliases"
|
||||||
|
</span>
|
||||||
|
,{" "}
|
||||||
|
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||||
|
"kms:Encrypt"
|
||||||
|
</span>
|
||||||
|
,{" "}
|
||||||
|
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||||
|
"kms:Decrypt"
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
options={kmsKeys}
|
options={kmsKeys}
|
||||||
placeholder="Leave blank to use default KMS key"
|
placeholder="Leave blank to use default KMS key"
|
||||||
getOptionLabel={(option) =>
|
getOptionLabel={(option) =>
|
||||||
|
|||||||
+3
-3
@@ -18,8 +18,8 @@ export const AwsParameterStoreSyncOptionsReviewFields = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||||
{tags?.length && (
|
{tags && tags.length > 0 && (
|
||||||
<SecretSyncLabel label="AWS Tags">
|
<SecretSyncLabel label="Resource Tags">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
side="right"
|
side="right"
|
||||||
className="max-w-xl p-1"
|
className="max-w-xl p-1"
|
||||||
@@ -52,7 +52,7 @@ export const AwsParameterStoreSyncOptionsReviewFields = () => {
|
|||||||
</SecretSyncLabel>
|
</SecretSyncLabel>
|
||||||
)}
|
)}
|
||||||
{syncSecretMetadataAsTags && (
|
{syncSecretMetadataAsTags && (
|
||||||
<SecretSyncLabel label="AWS Tags">
|
<SecretSyncLabel label="Sync Secret Metadata as Resource Tags">
|
||||||
<Badge variant="success">Enabled</Badge>
|
<Badge variant="success">Enabled</Badge>
|
||||||
</SecretSyncLabel>
|
</SecretSyncLabel>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
export type TDatabricksSecretScope = {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TDatabricksConnectionListSecretScopesResponse = {
|
|
||||||
secretScopes: TDatabricksSecretScope[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TListAwsConnectionKmsKeys = {
|
export type TListAwsConnectionKmsKeys = {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
region: string;
|
region: string;
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ export const SecretSyncsTab = () => {
|
|||||||
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
|
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
|
||||||
currentWorkspace.id,
|
currentWorkspace.id,
|
||||||
{
|
{
|
||||||
refetchInterval: 4000
|
refetchInterval: 30000
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ const PageContent = () => {
|
|||||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editSync"] as const);
|
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editSync"] as const);
|
||||||
|
|
||||||
const { data: secretSync, isPending } = useGetSecretSync(destination, syncId, {
|
const { data: secretSync, isPending } = useGetSecretSync(destination, syncId, {
|
||||||
refetchInterval: 4000
|
refetchInterval: 30000
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ export const AwsParameterStoreSyncOptionsSection = ({ secretSync }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||||
{tags?.length && (
|
{tags && tags.length > 0 && (
|
||||||
<SecretSyncLabel label="Resource Tags">
|
<SecretSyncLabel label="Resource Tags">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
side="right"
|
side="right"
|
||||||
|
|||||||
Reference in New Issue
Block a user