Merge pull request #1284 from Infisical/multi-integration-auth

Enable new integration auth credential for each new integration
This commit is contained in:
Maidul Islam
2024-01-07 14:49:04 -05:00
committed by GitHub
9 changed files with 132 additions and 51 deletions

View File

@@ -2,7 +2,7 @@ import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { standardRequest } from "../../config/request"; import { standardRequest } from "../../config/request";
import { getApps, getTeams, revokeAccess } from "../../integrations"; import { getApps, getTeams, revokeAccess } from "../../integrations";
import { Bot, IntegrationAuth, Workspace } from "../../models"; import { Bot, IIntegrationAuth, Integration, IntegrationAuth, Workspace } from "../../models";
import { EventType } from "../../ee/models"; import { EventType } from "../../ee/models";
import { IntegrationService } from "../../services"; import { IntegrationService } from "../../services";
import { EEAuditLogService } from "../../ee/services"; import { EEAuditLogService } from "../../ee/services";
@@ -130,7 +130,6 @@ export const oAuthExchange = async (req: Request, res: Response) => {
export const saveIntegrationToken = async (req: Request, res: Response) => { export const saveIntegrationToken = async (req: Request, res: Response) => {
// TODO: refactor // TODO: refactor
// TODO: check if access token is valid for each integration // TODO: check if access token is valid for each integration
let integrationAuth;
const { const {
body: { workspaceId, integration, url, accessId, namespace, accessToken, refreshToken } body: { workspaceId, integration, url, accessId, namespace, accessToken, refreshToken }
} = await validateRequest(reqValidator.SaveIntegrationAccessTokenV1, req); } = await validateRequest(reqValidator.SaveIntegrationAccessTokenV1, req);
@@ -152,31 +151,21 @@ export const saveIntegrationToken = async (req: Request, res: Response) => {
if (!bot) throw new Error("Bot must be enabled to save integration access token"); if (!bot) throw new Error("Bot must be enabled to save integration access token");
integrationAuth = await IntegrationAuth.findOneAndUpdate( let integrationAuth = await new IntegrationAuth({
{ workspace: new Types.ObjectId(workspaceId),
workspace: new Types.ObjectId(workspaceId), integration,
integration url,
}, namespace,
{ algorithm: ALGORITHM_AES_256_GCM,
workspace: new Types.ObjectId(workspaceId), keyEncoding: ENCODING_SCHEME_UTF8,
integration, ...(integration === INTEGRATION_GCP_SECRET_MANAGER
url, ? {
namespace, metadata: {
algorithm: ALGORITHM_AES_256_GCM, authMethod: "serviceAccount"
keyEncoding: ENCODING_SCHEME_UTF8,
...(integration === INTEGRATION_GCP_SECRET_MANAGER
? {
metadata: {
authMethod: "serviceAccount"
}
} }
: {}) }
}, : {})
{ }).save();
new: true,
upsert: true
}
);
// encrypt and save integration access details // encrypt and save integration access details
if (refreshToken) { if (refreshToken) {
@@ -188,12 +177,12 @@ export const saveIntegrationToken = async (req: Request, res: Response) => {
// encrypt and save integration access details // encrypt and save integration access details
if (accessId || accessToken) { if (accessId || accessToken) {
integrationAuth = await IntegrationService.setIntegrationAuthAccess({ integrationAuth = (await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
accessId, accessId,
accessToken, accessToken,
accessExpiresAt: undefined accessExpiresAt: undefined
}); })) as IIntegrationAuth;
} }
if (!integrationAuth) throw new Error("Failed to save integration access token"); if (!integrationAuth) throw new Error("Failed to save integration access token");
@@ -1208,13 +1197,64 @@ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res:
}); });
}; };
/**
* Delete all integration authorizations and integrations for workspace with id [workspaceId]
* with integration name [integration]
* @param req
* @param res
* @returns
*/
export const deleteIntegrationAuths = async (req: Request, res: Response) => {
const {
query: { integration, workspaceId }
} = await validateRequest(reqValidator.DeleteIntegrationAuthsV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Integrations
);
const integrationAuths = await IntegrationAuth.deleteMany({
integration,
workspace: new Types.ObjectId(workspaceId)
});
const integrations = await Integration.deleteMany({
integration,
workspace: new Types.ObjectId(workspaceId)
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UNAUTHORIZE_INTEGRATION,
metadata: {
integration
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
integrationAuths,
integrations
});
}
/** /**
* Delete integration authorization with id [integrationAuthId] * Delete integration authorization with id [integrationAuthId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteIntegrationAuth = async (req: Request, res: Response) => { export const deleteIntegrationAuthById = async (req: Request, res: Response) => {
const { const {
params: { integrationAuthId } params: { integrationAuthId }
} = await validateRequest(reqValidator.DeleteIntegrationAuthV1, req); } = await validateRequest(reqValidator.DeleteIntegrationAuthV1, req);

View File

@@ -251,6 +251,21 @@ export const deleteIntegration = async (req: Request, res: Response) => {
}); });
if (!deletedIntegration) throw new Error("Failed to find integration"); if (!deletedIntegration) throw new Error("Failed to find integration");
const numOtherIntegrationsUsingSameAuth = await Integration.countDocuments({
integrationAuth: deletedIntegration.integrationAuth,
_id: {
$nin: [deletedIntegration._id]
}
});
if (numOtherIntegrationsUsingSameAuth === 0) {
// no other integrations are using the same integration auth
// -> delete integration auth associated with the integration being deleted
await IntegrationAuth.deleteOne({
_id: deletedIntegration.integrationAuth
});
}
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,

View File

@@ -156,12 +156,20 @@ router.get(
integrationAuthController.getIntegrationAuthTeamCityBuildConfigs integrationAuthController.getIntegrationAuthTeamCityBuildConfigs
); );
router.delete(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.deleteIntegrationAuths
);
router.delete( router.delete(
"/:integrationAuthId", "/:integrationAuthId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
}), }),
integrationAuthController.deleteIntegrationAuth integrationAuthController.deleteIntegrationAuthById
); );
export default router; export default router;

View File

@@ -192,6 +192,13 @@ export const GetIntegrationAuthNorthflankSecretGroupsV1 = z.object({
}) })
}); });
export const DeleteIntegrationAuthsV1 = z.object({
query: z.object({
integration: z.string().trim(),
workspaceId: z.string().trim()
})
});
export const DeleteIntegrationAuthV1 = z.object({ export const DeleteIntegrationAuthV1 = z.object({
params: z.object({ params: z.object({
integrationAuthId: z.string().trim() integrationAuthId: z.string().trim()

View File

@@ -1,6 +1,7 @@
export { export {
useAuthorizeIntegration, useAuthorizeIntegration,
useDeleteIntegrationAuth, useDeleteIntegrationAuth,
useDeleteIntegrationAuths,
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById, useGetIntegrationAuthById,

View File

@@ -699,7 +699,22 @@ export const useSaveIntegrationAccessToken = () => {
}); });
}; };
export const useDeleteIntegrationAuth = () => { export const useDeleteIntegrationAuths = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, { integration: string; workspaceId: string }>({
mutationFn: ({ integration, workspaceId }) => apiRequest.delete(`/api/v1/integration-auth?${new URLSearchParams({
integration,
workspaceId
})}`),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
}
});
};
export const useDeleteIntegrationAuth = () => { // not used
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, { id: string; workspaceId: string }>({ return useMutation<{}, {}, { id: string; workspaceId: string }>({

View File

@@ -96,6 +96,7 @@ export const useDeleteIntegration = () => {
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`), mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`),
onSuccess: (_, { workspaceId }) => { onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
} }
}); });
}; };

View File

@@ -126,6 +126,7 @@ const fetchWorkspaceAuthorization = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ authorizations: IntegrationAuth[] }>( const { data } = await apiRequest.get<{ authorizations: IntegrationAuth[] }>(
`/api/v1/workspace/${workspaceId}/authorizations` `/api/v1/workspace/${workspaceId}/authorizations`
); );
return data.authorizations; return data.authorizations;
}; };

View File

@@ -1,6 +1,5 @@
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, Modal, ModalContent } from "@app/components/v2"; import { Button, Modal, ModalContent } from "@app/components/v2";
@@ -9,7 +8,7 @@ import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import {
useDeleteIntegration, useDeleteIntegration,
useDeleteIntegrationAuth, useDeleteIntegrationAuths,
useGetCloudIntegrations, useGetCloudIntegrations,
useGetUserWsKey, useGetUserWsKey,
useGetWorkspaceAuthorizations, useGetWorkspaceAuthorizations,
@@ -24,8 +23,7 @@ import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSe
import { IntegrationsSection } from "./components/IntegrationsSection"; import { IntegrationsSection } from "./components/IntegrationsSection";
import { import {
generateBotKey, generateBotKey,
redirectForProviderAuth, redirectForProviderAuth
redirectToIntegrationAppConfigScreen
} from "./IntegrationPage.utils"; } from "./IntegrationPage.utils";
type Props = { type Props = {
@@ -36,7 +34,6 @@ export const IntegrationsPage = withProjectPermission(
({ frameworkIntegrations }: Props) => { ({ frameworkIntegrations }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const router = useRouter();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || ""; const workspaceId = currentWorkspace?._id || "";
@@ -65,6 +62,7 @@ export const IntegrationsPage = withProjectPermission(
return groupBy; return groupBy;
}, []) }, [])
); );
// mutation // mutation
const { const {
data: integrations, data: integrations,
@@ -78,11 +76,11 @@ export const IntegrationsPage = withProjectPermission(
const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } = const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
useUpdateBotActiveStatus(); useUpdateBotActiveStatus();
const { mutateAsync: deleteIntegration } = useDeleteIntegration(); const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const { const {
mutateAsync: deleteIntegrationAuth, mutateAsync: deleteIntegrationAuths,
isSuccess: isDeleteIntegrationAuthSuccess, isSuccess: isDeleteIntegrationAuthSuccess,
reset: resetDeleteIntegrationAuth reset: resetDeleteIntegrationAuths
} = useDeleteIntegrationAuth(); } = useDeleteIntegrationAuths();
const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length; const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length;
const isIntegrationsEmpty = !integrations?.length; const isIntegrationsEmpty = !integrations?.length;
@@ -103,7 +101,7 @@ export const IntegrationsPage = withProjectPermission(
botId: bot._id, botId: bot._id,
workspaceId workspaceId
}); });
resetDeleteIntegrationAuth(); resetDeleteIntegrationAuths();
} }
}, [ }, [
isIntegrationFetching, isIntegrationFetching,
@@ -116,7 +114,7 @@ export const IntegrationsPage = withProjectPermission(
const handleProviderIntegration = async (provider: string) => { const handleProviderIntegration = async (provider: string) => {
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug); const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
if (!selectedCloudIntegration) return; if (!selectedCloudIntegration) return;
try { try {
if (bot && !bot.isActive) { if (bot && !bot.isActive) {
const botKey = generateBotKey(bot.publicKey, latestWsKey!); const botKey = generateBotKey(bot.publicKey, latestWsKey!);
@@ -127,14 +125,8 @@ export const IntegrationsPage = withProjectPermission(
botId: bot._id botId: bot._id
}); });
} }
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) {
redirectForProviderAuth(selectedCloudIntegration);
return;
}
const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id); redirectForProviderAuth(selectedCloudIntegration);
router.push(url);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -176,9 +168,10 @@ export const IntegrationsPage = withProjectPermission(
const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => { const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
const integrationAuthForProvider = integrationAuths?.[provider]; const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) return; if (!integrationAuthForProvider) return;
try { try {
await deleteIntegrationAuth({ await deleteIntegrationAuths({
id: integrationAuthForProvider._id, integration: provider,
workspaceId workspaceId
}); });
if (cb) cb(); if (cb) cb();