mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 23:28:25 +00:00
Merge pull request #2027 from akhilmhdh/feat/secret-manager-integration-auth
AWS Secret Manager assume role based integration
This commit is contained in:
Generated
+1059
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-iam": "^3.525.0",
|
"@aws-sdk/client-iam": "^3.525.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||||
|
"@aws-sdk/client-sts": "^3.600.0",
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@fastify/cookie": "^9.3.1",
|
"@fastify/cookie": "^9.3.1",
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasAwsAssumeRoleCipherText = await knex.schema.hasColumn(
|
||||||
|
TableName.IntegrationAuth,
|
||||||
|
"awsAssumeIamRoleArnCipherText"
|
||||||
|
);
|
||||||
|
const hasAwsAssumeRoleIV = await knex.schema.hasColumn(TableName.IntegrationAuth, "awsAssumeIamRoleArnIV");
|
||||||
|
const hasAwsAssumeRoleTag = await knex.schema.hasColumn(TableName.IntegrationAuth, "awsAssumeIamRoleArnTag");
|
||||||
|
if (await knex.schema.hasTable(TableName.IntegrationAuth)) {
|
||||||
|
await knex.schema.alterTable(TableName.IntegrationAuth, (t) => {
|
||||||
|
if (!hasAwsAssumeRoleCipherText) t.text("awsAssumeIamRoleArnCipherText");
|
||||||
|
if (!hasAwsAssumeRoleIV) t.text("awsAssumeIamRoleArnIV");
|
||||||
|
if (!hasAwsAssumeRoleTag) t.text("awsAssumeIamRoleArnTag");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasAwsAssumeRoleCipherText = await knex.schema.hasColumn(
|
||||||
|
TableName.IntegrationAuth,
|
||||||
|
"awsAssumeIamRoleArnCipherText"
|
||||||
|
);
|
||||||
|
const hasAwsAssumeRoleIV = await knex.schema.hasColumn(TableName.IntegrationAuth, "awsAssumeIamRoleArnIV");
|
||||||
|
const hasAwsAssumeRoleTag = await knex.schema.hasColumn(TableName.IntegrationAuth, "awsAssumeIamRoleArnTag");
|
||||||
|
if (await knex.schema.hasTable(TableName.IntegrationAuth)) {
|
||||||
|
await knex.schema.alterTable(TableName.IntegrationAuth, (t) => {
|
||||||
|
if (hasAwsAssumeRoleCipherText) t.dropColumn("awsAssumeIamRoleArnCipherText");
|
||||||
|
if (hasAwsAssumeRoleIV) t.dropColumn("awsAssumeIamRoleArnIV");
|
||||||
|
if (hasAwsAssumeRoleTag) t.dropColumn("awsAssumeIamRoleArnTag");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,10 @@ export const IntegrationAuthsSchema = z.object({
|
|||||||
keyEncoding: z.string(),
|
keyEncoding: z.string(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
awsAssumeIamRoleArnCipherText: z.string().nullable().optional(),
|
||||||
|
awsAssumeIamRoleArnIV: z.string().nullable().optional(),
|
||||||
|
awsAssumeIamRoleArnTag: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TIntegrationAuths = z.infer<typeof IntegrationAuthsSchema>;
|
export type TIntegrationAuths = z.infer<typeof IntegrationAuthsSchema>;
|
||||||
|
|||||||
@@ -692,6 +692,7 @@ export const INTEGRATION_AUTH = {
|
|||||||
integration: "The slug of integration for the auth object.",
|
integration: "The slug of integration for the auth object.",
|
||||||
accessId: "The unique authorized access id of the external integration provider.",
|
accessId: "The unique authorized access id of the external integration provider.",
|
||||||
accessToken: "The unique authorized access token of the external integration provider.",
|
accessToken: "The unique authorized access token of the external integration provider.",
|
||||||
|
awsAssumeIamRoleArn: "The AWS IAM Role to be assumed by Infisical",
|
||||||
url: "",
|
url: "",
|
||||||
namespace: "",
|
namespace: "",
|
||||||
refreshToken: "The refresh token for integration authorization."
|
refreshToken: "The refresh token for integration authorization."
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ const envSchema = z
|
|||||||
// azure
|
// azure
|
||||||
CLIENT_ID_AZURE: zpStr(z.string().optional()),
|
CLIENT_ID_AZURE: zpStr(z.string().optional()),
|
||||||
CLIENT_SECRET_AZURE: zpStr(z.string().optional()),
|
CLIENT_SECRET_AZURE: zpStr(z.string().optional()),
|
||||||
|
// aws
|
||||||
|
CLIENT_ID_AWS_INTEGRATION: zpStr(z.string().optional()),
|
||||||
|
CLIENT_SECRET_AWS_INTEGRATION: zpStr(z.string().optional()),
|
||||||
// gitlab
|
// gitlab
|
||||||
CLIENT_ID_GITLAB: zpStr(z.string().optional()),
|
CLIENT_ID_GITLAB: zpStr(z.string().optional()),
|
||||||
CLIENT_SECRET_GITLAB: zpStr(z.string().optional()),
|
CLIENT_SECRET_GITLAB: zpStr(z.string().optional()),
|
||||||
|
|||||||
@@ -240,6 +240,12 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
|||||||
integration: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.integration),
|
integration: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.integration),
|
||||||
accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId),
|
accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId),
|
||||||
accessToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessToken),
|
accessToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessToken),
|
||||||
|
awsAssumeIamRoleArn: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.trim()
|
||||||
|
.optional()
|
||||||
|
.describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.awsAssumeIamRoleArn),
|
||||||
url: z.string().url().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.url),
|
url: z.string().url().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.url),
|
||||||
namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace),
|
namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace),
|
||||||
refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken)
|
refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken)
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ export const integrationAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
accessId,
|
accessId,
|
||||||
namespace,
|
namespace,
|
||||||
accessToken
|
accessToken,
|
||||||
|
awsAssumeIamRoleArn
|
||||||
}: TSaveIntegrationAccessTokenDTO) => {
|
}: TSaveIntegrationAccessTokenDTO) => {
|
||||||
if (!Object.values(Integrations).includes(integration as Integrations))
|
if (!Object.values(Integrations).includes(integration as Integrations))
|
||||||
throw new BadRequestError({ message: "Invalid integration" });
|
throw new BadRequestError({ message: "Invalid integration" });
|
||||||
@@ -230,7 +231,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
|
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!refreshToken && (accessId || accessToken)) {
|
if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) {
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessToken, key);
|
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessToken, key);
|
||||||
updateDoc.accessIV = accessEncToken.iv;
|
updateDoc.accessIV = accessEncToken.iv;
|
||||||
@@ -243,6 +244,12 @@ export const integrationAuthServiceFactory = ({
|
|||||||
updateDoc.accessIdTag = accessEncToken.tag;
|
updateDoc.accessIdTag = accessEncToken.tag;
|
||||||
updateDoc.accessIdCiphertext = accessEncToken.ciphertext;
|
updateDoc.accessIdCiphertext = accessEncToken.ciphertext;
|
||||||
}
|
}
|
||||||
|
if (awsAssumeIamRoleArn) {
|
||||||
|
const awsAssumeIamRoleArnEnc = encryptSymmetric128BitHexKeyUTF8(awsAssumeIamRoleArn, key);
|
||||||
|
updateDoc.awsAssumeIamRoleArnCipherText = awsAssumeIamRoleArnEnc.ciphertext;
|
||||||
|
updateDoc.awsAssumeIamRoleArnIV = awsAssumeIamRoleArnEnc.iv;
|
||||||
|
updateDoc.awsAssumeIamRoleArnTag = awsAssumeIamRoleArnEnc.tag;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return integrationAuthDAL.create(updateDoc);
|
return integrationAuthDAL.create(updateDoc);
|
||||||
};
|
};
|
||||||
@@ -251,6 +258,14 @@ export const integrationAuthServiceFactory = ({
|
|||||||
const getIntegrationAccessToken = async (integrationAuth: TIntegrationAuths, botKey: string) => {
|
const getIntegrationAccessToken = async (integrationAuth: TIntegrationAuths, botKey: string) => {
|
||||||
let accessToken: string | undefined;
|
let accessToken: string | undefined;
|
||||||
let accessId: string | undefined;
|
let accessId: string | undefined;
|
||||||
|
// this means its not access token based
|
||||||
|
if (
|
||||||
|
integrationAuth.integration === Integrations.AWS_SECRET_MANAGER &&
|
||||||
|
integrationAuth.awsAssumeIamRoleArnCipherText
|
||||||
|
) {
|
||||||
|
return { accessToken: "", accessId: "" };
|
||||||
|
}
|
||||||
|
|
||||||
if (integrationAuth.accessTag && integrationAuth.accessIV && integrationAuth.accessCiphertext) {
|
if (integrationAuth.accessTag && integrationAuth.accessIV && integrationAuth.accessCiphertext) {
|
||||||
accessToken = decryptSymmetric128BitHexKeyUTF8({
|
accessToken = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: integrationAuth.accessCiphertext,
|
ciphertext: integrationAuth.accessCiphertext,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type TSaveIntegrationAccessTokenDTO = {
|
|||||||
url?: string;
|
url?: string;
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
|
awsAssumeIamRoleArn?: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
|
export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
|
||||||
|
|||||||
@@ -17,14 +17,17 @@ import {
|
|||||||
UntagResourceCommand,
|
UntagResourceCommand,
|
||||||
UpdateSecretCommand
|
UpdateSecretCommand
|
||||||
} from "@aws-sdk/client-secrets-manager";
|
} from "@aws-sdk/client-secrets-manager";
|
||||||
|
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import AWS, { AWSError } from "aws-sdk";
|
import AWS, { AWSError } from "aws-sdk";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
import sodium from "libsodium-wrappers";
|
import sodium from "libsodium-wrappers";
|
||||||
import isEqual from "lodash.isequal";
|
import isEqual from "lodash.isequal";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas";
|
import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
@@ -695,24 +698,61 @@ const syncSecretsAWSSecretManager = async ({
|
|||||||
integration,
|
integration,
|
||||||
secrets,
|
secrets,
|
||||||
accessId,
|
accessId,
|
||||||
accessToken
|
accessToken,
|
||||||
|
awsAssumeRoleArn,
|
||||||
|
projectId
|
||||||
}: {
|
}: {
|
||||||
integration: TIntegrations;
|
integration: TIntegrations;
|
||||||
secrets: Record<string, { value: string; comment?: string }>;
|
secrets: Record<string, { value: string; comment?: string }>;
|
||||||
accessId: string | null;
|
accessId: string | null;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
awsAssumeRoleArn: string | null;
|
||||||
|
projectId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const metadata = z.record(z.any()).parse(integration.metadata || {});
|
const metadata = z.record(z.any()).parse(integration.metadata || {});
|
||||||
|
|
||||||
if (!accessId) {
|
if (!accessId && !awsAssumeRoleArn) {
|
||||||
throw new Error("AWS access ID is required");
|
throw new Error("AWS access ID/AWS Assume Role is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
let accessKeyId = "";
|
||||||
|
let secretAccessKey = "";
|
||||||
|
let sessionToken;
|
||||||
|
if (awsAssumeRoleArn) {
|
||||||
|
const client = new STSClient({
|
||||||
|
region: integration.region as string,
|
||||||
|
credentials:
|
||||||
|
appCfg.CLIENT_ID_AWS_INTEGRATION && appCfg.CLIENT_SECRET_AWS_INTEGRATION
|
||||||
|
? {
|
||||||
|
accessKeyId: appCfg.CLIENT_ID_AWS_INTEGRATION,
|
||||||
|
secretAccessKey: appCfg.CLIENT_SECRET_AWS_INTEGRATION
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
const command = new AssumeRoleCommand({
|
||||||
|
RoleArn: awsAssumeRoleArn,
|
||||||
|
RoleSessionName: `infisical-sm-${randomUUID()}`,
|
||||||
|
DurationSeconds: 900, // 15mins
|
||||||
|
ExternalId: projectId
|
||||||
|
});
|
||||||
|
const response = await client.send(command);
|
||||||
|
if (!response.Credentials?.AccessKeyId || !response.Credentials?.SecretAccessKey)
|
||||||
|
throw new Error("Failed to assume role");
|
||||||
|
accessKeyId = response.Credentials?.AccessKeyId;
|
||||||
|
secretAccessKey = response.Credentials?.SecretAccessKey;
|
||||||
|
sessionToken = response.Credentials?.SessionToken;
|
||||||
|
} else {
|
||||||
|
accessKeyId = accessId as string;
|
||||||
|
secretAccessKey = accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretsManager = new SecretsManagerClient({
|
const secretsManager = new SecretsManagerClient({
|
||||||
region: integration.region as string,
|
region: integration.region as string,
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: accessId,
|
accessKeyId,
|
||||||
secretAccessKey: accessToken
|
secretAccessKey,
|
||||||
|
sessionToken
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3568,7 +3608,9 @@ export const syncIntegrationSecrets = async ({
|
|||||||
secrets,
|
secrets,
|
||||||
accessId,
|
accessId,
|
||||||
accessToken,
|
accessToken,
|
||||||
appendices
|
awsAssumeRoleArn,
|
||||||
|
appendices,
|
||||||
|
projectId
|
||||||
}: {
|
}: {
|
||||||
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
||||||
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
||||||
@@ -3585,8 +3627,10 @@ export const syncIntegrationSecrets = async ({
|
|||||||
integrationAuth: TIntegrationAuths;
|
integrationAuth: TIntegrationAuths;
|
||||||
secrets: Record<string, { value: string; comment?: string }>;
|
secrets: Record<string, { value: string; comment?: string }>;
|
||||||
accessId: string | null;
|
accessId: string | null;
|
||||||
|
awsAssumeRoleArn: string | null;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
appendices?: { prefix: string; suffix: string };
|
appendices?: { prefix: string; suffix: string };
|
||||||
|
projectId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
let response: { isSynced: boolean; syncMessage: string } | null = null;
|
let response: { isSynced: boolean; syncMessage: string } | null = null;
|
||||||
|
|
||||||
@@ -3620,7 +3664,9 @@ export const syncIntegrationSecrets = async ({
|
|||||||
integration,
|
integration,
|
||||||
secrets,
|
secrets,
|
||||||
accessId,
|
accessId,
|
||||||
accessToken
|
accessToken,
|
||||||
|
awsAssumeRoleArn,
|
||||||
|
projectId
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case Integrations.HEROKU:
|
case Integrations.HEROKU:
|
||||||
|
|||||||
@@ -120,7 +120,10 @@ export const integrationDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("accessExpiresAt").withSchema(TableName.IntegrationAuth).as("accessExpiresAtAu"),
|
db.ref("accessExpiresAt").withSchema(TableName.IntegrationAuth).as("accessExpiresAtAu"),
|
||||||
db.ref("metadata").withSchema(TableName.IntegrationAuth).as("metadataAu"),
|
db.ref("metadata").withSchema(TableName.IntegrationAuth).as("metadataAu"),
|
||||||
db.ref("algorithm").withSchema(TableName.IntegrationAuth).as("algorithmAu"),
|
db.ref("algorithm").withSchema(TableName.IntegrationAuth).as("algorithmAu"),
|
||||||
db.ref("keyEncoding").withSchema(TableName.IntegrationAuth).as("keyEncodingAu")
|
db.ref("keyEncoding").withSchema(TableName.IntegrationAuth).as("keyEncodingAu"),
|
||||||
|
db.ref("awsAssumeIamRoleArnCipherText").withSchema(TableName.IntegrationAuth),
|
||||||
|
db.ref("awsAssumeIamRoleArnIV").withSchema(TableName.IntegrationAuth),
|
||||||
|
db.ref("awsAssumeIamRoleArnTag").withSchema(TableName.IntegrationAuth)
|
||||||
);
|
);
|
||||||
return docs.map(
|
return docs.map(
|
||||||
({
|
({
|
||||||
@@ -146,6 +149,9 @@ export const integrationDALFactory = (db: TDbClient) => {
|
|||||||
algorithmAu: algorithm,
|
algorithmAu: algorithm,
|
||||||
keyEncodingAu: keyEncoding,
|
keyEncodingAu: keyEncoding,
|
||||||
accessExpiresAtAu: accessExpiresAt,
|
accessExpiresAtAu: accessExpiresAt,
|
||||||
|
awsAssumeIamRoleArnIV,
|
||||||
|
awsAssumeIamRoleArnCipherText,
|
||||||
|
awsAssumeIamRoleArnTag,
|
||||||
...el
|
...el
|
||||||
}) => ({
|
}) => ({
|
||||||
...el,
|
...el,
|
||||||
@@ -174,7 +180,10 @@ export const integrationDALFactory = (db: TDbClient) => {
|
|||||||
metadata,
|
metadata,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding,
|
keyEncoding,
|
||||||
accessExpiresAt
|
accessExpiresAt,
|
||||||
|
awsAssumeIamRoleArnIV,
|
||||||
|
awsAssumeIamRoleArnCipherText,
|
||||||
|
awsAssumeIamRoleArnTag
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -525,6 +525,18 @@ export const secretQueueFactory = ({
|
|||||||
|
|
||||||
const botKey = await projectBotService.getBotKey(projectId);
|
const botKey = await projectBotService.getBotKey(projectId);
|
||||||
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(integrationAuth, botKey);
|
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(integrationAuth, botKey);
|
||||||
|
const awsAssumeRoleArn =
|
||||||
|
integrationAuth.awsAssumeIamRoleArnTag &&
|
||||||
|
integrationAuth.awsAssumeIamRoleArnIV &&
|
||||||
|
integrationAuth.awsAssumeIamRoleArnCipherText
|
||||||
|
? decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText,
|
||||||
|
iv: integrationAuth.awsAssumeIamRoleArnIV,
|
||||||
|
tag: integrationAuth.awsAssumeIamRoleArnTag,
|
||||||
|
key: botKey
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
const secrets = await getIntegrationSecrets({
|
const secrets = await getIntegrationSecrets({
|
||||||
environment,
|
environment,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -544,6 +556,8 @@ export const secretQueueFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// akhilmhdh: this needs to changed later to be more easier to use
|
||||||
|
// at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places
|
||||||
const response = await syncIntegrationSecrets({
|
const response = await syncIntegrationSecrets({
|
||||||
createManySecretsRawFn,
|
createManySecretsRawFn,
|
||||||
updateManySecretsRawFn,
|
updateManySecretsRawFn,
|
||||||
@@ -552,7 +566,9 @@ export const secretQueueFactory = ({
|
|||||||
integrationAuth,
|
integrationAuth,
|
||||||
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
|
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
|
||||||
accessId: accessId as string,
|
accessId: accessId as string,
|
||||||
|
awsAssumeRoleArn,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
projectId,
|
||||||
appendices: {
|
appendices: {
|
||||||
prefix: metadata?.secretPrefix || "",
|
prefix: metadata?.secretPrefix || "",
|
||||||
suffix: metadata?.secretSuffix || ""
|
suffix: metadata?.secretSuffix || ""
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 43 KiB |
@@ -3,6 +3,156 @@ title: "AWS Secrets Manager"
|
|||||||
description: "Learn how to sync secrets from Infisical to AWS Secrets Manager."
|
description: "Learn how to sync secrets from Infisical to AWS Secrets Manager."
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Assume Role (Recommended)">
|
||||||
|
Infisical will assume the provided role in your AWS account securely, without the need to share any credentials.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
<Accordion title="Self-Hosted Users">
|
||||||
|
To connect your Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the AWS IAM Role for the integration.
|
||||||
|
|
||||||
|
If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2.
|
||||||
|
|
||||||
|
The following steps are for instances not deployed on AWS
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create an IAM User">
|
||||||
|
Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console.
|
||||||
|
</Step>
|
||||||
|
<Step title="Create an Inline Policy">
|
||||||
|
Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "AllowAssumeAnyRole",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": "sts:AssumeRole",
|
||||||
|
"Resource": "arn:aws:iam::*:role/*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step title="Obtain the IAM User Credentials">
|
||||||
|
Obtain the AWS access key ID and secret access key for your IAM User by navigating to IAM > Users > [Your User] > Security credentials > Access keys.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Set Up Integration Keys">
|
||||||
|
1. Set the access key as **CLIENT_ID_AWS_INTEGRATION**.
|
||||||
|
2. Set the secret key as **CLIENT_SECRET_AWS_INTEGRATION**.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create the Managing User IAM Role for AWS Secrets Manager">
|
||||||
|
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||||
|
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
||||||
|
4. Optionally, enable **Require external ID** and enter your **project ID** to further enhance security.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Add Required Permissions for the IAM Role">
|
||||||
|

|
||||||
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "AllowSecretsManagerAccess",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"secretsmanager:GetSecretValue",
|
||||||
|
"secretsmanager:CreateSecret",
|
||||||
|
"secretsmanager:UpdateSecret",
|
||||||
|
"secretsmanager:DescribeSecret",
|
||||||
|
"secretsmanager:TagResource",
|
||||||
|
"secretsmanager:UntagResource",
|
||||||
|
"kms:ListKeys",
|
||||||
|
"kms:ListAliases",
|
||||||
|
"kms:Encrypt",
|
||||||
|
"kms:Decrypt"
|
||||||
|
],
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Copy the AWS IAM Role ARN">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Authorize Infisical for AWS Secrets Manager">
|
||||||
|
1. Navigate to your project's integrations tab in Infisical.
|
||||||
|
2. Click on the **AWS Secrets Manager** tile.
|
||||||
|

|
||||||
|
|
||||||
|
3. Select the **AWS Assume Role** option.
|
||||||
|

|
||||||
|
|
||||||
|
4. Provide the **AWS IAM Role ARN** obtained from the previous step.
|
||||||
|
</Step> <Step title="Start integration">
|
||||||
|
Select how you want to integration to work by specifying a number of parameters:
|
||||||
|
|
||||||
|
<ParamField path="Project Environment" type="string" required>
|
||||||
|
The environment in Infisical from which you want to sync secrets to AWS Secrets Manager.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Secrets Path" type="string" required>
|
||||||
|
The path within the preselected environment form which you want to sync secrets to AWS Secrets Manager.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="AWS Region" type="string" required>
|
||||||
|
The region that you want to integrate with in AWS Secrets Manager.
|
||||||
|
</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>
|
||||||
|
The secret name/path in AWS into which you want to sync the secrets from Infisical.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Optionally, you can add tags or specify the encryption key of all the secrets created via this integration:
|
||||||
|
|
||||||
|
<ParamField path="Secret Tag" type="string" optional>
|
||||||
|
The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Encryption Key" type="string" optional>
|
||||||
|
The alias/ID of the AWS KMS key used for encryption. Please note that key should be enabled in order to work and the IAM user should have access to it.
|
||||||
|
</ParamField>
|
||||||
|

|
||||||
|
|
||||||
|
Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Infisical currently syncs environment variables to AWS Secrets Manager as
|
||||||
|
key-value pairs under one secret. We're actively exploring ways to help users
|
||||||
|
group environment variable key-pairs under multiple secrets for greater
|
||||||
|
control.
|
||||||
|
</Info>
|
||||||
|
<Info>
|
||||||
|
Please note that upon deleting secrets in Infisical, AWS Secrets Manager immediately makes the secrets inaccessible but only schedules them for deletion after at least 7 days.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Access Key">
|
||||||
|
Infisical will access your account using the provided AWS access key and secret key.
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
@@ -51,13 +201,13 @@ Prerequisites:
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
Navigate to your project's integrations tab in Infisical.
|
1. Navigate to your project's integrations tab in Infisical.
|
||||||
|
2. Click on the **AWS Secrets Manager** tile.
|
||||||
|

|
||||||
|
|
||||||

|
3. Select the **Access Key** option for Authentication Mode.
|
||||||
|

|
||||||
Press on the AWS Secrets Manager tile and input your AWS access key ID and secret access key from the previous step.
|
4. Provide the **access key** and **secret key** for the AWS Iam User.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Start integration">
|
<Step title="Start integration">
|
||||||
@@ -105,3 +255,5 @@ Prerequisites:
|
|||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|||||||
@@ -464,6 +464,16 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="AWS">
|
||||||
|
<ParamField query="CLIENT_ID_AWS_INTEGRATION" type="string" default="none" optional>
|
||||||
|
The AWS IAM User access key for assuming roles.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="CLIENT_SECRET_AWS_INTEGRATION" type="string" default="none" optional>
|
||||||
|
The AWS IAM User secret key for assuming roles.
|
||||||
|
</ParamField>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="Azure">
|
<Accordion title="Azure">
|
||||||
<ParamField query="CLIENT_ID_AZURE" type="string" default="none" optional>
|
<ParamField query="CLIENT_ID_AZURE" type="string" default="none" optional>
|
||||||
OAuth2 client id for Azure integration
|
OAuth2 client id for Azure integration
|
||||||
|
|||||||
@@ -802,6 +802,7 @@ export const useSaveIntegrationAccessToken = () => {
|
|||||||
refreshToken,
|
refreshToken,
|
||||||
accessId,
|
accessId,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
awsAssumeIamRoleArn,
|
||||||
url,
|
url,
|
||||||
namespace
|
namespace
|
||||||
}: {
|
}: {
|
||||||
@@ -810,6 +811,7 @@ export const useSaveIntegrationAccessToken = () => {
|
|||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
accessId?: string;
|
accessId?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
awsAssumeIamRoleArn?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -821,6 +823,7 @@ export const useSaveIntegrationAccessToken = () => {
|
|||||||
refreshToken,
|
refreshToken,
|
||||||
accessId,
|
accessId,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
awsAssumeIamRoleArn,
|
||||||
url,
|
url,
|
||||||
namespace
|
namespace
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,54 +1,70 @@
|
|||||||
import { useState } from "react";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
|
enum AwsAuthType {
|
||||||
|
AccessKey = "access-key",
|
||||||
|
AssumeRole = "assume-role"
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSchema = z.discriminatedUnion("type", [
|
||||||
|
z.object({
|
||||||
|
type: z.literal(AwsAuthType.AccessKey),
|
||||||
|
accessKey: z.string().min(1),
|
||||||
|
accessSecretKey: z.string().min(1)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(AwsAuthType.AssumeRole),
|
||||||
|
iamRoleArn: z.string().min(1)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export default function AWSSecretManagerCreateIntegrationPage() {
|
export default function AWSSecretManagerCreateIntegrationPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const { control, handleSubmit, formState, watch } = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
type: AwsAuthType.AccessKey
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const formAwsAuthTypeField = watch("type");
|
||||||
|
|
||||||
const [accessKey, setAccessKey] = useState("");
|
const handleFormSubmit = async (data: TForm) => {
|
||||||
const [accessKeyErrorText, setAccessKeyErrorText] = useState("");
|
|
||||||
const [accessSecretKey, setAccessSecretKey] = useState("");
|
|
||||||
const [accessSecretKeyErrorText, setAccessSecretKeyErrorText] = useState("");
|
|
||||||
|
|
||||||
const handleButtonClick = async () => {
|
|
||||||
try {
|
try {
|
||||||
setAccessKeyErrorText("");
|
|
||||||
setAccessSecretKeyErrorText("");
|
|
||||||
|
|
||||||
if (accessKey.length === 0) {
|
|
||||||
setAccessKeyErrorText("Access key cannot be blank");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accessSecretKey.length === 0) {
|
|
||||||
setAccessSecretKeyErrorText("Secret access key cannot be blank");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const integrationAuth = await mutateAsync({
|
const integrationAuth = await mutateAsync({
|
||||||
workspaceId: localStorage.getItem("projectData.id"),
|
workspaceId: localStorage.getItem("projectData.id"),
|
||||||
integration: "aws-secret-manager",
|
integration: "aws-secret-manager",
|
||||||
accessId: accessKey,
|
...(data.type === AwsAuthType.AssumeRole
|
||||||
accessToken: accessSecretKey
|
? {
|
||||||
|
awsAssumeIamRoleArn: data.iamRoleArn
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
accessId: data.accessKey,
|
||||||
|
accessToken: data.accessSecretKey
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
setAccessKey("");
|
|
||||||
setAccessSecretKey("");
|
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
router.push(
|
router.push(
|
||||||
`/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth.id}`
|
`/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth.id}`
|
||||||
);
|
);
|
||||||
@@ -69,7 +85,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
|||||||
subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment."
|
subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<div className="inline flex items-center">
|
<div className="flex items-center">
|
||||||
<Image
|
<Image
|
||||||
src="/images/integrations/Amazon Web Services.png"
|
src="/images/integrations/Amazon Web Services.png"
|
||||||
height={35}
|
height={35}
|
||||||
@@ -92,37 +108,90 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<FormControl
|
<CardBody>
|
||||||
label="Access Key ID"
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
errorText={accessKeyErrorText}
|
<Controller
|
||||||
isError={accessKeyErrorText !== "" ?? false}
|
control={control}
|
||||||
className="px-6"
|
name="type"
|
||||||
>
|
defaultValue={AwsAuthType.AccessKey}
|
||||||
<Input placeholder="" value={accessKey} onChange={(e) => setAccessKey(e.target.value)} />
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
</FormControl>
|
<FormControl
|
||||||
<FormControl
|
label="Authentication Mode"
|
||||||
label="Secret Access Key"
|
errorText={error?.message}
|
||||||
errorText={accessSecretKeyErrorText}
|
isError={Boolean(error)}
|
||||||
isError={accessSecretKeyErrorText !== "" ?? false}
|
>
|
||||||
className="px-6"
|
<Select
|
||||||
>
|
defaultValue={field.value}
|
||||||
<Input
|
{...field}
|
||||||
placeholder=""
|
onValueChange={(e) => onChange(e)}
|
||||||
type="password"
|
className="w-full"
|
||||||
autoComplete="new-password"
|
>
|
||||||
value={accessSecretKey}
|
<SelectItem value={AwsAuthType.AccessKey}>Access Key</SelectItem>
|
||||||
onChange={(e) => setAccessSecretKey(e.target.value)}
|
<SelectItem value={AwsAuthType.AssumeRole}>AWS Assume Role</SelectItem>
|
||||||
/>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Button
|
)}
|
||||||
onClick={handleButtonClick}
|
/>
|
||||||
colorSchema="primary"
|
{formAwsAuthTypeField === AwsAuthType.AccessKey ? (
|
||||||
variant="outline_bg"
|
<>
|
||||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
<Controller
|
||||||
isLoading={isLoading}
|
control={control}
|
||||||
>
|
name="accessKey"
|
||||||
Connect to AWS Secrets Manager
|
render={({ field, fieldState: { error } }) => (
|
||||||
</Button>
|
<FormControl
|
||||||
|
label="Access Key ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessSecretKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Access Key"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder=""
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="iamRoleArn"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="IAM Role ARN For Role Assumption"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
||||||
|
isLoading={formState.isSubmitting}
|
||||||
|
>
|
||||||
|
Connect to AWS Secrets Manager
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user