mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: updated integration api and queue to support aws secret manager assume role feature
This commit is contained in:
1059
backend/package-lock.json
generated
1059
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-iam": "^3.525.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||
"@aws-sdk/client-sts": "^3.600.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@fastify/cookie": "^9.3.1",
|
||||
"@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(),
|
||||
projectId: z.string(),
|
||||
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>;
|
||||
|
||||
@@ -692,6 +692,7 @@ export const INTEGRATION_AUTH = {
|
||||
integration: "The slug of integration for the auth object.",
|
||||
accessId: "The unique authorized access id 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: "",
|
||||
namespace: "",
|
||||
refreshToken: "The refresh token for integration authorization."
|
||||
|
||||
@@ -101,6 +101,9 @@ const envSchema = z
|
||||
// azure
|
||||
CLIENT_ID_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
|
||||
CLIENT_ID_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),
|
||||
accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId),
|
||||
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),
|
||||
namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace),
|
||||
refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken)
|
||||
|
||||
@@ -178,7 +178,8 @@ export const integrationAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
accessId,
|
||||
namespace,
|
||||
accessToken
|
||||
accessToken,
|
||||
awsAssumeIamRoleArn
|
||||
}: TSaveIntegrationAccessTokenDTO) => {
|
||||
if (!Object.values(Integrations).includes(integration as Integrations))
|
||||
throw new BadRequestError({ message: "Invalid integration" });
|
||||
@@ -230,7 +231,7 @@ export const integrationAuthServiceFactory = ({
|
||||
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
|
||||
}
|
||||
|
||||
if (!refreshToken && (accessId || accessToken)) {
|
||||
if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) {
|
||||
if (accessToken) {
|
||||
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessToken, key);
|
||||
updateDoc.accessIV = accessEncToken.iv;
|
||||
@@ -243,6 +244,12 @@ export const integrationAuthServiceFactory = ({
|
||||
updateDoc.accessIdTag = accessEncToken.tag;
|
||||
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);
|
||||
};
|
||||
@@ -251,6 +258,14 @@ export const integrationAuthServiceFactory = ({
|
||||
const getIntegrationAccessToken = async (integrationAuth: TIntegrationAuths, botKey: string) => {
|
||||
let accessToken: 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) {
|
||||
accessToken = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: integrationAuth.accessCiphertext,
|
||||
|
||||
@@ -17,6 +17,7 @@ export type TSaveIntegrationAccessTokenDTO = {
|
||||
url?: string;
|
||||
namespace?: string;
|
||||
refreshToken?: string;
|
||||
awsAssumeIamRoleArn?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
|
||||
|
||||
@@ -17,14 +17,17 @@ import {
|
||||
UntagResourceCommand,
|
||||
UpdateSecretCommand
|
||||
} from "@aws-sdk/client-secrets-manager";
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import AWS, { AWSError } from "aws-sdk";
|
||||
import { AxiosError } from "axios";
|
||||
import { randomUUID } from "crypto";
|
||||
import sodium from "libsodium-wrappers";
|
||||
import isEqual from "lodash.isequal";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -695,24 +698,61 @@ const syncSecretsAWSSecretManager = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
accessToken,
|
||||
awsAssumeRoleArn,
|
||||
projectId
|
||||
}: {
|
||||
integration: TIntegrations;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
awsAssumeRoleArn: string | null;
|
||||
projectId?: string;
|
||||
}) => {
|
||||
const appCfg = getConfig();
|
||||
const metadata = z.record(z.any()).parse(integration.metadata || {});
|
||||
|
||||
if (!accessId) {
|
||||
throw new Error("AWS access ID is required");
|
||||
if (!accessId && !awsAssumeRoleArn) {
|
||||
throw new Error("AWS access ID/AWS Assume Role is required");
|
||||
}
|
||||
|
||||
let accessKeyId = "";
|
||||
let secretAccessKey = "";
|
||||
let sessionToken;
|
||||
if (awsAssumeRoleArn) {
|
||||
if (!appCfg.CLIENT_ID_AWS_INTEGRATION || !appCfg.CLIENT_SECRET_AWS_INTEGRATION)
|
||||
throw new Error("Missing AWS Credentials for Assuming Role. Contact server admin.");
|
||||
|
||||
const client = new STSClient({
|
||||
region: integration.region as string,
|
||||
credentials: {
|
||||
accessKeyId: appCfg.CLIENT_ID_AWS_INTEGRATION,
|
||||
secretAccessKey: appCfg.CLIENT_SECRET_AWS_INTEGRATION
|
||||
}
|
||||
});
|
||||
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({
|
||||
region: integration.region as string,
|
||||
credentials: {
|
||||
accessKeyId: accessId,
|
||||
secretAccessKey: accessToken
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3568,7 +3608,9 @@ export const syncIntegrationSecrets = async ({
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken,
|
||||
appendices
|
||||
awsAssumeRoleArn,
|
||||
appendices,
|
||||
projectId
|
||||
}: {
|
||||
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
||||
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
||||
@@ -3585,8 +3627,10 @@ export const syncIntegrationSecrets = async ({
|
||||
integrationAuth: TIntegrationAuths;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessId: string | null;
|
||||
awsAssumeRoleArn: string | null;
|
||||
accessToken: string;
|
||||
appendices?: { prefix: string; suffix: string };
|
||||
projectId?: string;
|
||||
}) => {
|
||||
let response: { isSynced: boolean; syncMessage: string } | null = null;
|
||||
|
||||
@@ -3620,7 +3664,9 @@ export const syncIntegrationSecrets = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
accessToken,
|
||||
awsAssumeRoleArn,
|
||||
projectId
|
||||
});
|
||||
break;
|
||||
case Integrations.HEROKU:
|
||||
|
||||
@@ -120,7 +120,10 @@ export const integrationDALFactory = (db: TDbClient) => {
|
||||
db.ref("accessExpiresAt").withSchema(TableName.IntegrationAuth).as("accessExpiresAtAu"),
|
||||
db.ref("metadata").withSchema(TableName.IntegrationAuth).as("metadataAu"),
|
||||
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(
|
||||
({
|
||||
@@ -146,6 +149,9 @@ export const integrationDALFactory = (db: TDbClient) => {
|
||||
algorithmAu: algorithm,
|
||||
keyEncodingAu: keyEncoding,
|
||||
accessExpiresAtAu: accessExpiresAt,
|
||||
awsAssumeIamRoleArnIV,
|
||||
awsAssumeIamRoleArnCipherText,
|
||||
awsAssumeIamRoleArnTag,
|
||||
...el
|
||||
}) => ({
|
||||
...el,
|
||||
@@ -174,7 +180,10 @@ export const integrationDALFactory = (db: TDbClient) => {
|
||||
metadata,
|
||||
algorithm,
|
||||
keyEncoding,
|
||||
accessExpiresAt
|
||||
accessExpiresAt,
|
||||
awsAssumeIamRoleArnIV,
|
||||
awsAssumeIamRoleArnCipherText,
|
||||
awsAssumeIamRoleArnTag
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -525,6 +525,18 @@ export const secretQueueFactory = ({
|
||||
|
||||
const botKey = await projectBotService.getBotKey(projectId);
|
||||
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({
|
||||
environment,
|
||||
projectId,
|
||||
@@ -544,6 +556,8 @@ export const secretQueueFactory = ({
|
||||
}
|
||||
|
||||
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({
|
||||
createManySecretsRawFn,
|
||||
updateManySecretsRawFn,
|
||||
@@ -552,7 +566,9 @@ export const secretQueueFactory = ({
|
||||
integrationAuth,
|
||||
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
|
||||
accessId: accessId as string,
|
||||
awsAssumeRoleArn,
|
||||
accessToken,
|
||||
projectId,
|
||||
appendices: {
|
||||
prefix: metadata?.secretPrefix || "",
|
||||
suffix: metadata?.secretSuffix || ""
|
||||
|
||||
Reference in New Issue
Block a user