diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index d04ead63f..857fadda5 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -2,7 +2,7 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { standardRequest } from "../../config/request"; 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 { IntegrationService } from "../../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) => { // TODO: refactor // TODO: check if access token is valid for each integration - let integrationAuth; const { body: { workspaceId, integration, url, accessId, namespace, accessToken, refreshToken } } = 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"); - integrationAuth = await IntegrationAuth.findOneAndUpdate( - { - workspace: new Types.ObjectId(workspaceId), - integration - }, - { - workspace: new Types.ObjectId(workspaceId), - integration, - url, - namespace, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - ...(integration === INTEGRATION_GCP_SECRET_MANAGER - ? { - metadata: { - authMethod: "serviceAccount" - } + let integrationAuth = await new IntegrationAuth({ + workspace: new Types.ObjectId(workspaceId), + integration, + url, + namespace, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + ...(integration === INTEGRATION_GCP_SECRET_MANAGER + ? { + metadata: { + authMethod: "serviceAccount" } - : {}) - }, - { - new: true, - upsert: true - } - ); + } + : {}) + }).save(); // encrypt and save integration access details if (refreshToken) { @@ -188,12 +177,12 @@ export const saveIntegrationToken = async (req: Request, res: Response) => { // encrypt and save integration access details if (accessId || accessToken) { - integrationAuth = await IntegrationService.setIntegrationAuthAccess({ + integrationAuth = (await IntegrationService.setIntegrationAuthAccess({ integrationAuthId: integrationAuth._id.toString(), accessId, accessToken, accessExpiresAt: undefined - }); + })) as IIntegrationAuth; } 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] * @param req * @param res * @returns */ -export const deleteIntegrationAuth = async (req: Request, res: Response) => { +export const deleteIntegrationAuthById = async (req: Request, res: Response) => { const { params: { integrationAuthId } } = await validateRequest(reqValidator.DeleteIntegrationAuthV1, req); diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index d426be0a1..837936af7 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -251,6 +251,21 @@ export const deleteIntegration = async (req: Request, res: Response) => { }); 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( req.authData, diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index 68d0433b3..9e4236dbc 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -156,12 +156,20 @@ router.get( integrationAuthController.getIntegrationAuthTeamCityBuildConfigs ); +router.delete( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.deleteIntegrationAuths +); + router.delete( "/:integrationAuthId", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - integrationAuthController.deleteIntegrationAuth + integrationAuthController.deleteIntegrationAuthById ); export default router; diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index 7488109d0..928e7f233 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -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({ params: z.object({ integrationAuthId: z.string().trim() diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index f5065919f..e70ffdccb 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -1,6 +1,7 @@ export { useAuthorizeIntegration, useDeleteIntegrationAuth, + useDeleteIntegrationAuths, useGetIntegrationAuthApps, useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthById, diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 96c23e4cc..21257f5ac 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -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(); return useMutation<{}, {}, { id: string; workspaceId: string }>({ diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 0ccc0a3ce..70bee92c3 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -96,6 +96,7 @@ export const useDeleteIntegration = () => { mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`), onSuccess: (_, { workspaceId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId)); + queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId)); } }); }; \ No newline at end of file diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 86f5206c7..bc16d5508 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -126,6 +126,7 @@ const fetchWorkspaceAuthorization = async (workspaceId: string) => { const { data } = await apiRequest.get<{ authorizations: IntegrationAuth[] }>( `/api/v1/workspace/${workspaceId}/authorizations` ); + return data.authorizations; }; diff --git a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx index fda0f6a1d..f2c5c3ea2 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useRouter } from "next/router"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Button, Modal, ModalContent } from "@app/components/v2"; @@ -9,7 +8,7 @@ import { withProjectPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; import { useDeleteIntegration, - useDeleteIntegrationAuth, + useDeleteIntegrationAuths, useGetCloudIntegrations, useGetUserWsKey, useGetWorkspaceAuthorizations, @@ -24,8 +23,7 @@ import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSe import { IntegrationsSection } from "./components/IntegrationsSection"; import { generateBotKey, - redirectForProviderAuth, - redirectToIntegrationAppConfigScreen + redirectForProviderAuth } from "./IntegrationPage.utils"; type Props = { @@ -36,7 +34,6 @@ export const IntegrationsPage = withProjectPermission( ({ frameworkIntegrations }: Props) => { const { t } = useTranslation(); const { createNotification } = useNotificationContext(); - const router = useRouter(); const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?._id || ""; @@ -65,6 +62,7 @@ export const IntegrationsPage = withProjectPermission( return groupBy; }, []) ); + // mutation const { data: integrations, @@ -78,11 +76,11 @@ export const IntegrationsPage = withProjectPermission( const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } = useUpdateBotActiveStatus(); const { mutateAsync: deleteIntegration } = useDeleteIntegration(); - const { - mutateAsync: deleteIntegrationAuth, + const { + mutateAsync: deleteIntegrationAuths, isSuccess: isDeleteIntegrationAuthSuccess, - reset: resetDeleteIntegrationAuth - } = useDeleteIntegrationAuth(); + reset: resetDeleteIntegrationAuths + } = useDeleteIntegrationAuths(); const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length; const isIntegrationsEmpty = !integrations?.length; @@ -103,7 +101,7 @@ export const IntegrationsPage = withProjectPermission( botId: bot._id, workspaceId }); - resetDeleteIntegrationAuth(); + resetDeleteIntegrationAuths(); } }, [ isIntegrationFetching, @@ -116,7 +114,7 @@ export const IntegrationsPage = withProjectPermission( const handleProviderIntegration = async (provider: string) => { const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug); if (!selectedCloudIntegration) return; - + try { if (bot && !bot.isActive) { const botKey = generateBotKey(bot.publicKey, latestWsKey!); @@ -127,14 +125,8 @@ export const IntegrationsPage = withProjectPermission( botId: bot._id }); } - const integrationAuthForProvider = integrationAuths?.[provider]; - if (!integrationAuthForProvider) { - redirectForProviderAuth(selectedCloudIntegration); - return; - } - const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id); - router.push(url); + redirectForProviderAuth(selectedCloudIntegration); } catch (error) { console.error(error); } @@ -176,9 +168,10 @@ export const IntegrationsPage = withProjectPermission( const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => { const integrationAuthForProvider = integrationAuths?.[provider]; if (!integrationAuthForProvider) return; + try { - await deleteIntegrationAuth({ - id: integrationAuthForProvider._id, + await deleteIntegrationAuths({ + integration: provider, workspaceId }); if (cb) cb();