feat: added endpoint to update integration auth

This commit is contained in:
=
2024-12-09 19:15:17 +05:30
parent 5ceb30f43f
commit 40d69d4620
7 changed files with 226 additions and 2 deletions
+5
View File
@@ -1032,6 +1032,9 @@ export const INTEGRATION_AUTH = {
DELETE_BY_ID: { DELETE_BY_ID: {
integrationAuthId: "The ID of integration authentication object to delete." integrationAuthId: "The ID of integration authentication object to delete."
}, },
UPDATE_BY_ID: {
integrationAuthId: "The ID of integration authentication object to update."
},
CREATE_ACCESS_TOKEN: { CREATE_ACCESS_TOKEN: {
workspaceId: "The ID of the project to create the integration auth for.", workspaceId: "The ID of the project to create the integration auth for.",
integration: "The slug of integration for the auth object.", integration: "The slug of integration for the auth object.",
@@ -1088,11 +1091,13 @@ export const INTEGRATION = {
}, },
UPDATE: { UPDATE: {
integrationId: "The ID of the integration object.", integrationId: "The ID of the integration object.",
region: "AWS region to sync secrets to.",
app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
appId: appId:
"The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", "The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
isActive: "Whether the integration should be active or disabled.", isActive: "Whether the integration should be active or disabled.",
secretPath: "The path of the secrets to sync secrets from.", secretPath: "The path of the secrets to sync secrets from.",
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault.",
owner: "External integration providers service entity owner. Used in Github.", owner: "External integration providers service entity owner. Used in Github.",
targetEnvironment: targetEnvironment:
"The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.", "The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.",
@@ -82,6 +82,67 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
} }
}); });
server.route({
method: "PATCH",
url: "/:integrationAuthId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update the integration authentication object required for syncing secrets.",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.UPDATE_BY_ID.integrationAuthId)
}),
body: z.object({
integration: z.string().trim().optional().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)
}),
response: {
200: z.object({
integrationAuth: integrationAuthPubSchema
})
}
},
handler: async (req) => {
const integrationAuth = await server.services.integrationAuth.updateIntegrationAuth({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
integrationAuthId: req.query.integrationAuthId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: integrationAuth.projectId,
event: {
type: EventType.AUTHORIZE_INTEGRATION,
metadata: {
integration: integrationAuth.integration
}
}
});
return { integrationAuth };
}
});
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/", url: "/",
@@ -141,7 +141,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment), targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner), owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment), environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment),
metadata: IntegrationMetadataSchema.optional() path: z.string().trim().optional().describe(INTEGRATION.UPDATE.path),
metadata: IntegrationMetadataSchema.optional(),
region: z.string().trim().optional().describe(INTEGRATION.UPDATE.region)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -55,6 +55,7 @@ import {
TOctopusDeployVariableSet, TOctopusDeployVariableSet,
TSaveIntegrationAccessTokenDTO, TSaveIntegrationAccessTokenDTO,
TTeamCityBuildConfig, TTeamCityBuildConfig,
TUpdateIntegrationAuthDTO,
TVercelBranches TVercelBranches
} from "./integration-auth-types"; } from "./integration-auth-types";
import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list"; import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list";
@@ -368,6 +369,149 @@ export const integrationAuthServiceFactory = ({
return integrationAuthDAL.create(updateDoc); return integrationAuthDAL.create(updateDoc);
}; };
const updateIntegrationAuth = async ({
integrationAuthId,
refreshToken,
actorId,
integration: newIntegration,
url,
actor,
actorOrgId,
actorAuthMethod,
accessId,
namespace,
accessToken,
awsAssumeIamRoleArn
}: TUpdateIntegrationAuthDTO) => {
const integrationAuth = await integrationAuthDAL.findById(integrationAuthId);
if (!integrationAuth) {
throw new NotFoundError({ message: `Integration auth with id ${integrationAuthId} not found.` });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
const { projectId } = integrationAuth;
const integration = newIntegration || integrationAuth.integration;
if (!Object.values(Integrations).includes(integration as Integrations))
throw new BadRequestError({ message: "Invalid integration" });
const updateDoc: TIntegrationAuthsInsert = {
projectId,
integration,
namespace,
url,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8,
...(integration === Integrations.GCP_SECRET_MANAGER
? {
metadata: {
authMethod: "serviceAccount"
}
}
: {})
};
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
if (refreshToken) {
const tokenDetails = await exchangeRefresh(
integration,
refreshToken,
url,
updateDoc.metadata as Record<string, string>
);
const refreshEncToken = secretManagerEncryptor({
plainText: Buffer.from(tokenDetails.refreshToken)
}).cipherTextBlob;
updateDoc.encryptedRefresh = refreshEncToken;
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(tokenDetails.accessToken)
}).cipherTextBlob;
updateDoc.encryptedAccess = accessEncToken;
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
}
if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) {
if (accessToken) {
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(accessToken)
}).cipherTextBlob;
updateDoc.encryptedAccess = accessEncToken;
updateDoc.encryptedAwsAssumeIamRoleArn = null;
}
if (accessId) {
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(accessId)
}).cipherTextBlob;
updateDoc.encryptedAccessId = accessEncToken;
updateDoc.encryptedAwsAssumeIamRoleArn = null;
}
if (awsAssumeIamRoleArn) {
const awsAssumeIamRoleArnEncrypted = secretManagerEncryptor({
plainText: Buffer.from(awsAssumeIamRoleArn)
}).cipherTextBlob;
updateDoc.encryptedAwsAssumeIamRoleArn = awsAssumeIamRoleArnEncrypted;
updateDoc.encryptedAccess = null;
updateDoc.encryptedAccessId = null;
}
}
} else {
if (!botKey) throw new NotFoundError({ message: `Project bot key for project with ID '${projectId}' not found` });
if (refreshToken) {
const tokenDetails = await exchangeRefresh(
integration,
refreshToken,
url,
updateDoc.metadata as Record<string, string>
);
const refreshEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.refreshToken, botKey);
updateDoc.refreshIV = refreshEncToken.iv;
updateDoc.refreshTag = refreshEncToken.tag;
updateDoc.refreshCiphertext = refreshEncToken.ciphertext;
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.accessToken, botKey);
updateDoc.accessIV = accessEncToken.iv;
updateDoc.accessTag = accessEncToken.tag;
updateDoc.accessCiphertext = accessEncToken.ciphertext;
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
}
if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) {
if (accessToken) {
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessToken, botKey);
updateDoc.accessIV = accessEncToken.iv;
updateDoc.accessTag = accessEncToken.tag;
updateDoc.accessCiphertext = accessEncToken.ciphertext;
}
if (accessId) {
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessId, botKey);
updateDoc.accessIdIV = accessEncToken.iv;
updateDoc.accessIdTag = accessEncToken.tag;
updateDoc.accessIdCiphertext = accessEncToken.ciphertext;
}
if (awsAssumeIamRoleArn) {
const awsAssumeIamRoleArnEnc = encryptSymmetric128BitHexKeyUTF8(awsAssumeIamRoleArn, botKey);
updateDoc.awsAssumeIamRoleArnCipherText = awsAssumeIamRoleArnEnc.ciphertext;
updateDoc.awsAssumeIamRoleArnIV = awsAssumeIamRoleArnEnc.iv;
updateDoc.awsAssumeIamRoleArnTag = awsAssumeIamRoleArnEnc.tag;
}
}
}
return integrationAuthDAL.updateById(integrationAuthId, updateDoc);
};
// helper function // helper function
const getIntegrationAccessToken = async ( const getIntegrationAccessToken = async (
integrationAuth: TIntegrationAuths, integrationAuth: TIntegrationAuths,
@@ -1615,6 +1759,7 @@ export const integrationAuthServiceFactory = ({
getIntegrationAuth, getIntegrationAuth,
oauthExchange, oauthExchange,
saveIntegrationToken, saveIntegrationToken,
updateIntegrationAuth,
deleteIntegrationAuthById, deleteIntegrationAuthById,
deleteIntegrationAuths, deleteIntegrationAuths,
getIntegrationAuthTeams, getIntegrationAuthTeams,
@@ -22,6 +22,11 @@ export type TSaveIntegrationAccessTokenDTO = {
awsAssumeIamRoleArn?: string; awsAssumeIamRoleArn?: string;
} & TProjectPermission; } & TProjectPermission;
export type TUpdateIntegrationAuthDTO = Omit<TSaveIntegrationAccessTokenDTO, "projectId" | "integration"> & {
integrationAuthId: string;
integration?: string;
};
export type TDeleteIntegrationAuthsDTO = TProjectPermission & { export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
integration: string; integration: string;
projectId: string; projectId: string;
@@ -151,7 +151,9 @@ export const integrationServiceFactory = ({
isActive, isActive,
environment, environment,
secretPath, secretPath,
metadata region,
metadata,
path
}: TUpdateIntegrationDTO) => { }: TUpdateIntegrationDTO) => {
const integration = await integrationDAL.findById(id); const integration = await integrationDAL.findById(id);
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` }); if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
@@ -192,7 +194,9 @@ export const integrationServiceFactory = ({
appId, appId,
targetEnvironment, targetEnvironment,
owner, owner,
region,
secretPath, secretPath,
path,
metadata: { metadata: {
...(integration.metadata as object), ...(integration.metadata as object),
...metadata ...metadata
@@ -49,6 +49,8 @@ export type TUpdateIntegrationDTO = {
appId?: string; appId?: string;
isActive?: boolean; isActive?: boolean;
secretPath?: string; secretPath?: string;
region?: string;
path?: string;
targetEnvironment?: string; targetEnvironment?: string;
owner?: string; owner?: string;
environment?: string; environment?: string;