Merge pull request #1865 from Infisical/feat/add-one-to-one-support-for-aws-sm

feat: added one to one support for aws secret manager integration
This commit is contained in:
Sheen Capadngan
2024-05-23 23:48:47 +08:00
committed by GitHub
10 changed files with 228 additions and 140 deletions
+1
View File
@@ -662,6 +662,7 @@ export const INTEGRATION = {
secretPrefix: "The prefix for the saved secret. Used by GCP.", secretPrefix: "The prefix for the saved secret. Used by GCP.",
secretSuffix: "The suffix for the saved secret. Used by GCP.", secretSuffix: "The suffix for the saved secret. Used by GCP.",
initialSyncBehavoir: "Type of syncing behavoir with the integration.", initialSyncBehavoir: "Type of syncing behavoir with the integration.",
mappingBehavior: "The mapping behavior of the integration.",
shouldAutoRedeploy: "Used by Render to trigger auto deploy.", shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.", secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.", secretAWSTag: "The tags for AWS secrets.",
@@ -8,6 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { IntegrationMappingBehavior } from "@app/services/integration-auth/integration-list";
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
export const registerIntegrationRouter = async (server: FastifyZodProvider) => { export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
@@ -49,6 +50,10 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z
.nativeEnum(IntegrationMappingBehavior)
.optional()
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z secretGCPLabel: z
.object({ .object({
@@ -160,6 +165,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z secretGCPLabel: z
.object({ .object({
@@ -43,6 +43,11 @@ export enum IntegrationInitialSyncBehavior {
PREFER_SOURCE = "prefer-source" PREFER_SOURCE = "prefer-source"
} }
export enum IntegrationMappingBehavior {
ONE_TO_ONE = "one-to-one",
MANY_TO_ONE = "many-to-one"
}
export enum IntegrationUrls { export enum IntegrationUrls {
// integration oauth endpoints // integration oauth endpoints
GCP_TOKEN_URL = "https://oauth2.googleapis.com/token", GCP_TOKEN_URL = "https://oauth2.googleapis.com/token",
@@ -30,7 +30,12 @@ import { BadRequestError } from "@app/lib/errors";
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types"; import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationInitialSyncBehavior, Integrations, IntegrationUrls } from "./integration-list"; import {
IntegrationInitialSyncBehavior,
IntegrationMappingBehavior,
Integrations,
IntegrationUrls
} from "./integration-list";
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) => const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => { Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
@@ -570,13 +575,11 @@ const syncSecretsAWSSecretManager = async ({
accessId: string | null; accessId: string | null;
accessToken: string; accessToken: string;
}) => { }) => {
let secretsManager;
const secKeyVal = getSecretKeyValuePair(secrets);
const metadata = z.record(z.any()).parse(integration.metadata || {}); const metadata = z.record(z.any()).parse(integration.metadata || {});
try {
if (!accessId) return; if (!accessId) return;
secretsManager = new SecretsManagerClient({ const secretsManager = new SecretsManagerClient({
region: integration.region as string, region: integration.region as string,
credentials: { credentials: {
accessKeyId: accessId, accessKeyId: accessId,
@@ -584,9 +587,11 @@ const syncSecretsAWSSecretManager = async ({
} }
}); });
const processAwsSecret = async (secretId: string, keyValuePairs: Record<string, string | null | undefined>) => {
try {
const awsSecretManagerSecret = await secretsManager.send( const awsSecretManagerSecret = await secretsManager.send(
new GetSecretValueCommand({ new GetSecretValueCommand({
SecretId: integration.app as string SecretId: secretId
}) })
); );
@@ -596,11 +601,11 @@ const syncSecretsAWSSecretManager = async ({
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString); awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
} }
if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) { if (!isEqual(awsSecretManagerSecretObj, keyValuePairs)) {
await secretsManager.send( await secretsManager.send(
new UpdateSecretCommand({ new UpdateSecretCommand({
SecretId: integration.app as string, SecretId: secretId,
SecretString: JSON.stringify(secKeyVal) SecretString: JSON.stringify(keyValuePairs)
}) })
); );
} }
@@ -611,7 +616,7 @@ const syncSecretsAWSSecretManager = async ({
const describedSecret = await secretsManager.send( const describedSecret = await secretsManager.send(
// requires secretsmanager:DescribeSecret policy // requires secretsmanager:DescribeSecret policy
new DescribeSecretCommand({ new DescribeSecretCommand({
SecretId: integration.app as string SecretId: secretId
}) })
); );
@@ -669,7 +674,7 @@ const syncSecretsAWSSecretManager = async ({
if (tagsToUpdate.length) { if (tagsToUpdate.length) {
await secretsManager.send( await secretsManager.send(
new TagResourceCommand({ new TagResourceCommand({
SecretId: integration.app as string, SecretId: secretId,
Tags: tagsToUpdate Tags: tagsToUpdate
}) })
); );
@@ -678,7 +683,7 @@ const syncSecretsAWSSecretManager = async ({
if (tagsToDelete.length) { if (tagsToDelete.length) {
await secretsManager.send( await secretsManager.send(
new UntagResourceCommand({ new UntagResourceCommand({
SecretId: integration.app as string, SecretId: secretId,
TagKeys: tagsToDelete.map((tag) => tag.Key) TagKeys: tagsToDelete.map((tag) => tag.Key)
}) })
); );
@@ -689,8 +694,8 @@ const syncSecretsAWSSecretManager = async ({
if (err instanceof ResourceNotFoundException && secretsManager) { if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send( await secretsManager.send(
new CreateSecretCommand({ new CreateSecretCommand({
Name: integration.app as string, Name: secretId,
SecretString: JSON.stringify(secKeyVal), SecretString: JSON.stringify(keyValuePairs),
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }), ...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
Tags: metadata.secretAWSTag Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value })) ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
@@ -699,6 +704,17 @@ const syncSecretsAWSSecretManager = async ({
); );
} }
} }
};
if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) {
for await (const [key, value] of Object.entries(secrets)) {
await processAwsSecret(key, {
[key]: value.value
});
}
} else {
await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets));
}
}; };
/** /**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 142 KiB

@@ -72,6 +72,9 @@ Prerequisites:
<ParamField path="AWS Region" type="string" required> <ParamField path="AWS Region" type="string" required>
The region that you want to integrate with in AWS Secrets Manager. The region that you want to integrate with in AWS Secrets Manager.
</ParamField> </ParamField>
<ParamField path="Mapping Behavior" type="string" required>
How you want the integration to map the secrets. The selected value could be either one to one or one to many.
</ParamField>
<ParamField path="AWS SM Secret Name" type="string" required> <ParamField path="AWS SM Secret Name" type="string" required>
The secret name/path in AWS into which you want to sync the secrets from Infisical. The secret name/path in AWS into which you want to sync the secrets from Infisical.
</ParamField> </ParamField>
@@ -64,6 +64,7 @@ export const useCreateIntegration = () => {
secretSuffix?: string; secretSuffix?: string;
initialSyncBehavior?: string; initialSyncBehavior?: string;
shouldAutoRedeploy?: boolean; shouldAutoRedeploy?: boolean;
mappingBehavior?: string;
secretAWSTag?: { secretAWSTag?: {
key: string; key: string;
value: string; value: string;
@@ -36,6 +36,7 @@ export type TIntegration = {
metadata?: { metadata?: {
secretSuffix?: string; secretSuffix?: string;
syncBehavior?: IntegrationSyncBehavior; syncBehavior?: IntegrationSyncBehavior;
mappingBehavior?: IntegrationMappingBehavior;
scope: string; scope: string;
org: string; org: string;
project: string; project: string;
@@ -48,3 +49,8 @@ export enum IntegrationSyncBehavior {
PREFER_TARGET = "prefer-target", PREFER_TARGET = "prefer-target",
PREFER_SOURCE = "prefer-source" PREFER_SOURCE = "prefer-source"
} }
export enum IntegrationMappingBehavior {
ONE_TO_ONE = "one-to-one",
MANY_TO_ONE = "many-to-one"
}
@@ -15,6 +15,7 @@ import queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api"; import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries"; import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
import { import {
Button, Button,
@@ -70,6 +71,17 @@ const awsRegions = [
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" } { name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" }
]; ];
const mappingBehaviors = [
{
label: "Many to One (All Infisical secrets will be mapped to a single AWS secret)",
value: IntegrationMappingBehavior.MANY_TO_ONE
},
{
label: "One to One - (Each Infisical secret will be mapped to its own AWS secret)",
value: IntegrationMappingBehavior.ONE_TO_ONE
}
];
export default function AWSSecretManagerCreateIntegrationPage() { export default function AWSSecretManagerCreateIntegrationPage() {
const router = useRouter(); const router = useRouter();
const { mutateAsync } = useCreateIntegration(); const { mutateAsync } = useCreateIntegration();
@@ -84,6 +96,9 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/"); const [secretPath, setSecretPath] = useState("/");
const [selectedAWSRegion, setSelectedAWSRegion] = useState(""); const [selectedAWSRegion, setSelectedAWSRegion] = useState("");
const [selectedMappingBehavior, setSelectedMappingBehavior] = useState(
IntegrationMappingBehavior.MANY_TO_ONE
);
const [targetSecretName, setTargetSecretName] = useState(""); const [targetSecretName, setTargetSecretName] = useState("");
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState(""); const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState("");
const [tagKey, setTagKey] = useState(""); const [tagKey, setTagKey] = useState("");
@@ -116,7 +131,14 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
if (targetSecretName.trim() === "") { if (!selectedMappingBehavior) {
return;
}
if (
selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE &&
targetSecretName.trim() === ""
) {
setTargetSecretName("Secret name cannot be blank"); setTargetSecretName("Secret name cannot be blank");
return; return;
} }
@@ -143,7 +165,8 @@ export default function AWSSecretManagerCreateIntegrationPage() {
] ]
} }
: {}), : {}),
...(kmsKeyId && { kmsKeyId }) ...(kmsKeyId && { kmsKeyId }),
mappingBehavior: selectedMappingBehavior
} }
}); });
@@ -248,6 +271,26 @@ export default function AWSSecretManagerCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Mapping Behavior">
<Select
value={selectedMappingBehavior}
onValueChange={(val) => {
setSelectedMappingBehavior(val as IntegrationMappingBehavior);
}}
className="w-full border border-mineshaft-500 text-left"
>
{mappingBehaviors.map((option) => (
<SelectItem
value={option.value}
className="text-left"
key={`aws-environment-${option.value}`}
>
{option.label}
</SelectItem>
))}
</Select>
</FormControl>
{selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && (
<FormControl <FormControl
label="AWS SM Secret Name" label="AWS SM Secret Name"
errorText={targetSecretNameErrorText} errorText={targetSecretNameErrorText}
@@ -261,6 +304,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
onChange={(e) => setTargetSecretName(e.target.value)} onChange={(e) => setTargetSecretName(e.target.value)}
/> />
</FormControl> </FormControl>
)}
</motion.div> </motion.div>
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Options}> <TabPanel value={TabSections.Options}>
@@ -21,6 +21,7 @@ import {
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useSyncIntegration } from "@app/hooks/api/integrations/queries"; import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
import { TIntegration } from "@app/hooks/api/types"; import { TIntegration } from "@app/hooks/api/types";
type Props = { type Props = {
@@ -131,6 +132,10 @@ export const IntegrationsSection = ({
</div> </div>
</div> </div>
)} )}
{!(
integration.integration === "aws-secret-manager" &&
integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE
) && (
<div className="ml-2 flex flex-col"> <div className="ml-2 flex flex-col">
<FormLabel <FormLabel
label={ label={
@@ -155,6 +160,7 @@ export const IntegrationsSection = ({
integration.app} integration.app}
</div> </div>
</div> </div>
)}
{(integration.integration === "vercel" || {(integration.integration === "vercel" ||
integration.integration === "netlify" || integration.integration === "netlify" ||
integration.integration === "railway" || integration.integration === "railway" ||