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

View File

@@ -307,7 +307,6 @@ export const AwsParameterStoreSyncFns = {
awsParameterStoreSecretsRecord,
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)) {
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({
syncTagsRecord: {
// configured sync tags take preference over secret metadata
...(syncOptions.syncSecretMetadataAsTags &&
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] ?? {}
});

View File

@@ -366,37 +366,39 @@ export const AwsSecretsManagerSyncFns = {
}
}
const { tagsToAdd, tagKeysToRemove } = processTags({
syncTagsRecord: {
// configured sync tags take preference over secret metadata
...(syncOptions.syncSecretMetadataAsTags &&
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
...syncTagsRecord
},
awsTagsRecord: Object.fromEntries(
awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
)
});
if (syncOptions.tags !== undefined || syncOptions.syncSecretMetadataAsTags) {
const { tagsToAdd, tagKeysToRemove } = processTags({
syncTagsRecord: {
// configured sync tags take preference over secret metadata
...(syncOptions.syncSecretMetadataAsTags &&
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!]) ?? []
)
});
if (tagsToAdd.length) {
try {
await addTags(client, key, tagsToAdd);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
if (tagsToAdd.length) {
try {
await addTags(client, key, tagsToAdd);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
if (tagKeysToRemove.length) {
try {
await removeTags(client, key, tagKeysToRemove);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
if (tagKeysToRemove.length) {
try {
await removeTags(client, key, tagKeysToRemove);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
}
@@ -439,32 +441,34 @@ export const AwsSecretsManagerSyncFns = {
});
}
const { tagsToAdd, tagKeysToRemove } = processTags({
syncTagsRecord,
awsTagsRecord: Object.fromEntries(
awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
)
});
if (syncOptions.tags !== undefined) {
const { tagsToAdd, tagKeysToRemove } = processTags({
syncTagsRecord,
awsTagsRecord: Object.fromEntries(
awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
)
});
if (tagsToAdd.length) {
try {
await addTags(client, destinationConfig.secretName, tagsToAdd);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: destinationConfig.secretName
});
if (tagsToAdd.length) {
try {
await addTags(client, destinationConfig.secretName, tagsToAdd);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: destinationConfig.secretName
});
}
}
}
if (tagKeysToRemove.length) {
try {
await removeTags(client, destinationConfig.secretName, tagKeysToRemove);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: destinationConfig.secretName
});
if (tagKeysToRemove.length) {
try {
await removeTags(client, destinationConfig.secretName, tagKeysToRemove);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: destinationConfig.secretName
});
}
}
}
}

View File

@@ -22,87 +22,23 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const AwsParameterStoreSyncOptionsFields = () => {
const { control, watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
const AwsTagsSection = () => {
const { control } = useFormContext<
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({
control,
name: "syncOptions.tags"
});
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>
)}
/>
<div className="mb-4 mt-2 flex flex-col pl-2">
<FormLabel
label="Resource Tags"
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) => (
<Fragment key={tagFieldId}>
<div className="col-span-5">
@@ -164,12 +100,118 @@ export const AwsParameterStoreSyncOptionsFields = () => {
Add Tag
</Button>
</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
name="syncOptions.syncSecretMetadataAsTags"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className="mt-6"
className="mt-4"
isError={Boolean(error?.message)}
errorText={error?.message}
>

View File

@@ -23,14 +23,94 @@ import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs
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 = () => {
const { control, watch } = useFormContext<
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
const region = watch("destinationConfig.region");
const connectionId = useWatch({ name: "connection.id", control });
const mappingBehavior = watch("destinationConfig.mappingBehavior");
const watchedTags = watch("syncOptions.tags");
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
{
@@ -41,11 +121,6 @@ export const AwsSecretsManagerSyncOptionsFields = () => {
{ enabled: Boolean(connectionId && region) }
);
const tagFields = useFieldArray({
control,
name: "syncOptions.tags"
});
return (
<>
<Controller
@@ -102,75 +177,47 @@ export const AwsSecretsManagerSyncOptionsFields = () => {
</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">
{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="mb-6 mt-2 flex">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => tagFields.append({ key: "", value: "" })}
>
Add Tag
</Button>
</div>
<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 Secret Tags{" "}
<Tooltip
className="max-w-md"
content={
<p>
If enabled, AWS secret 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 />}
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne && (
<Controller
name="syncOptions.syncSecretMetadataAsTags"
control={control}
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
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="overwrite-existing-secrets"