Allow undefined value for tags to prevent unwanted overrides

This commit is contained in:
x032205
2025-06-24 23:13:53 -04:00
parent b59fa14bb6
commit f0a95808e7
4 changed files with 286 additions and 194 deletions
@@ -307,7 +307,6 @@ export const AwsParameterStoreSyncFns = {
awsParameterStoreSecretsRecord, awsParameterStoreSecretsRecord,
Boolean(syncOptions.tags?.length || syncOptions.syncSecretMetadataAsTags) Boolean(syncOptions.tags?.length || syncOptions.syncSecretMetadataAsTags)
); );
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)) {
const [key, { value, secretMetadata }] = entry; const [key, { value, secretMetadata }] = entry;
@@ -342,13 +341,13 @@ export const AwsParameterStoreSyncFns = {
} }
} }
if (shouldManageTags) { if ((syncOptions.tags !== undefined || syncOptions.syncSecretMetadataAsTags) && shouldManageTags) {
const { tagsToAdd, tagKeysToRemove } = processParameterTags({ const { tagsToAdd, tagKeysToRemove } = processParameterTags({
syncTagsRecord: { syncTagsRecord: {
// configured sync tags take preference over secret metadata // configured sync tags take preference over secret metadata
...(syncOptions.syncSecretMetadataAsTags && ...(syncOptions.syncSecretMetadataAsTags &&
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])), Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
...syncTagsRecord ...(syncOptions.tags && Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []))
}, },
awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {} awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {}
}); });
@@ -366,37 +366,39 @@ export const AwsSecretsManagerSyncFns = {
} }
} }
const { tagsToAdd, tagKeysToRemove } = processTags({ if (syncOptions.tags !== undefined || syncOptions.syncSecretMetadataAsTags) {
syncTagsRecord: { const { tagsToAdd, tagKeysToRemove } = processTags({
// 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]) ?? [])),
}, ...(syncOptions.tags !== undefined && syncTagsRecord)
awsTagsRecord: Object.fromEntries( },
awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? [] awsTagsRecord: Object.fromEntries(
) awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
}); )
});
if (tagsToAdd.length) { if (tagsToAdd.length) {
try { try {
await addTags(client, key, tagsToAdd); await addTags(client, key, tagsToAdd);
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error, error,
secretKey: key secretKey: key
}); });
}
} }
}
if (tagKeysToRemove.length) { if (tagKeysToRemove.length) {
try { try {
await removeTags(client, key, tagKeysToRemove); await removeTags(client, key, tagKeysToRemove);
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error, error,
secretKey: key secretKey: key
}); });
}
} }
} }
} }
@@ -439,32 +441,34 @@ export const AwsSecretsManagerSyncFns = {
}); });
} }
const { tagsToAdd, tagKeysToRemove } = processTags({ if (syncOptions.tags !== undefined) {
syncTagsRecord, const { tagsToAdd, tagKeysToRemove } = processTags({
awsTagsRecord: Object.fromEntries( syncTagsRecord,
awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? [] awsTagsRecord: Object.fromEntries(
) awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
}); )
});
if (tagsToAdd.length) { if (tagsToAdd.length) {
try { try {
await addTags(client, destinationConfig.secretName, tagsToAdd); await addTags(client, destinationConfig.secretName, tagsToAdd);
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error, error,
secretKey: destinationConfig.secretName secretKey: destinationConfig.secretName
}); });
}
} }
}
if (tagKeysToRemove.length) { if (tagKeysToRemove.length) {
try { try {
await removeTags(client, destinationConfig.secretName, tagKeysToRemove); await removeTags(client, destinationConfig.secretName, tagKeysToRemove);
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error, error,
secretKey: destinationConfig.secretName secretKey: destinationConfig.secretName
}); });
}
} }
} }
} }
@@ -22,87 +22,23 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas"; import { TSecretSyncForm } from "../schemas";
export const AwsParameterStoreSyncOptionsFields = () => { const AwsTagsSection = () => {
const { control, watch } = useFormContext< const { control } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSParameterStore } TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>(); >();
const region = watch("destinationConfig.region");
const connectionId = useWatch({ name: "connection.id", control });
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
{
connectionId,
region,
destination: SecretSync.AWSParameterStore
},
{ enabled: Boolean(connectionId && region) }
);
const tagFields = useFieldArray({ const tagFields = useFieldArray({
control, control,
name: "syncOptions.tags" name: "syncOptions.tags"
}); });
return ( return (
<> <div className="mb-4 mt-2 flex flex-col pl-2">
<Controller
name="syncOptions.keyId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="The AWS KMS key to encrypt parameters with"
isError={Boolean(error)}
errorText={error?.message}
label="KMS Key"
>
<FilterableSelect
isLoading={isKmsKeysPending && Boolean(connectionId && region)}
isDisabled={!connectionId}
value={kmsKeys.find((org) => org.alias === value) ?? null}
onChange={(option) =>
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">
&#34;kms:ListAliases&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:DescribeKey&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Encrypt&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Decrypt&#34;
</span>
.
</p>
)
}
options={kmsKeys}
placeholder="Leave blank to use default KMS key"
getOptionLabel={(option) =>
option.alias === "alias/aws/ssm" ? `${option.alias} (Default)` : option.alias
}
getOptionValue={(option) => option.alias}
/>
</FormControl>
)}
/>
<FormLabel <FormLabel
label="Resource Tags" label="Resource Tags"
tooltipText="Add resource tags to parameters synced by Infisical" tooltipText="Add resource tags to parameters synced by Infisical"
/> />
<div className="mb-3 grid max-h-[20vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto"> <div className="grid max-h-[20vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto">
{tagFields.fields.map(({ id: tagFieldId }, i) => ( {tagFields.fields.map(({ id: tagFieldId }, i) => (
<Fragment key={tagFieldId}> <Fragment key={tagFieldId}>
<div className="col-span-5"> <div className="col-span-5">
@@ -164,12 +100,118 @@ export const AwsParameterStoreSyncOptionsFields = () => {
Add Tag Add Tag
</Button> </Button>
</div> </div>
</div>
);
};
export const AwsParameterStoreSyncOptionsFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
>();
const region = watch("destinationConfig.region");
const connectionId = useWatch({ name: "connection.id", control });
const watchedTags = watch("syncOptions.tags");
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
{
connectionId,
region,
destination: SecretSync.AWSParameterStore
},
{ enabled: Boolean(connectionId && region) }
);
return (
<>
<Controller
name="syncOptions.keyId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="The AWS KMS key to encrypt parameters with"
isError={Boolean(error)}
errorText={error?.message}
label="KMS Key"
>
<FilterableSelect
isLoading={isKmsKeysPending && Boolean(connectionId && region)}
isDisabled={!connectionId}
value={kmsKeys.find((org) => org.alias === value) ?? null}
onChange={(option) =>
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">
&#34;kms:ListAliases&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:DescribeKey&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Encrypt&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Decrypt&#34;
</span>
.
</p>
)
}
options={kmsKeys}
placeholder="Leave blank to use default KMS key"
getOptionLabel={(option) =>
option.alias === "alias/aws/ssm" ? `${option.alias} (Default)` : option.alias
}
getOptionValue={(option) => option.alias}
/>
</FormControl>
)}
/>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="overwrite-tags"
thumbClassName="bg-mineshaft-800"
isChecked={Array.isArray(watchedTags)}
onCheckedChange={(isChecked) => {
if (isChecked) {
setValue("syncOptions.tags", []);
} else {
setValue("syncOptions.tags", undefined);
}
}}
>
<p className="w-[14rem]">
Override Resource Tags{" "}
<Tooltip
className="max-w-md"
content={
<p>
If enabled, AWS resource tags will be overwritten using static values defined below.
</p>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
{Array.isArray(watchedTags) && <AwsTagsSection />}
<Controller <Controller
name="syncOptions.syncSecretMetadataAsTags" name="syncOptions.syncSecretMetadataAsTags"
control={control} control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
className="mt-6" className="mt-4"
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
> >
@@ -23,14 +23,94 @@ import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs
import { TSecretSyncForm } from "../schemas"; import { TSecretSyncForm } from "../schemas";
const AwsTagsSection = () => {
const { control } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
const tagFields = useFieldArray({
control,
name: "syncOptions.tags"
});
return (
<div className="mb-4 mt-2 flex flex-col pl-2">
<FormLabel label="Tags" tooltipText="Add tags to secrets synced by Infisical" />
<div className="grid max-h-[20vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto">
{tagFields.fields.map(({ id: tagFieldId }, i) => (
<Fragment key={tagFieldId}>
<div className="col-span-5">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`syncOptions.tags.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input className="text-xs" {...field} />
</FormControl>
)}
/>
</div>
<div className="col-span-6">
{i === 0 && (
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
)}
<Controller
control={control}
name={`syncOptions.tags.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input className="text-xs" {...field} />
</FormControl>
)}
/>
</div>
<Tooltip content="Remove tag" position="right">
<IconButton
variant="plain"
ariaLabel="Remove tag"
className="col-span-1 mb-1.5"
colorSchema="danger"
size="xs"
onClick={() => tagFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</Fragment>
))}
</div>
<div className="mt-2 flex">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => tagFields.append({ key: "", value: "" })}
>
Add Tag
</Button>
</div>
</div>
);
};
export const AwsSecretsManagerSyncOptionsFields = () => { export const AwsSecretsManagerSyncOptionsFields = () => {
const { control, watch } = useFormContext< const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager } TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>(); >();
const region = watch("destinationConfig.region"); const region = watch("destinationConfig.region");
const connectionId = useWatch({ name: "connection.id", control }); const connectionId = useWatch({ name: "connection.id", control });
const mappingBehavior = watch("destinationConfig.mappingBehavior"); const mappingBehavior = watch("destinationConfig.mappingBehavior");
const watchedTags = watch("syncOptions.tags");
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys( const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
{ {
@@ -41,11 +121,6 @@ export const AwsSecretsManagerSyncOptionsFields = () => {
{ enabled: Boolean(connectionId && region) } { enabled: Boolean(connectionId && region) }
); );
const tagFields = useFieldArray({
control,
name: "syncOptions.tags"
});
return ( return (
<> <>
<Controller <Controller
@@ -102,75 +177,47 @@ export const AwsSecretsManagerSyncOptionsFields = () => {
</FormControl> </FormControl>
)} )}
/> />
<FormLabel label="Tags" tooltipText="Add tags to secrets synced by Infisical" />
<div className="mb-3 grid max-h-[20vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto"> <Switch
{tagFields.fields.map(({ id: tagFieldId }, i) => ( className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
<Fragment key={tagFieldId}> id="overwrite-tags"
<div className="col-span-5"> thumbClassName="bg-mineshaft-800"
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>} isChecked={Array.isArray(watchedTags)}
<Controller onCheckedChange={(isChecked) => {
control={control} if (isChecked) {
name={`syncOptions.tags.${i}.key`} setValue("syncOptions.tags", []);
render={({ field, fieldState: { error } }) => ( } else {
<FormControl setValue("syncOptions.tags", undefined);
isError={Boolean(error?.message)} }
errorText={error?.message} }}
className="mb-0" >
> <p className="w-[14rem]">
<Input className="text-xs" {...field} /> Override Secret Tags{" "}
</FormControl> <Tooltip
)} className="max-w-md"
/> content={
</div> <p>
<div className="col-span-6"> If enabled, AWS secret tags will be overwritten using static values defined below.
{i === 0 && ( </p>
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional /> }
)} >
<Controller <FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
control={control} </Tooltip>
name={`syncOptions.tags.${i}.value`} </p>
render={({ field, fieldState: { error } }) => ( </Switch>
<FormControl
isError={Boolean(error?.message)} {Array.isArray(watchedTags) && <AwsTagsSection />}
errorText={error?.message}
className="mb-0"
>
<Input className="text-xs" {...field} />
</FormControl>
)}
/>
</div>
<Tooltip content="Remove tag" position="right">
<IconButton
variant="plain"
ariaLabel="Remove tag"
className="col-span-1 mb-1.5"
colorSchema="danger"
size="xs"
onClick={() => tagFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</Fragment>
))}
</div>
<div className="mb-6 mt-2 flex">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => tagFields.append({ key: "", value: "" })}
>
Add Tag
</Button>
</div>
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne && ( {mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne && (
<Controller <Controller
name="syncOptions.syncSecretMetadataAsTags" name="syncOptions.syncSecretMetadataAsTags"
control={control} control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}> <FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mt-4"
>
<Switch <Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80" className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="overwrite-existing-secrets" id="overwrite-existing-secrets"