feat: updated integration api and queue to support aws secret manager assume role feature

This commit is contained in:
=
2024-06-26 22:33:49 +05:30
parent 2e3aedc62b
commit 58f6c6b409
12 changed files with 1207 additions and 12 deletions
+1059
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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");
});
}
}
+4 -1
View File
@@ -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>;
+1
View File
@@ -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."
+3
View File
@@ -101,6 +101,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) {
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({ 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 || ""