From 63a3ce2dbabd3065dc8103f6bc5dab84727b7a63 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 28 Apr 2025 04:58:25 +0400 Subject: [PATCH] feat(workflow-integrations): ms-teams audit logs and pagination support --- .../ee/services/audit-log/audit-log-types.ts | 79 ++++++++++++++- .../routes/v1/microsoft-teams-router.ts | 95 ++++++++++++++++++- .../microsoft-teams/microsoft-teams-fns.ts | 82 +++++++++++----- .../microsoft-teams-service.ts | 11 ++- .../microsoft-teams-integration.mdx | 16 ++-- .../src/hooks/api/auditLogs/constants.tsx | 14 ++- frontend/src/hooks/api/auditLogs/enums.tsx | 10 +- 7 files changed, 265 insertions(+), 42 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 7808f38b9..34761b6f4 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -320,7 +320,15 @@ export enum EventType { DELETE_SECRET_ROTATION = "delete-secret-rotation", SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets", - PROJECT_ACCESS_REQUEST = "project-access-request" + PROJECT_ACCESS_REQUEST = "project-access-request", + + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list" } export const filterableSecretEvents: EventType[] = [ @@ -2517,6 +2525,66 @@ interface RotateSecretRotationEvent { }; } +interface MicrosoftTeamsWorkflowIntegrationCreateEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE; + metadata: { + tenantId: string; + slug: string; + description?: string; + }; +} + +interface MicrosoftTeamsWorkflowIntegrationDeleteEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE; + metadata: { + tenantId: string; + id: string; + slug: string; + }; +} + +interface MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS; + metadata: { + tenantId: string; + slug: string; + }; +} + +interface MicrosoftTeamsWorkflowIntegrationGetTeamsEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS; + metadata: { + tenantId: string; + slug: string; + id: string; + }; +} + +interface MicrosoftTeamsWorkflowIntegrationGetEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET; + metadata: { + tenantId: string; + slug: string; + id: string; + }; +} + +interface MicrosoftTeamsWorkflowIntegrationListEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST; + metadata: Record; +} + +interface MicrosoftTeamsWorkflowIntegrationUpdateEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE; + metadata: { + tenantId: string; + slug: string; + id: string; + newSlug?: string; + newDescription?: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -2746,4 +2814,11 @@ export type Event = | CreateSecretRotationEvent | UpdateSecretRotationEvent | DeleteSecretRotationEvent - | RotateSecretRotationEvent; + | RotateSecretRotationEvent + | MicrosoftTeamsWorkflowIntegrationCreateEvent + | MicrosoftTeamsWorkflowIntegrationDeleteEvent + | MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent + | MicrosoftTeamsWorkflowIntegrationGetTeamsEvent + | MicrosoftTeamsWorkflowIntegrationGetEvent + | MicrosoftTeamsWorkflowIntegrationListEvent + | MicrosoftTeamsWorkflowIntegrationUpdateEvent; diff --git a/backend/src/server/routes/v1/microsoft-teams-router.ts b/backend/src/server/routes/v1/microsoft-teams-router.ts index caed33ba8..ee8d1760e 100644 --- a/backend/src/server/routes/v1/microsoft-teams-router.ts +++ b/backend/src/server/routes/v1/microsoft-teams-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { MicrosoftTeamsIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -76,6 +77,19 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) = actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE, + metadata: { + tenantId: req.body.tenantId, + slug: req.body.slug, + description: req.body.description + } + } + }); } }); @@ -104,6 +118,15 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) = actorOrgId: req.permission.orgId }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST, + metadata: {} + } + }); + return microsoftTeamsIntegrations; } }); @@ -121,13 +144,25 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) = }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - await server.services.microsoftTeams.checkInstallationStatus({ + const microsoftTeamsIntegration = await server.services.microsoftTeams.checkInstallationStatus({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, workflowIntegrationId: req.params.id }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS, + metadata: { + tenantId: microsoftTeamsIntegration.tenantId, + slug: microsoftTeamsIntegration.slug + } + } + }); } }); @@ -160,6 +195,19 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) = id: req.params.id }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE, + metadata: { + tenantId: deletedMicrosoftTeamsIntegration.tenantId, + slug: deletedMicrosoftTeamsIntegration.slug, + id: deletedMicrosoftTeamsIntegration.id + } + } + }); + return deletedMicrosoftTeamsIntegration; } }); @@ -193,6 +241,19 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) = id: req.params.id }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET, + metadata: { + slug: microsoftTeamsIntegration.slug, + id: microsoftTeamsIntegration.id, + tenantId: microsoftTeamsIntegration.tenantId + } + } + }); + return microsoftTeamsIntegration; } }); @@ -231,6 +292,21 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) = ...req.body }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE, + metadata: { + slug: microsoftTeamsIntegration.slug, + id: microsoftTeamsIntegration.id, + tenantId: microsoftTeamsIntegration.tenantId, + newSlug: req.body.slug, + newDescription: req.body.description + } + } + }); + return microsoftTeamsIntegration; } }); @@ -262,7 +338,7 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) = }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const teams = await server.services.microsoftTeams.getTeams({ + const microsoftTeamsIntegration = await server.services.microsoftTeams.getTeams({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -270,7 +346,20 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) = workflowIntegrationId: req.params.workflowIntegrationId }); - return teams; + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS, + metadata: { + tenantId: microsoftTeamsIntegration.tenantId, + slug: microsoftTeamsIntegration.slug, + id: microsoftTeamsIntegration.id + } + } + }); + + return microsoftTeamsIntegration.teams; } }); diff --git a/backend/src/services/microsoft-teams/microsoft-teams-fns.ts b/backend/src/services/microsoft-teams/microsoft-teams-fns.ts index 3a3b5ff33..fbbc4dc7a 100644 --- a/backend/src/services/microsoft-teams/microsoft-teams-fns.ts +++ b/backend/src/services/microsoft-teams/microsoft-teams-fns.ts @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this */ import axios from "axios"; import { TeamsActivityHandler, TurnContext } from "botbuilder"; +import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { z } from "zod"; @@ -45,11 +46,10 @@ export const verifyTenantFromCode = async ( }); const accessToken = response.data.access_token; - const tokenParts = accessToken.split("."); - const tokenPayload = JSON.parse(Buffer.from(tokenParts[1], "base64").toString()) as { tid: string }; + const decodedToken = jwt.decode(accessToken) as { tid: string }; // the 'tid' claim in the token contains the tenant ID - const tenantIdFromToken = tokenPayload.tid; + const tenantIdFromToken = decodedToken.tid; if (tenantIdFromToken !== tenantId) { throw new BadRequestError({ @@ -562,24 +562,34 @@ export class TeamsBot extends TeamsActivityHandler { } } - // todo: filter out teams that the bot is not a member of - async getTeamsAndChannels(accessToken: string, tenantId: string, internalAppId: string) { + async getTeamsAndChannels(accessToken: string, internalAppId: string) { try { - const teamsResponse = await axios - .get<{ value: { displayName: string; id: string }[] }>(`https://graph.microsoft.com/v1.0/teams`, { - headers: { - Authorization: `Bearer ${accessToken}` - } - }) - .catch((error) => { + let teamsNextLink: string = "https://graph.microsoft.com/v1.0/teams"; + + let allTeams: { displayName: string; id: string }[] = []; + while (teamsNextLink?.length) { + try { + // eslint-disable-next-line no-await-in-loop + const response = await axios.get<{ + value: { displayName: string; id: string }[]; + "@odata.nextLink"?: string; + }>(teamsNextLink, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + allTeams = allTeams.concat(response.data.value); + teamsNextLink = response.data["@odata.nextLink"] || ""; + } catch (error) { logger.error(error, "Microsoft Teams Workflow Integration: Failed to fetch teams"); throw error; - }); + } + } - const teams = teamsResponse.data.value; const result = []; - for await (const team of teams) { + for await (const team of allTeams) { try { // Get installed apps for this team const installedAppsResponse = await axios.get<{ value: { teamsAppDefinition: { teamsAppId: string } }[] }>( @@ -600,21 +610,41 @@ export class TeamsBot extends TeamsActivityHandler { continue; // skip this team if we can't determine if the bot is installed } - const channelsResponse = await axios - .get<{ value: { displayName: string; id: string }[] }>( - `https://graph.microsoft.com/v1.0/teams/${team.id}/channels`, - { + let allChannels: { displayName: string; id: string }[] = []; + + let channelNextLink: string = `https://graph.microsoft.com/v1.0/teams/${team.id}/channels`; + + while (channelNextLink?.length) { + // eslint-disable-next-line no-await-in-loop + const resp = await axios + .get<{ + value: { displayName: string; id: string }[]; + "@odata.nextLink"?: string; + }>(channelNextLink, { headers: { Authorization: `Bearer ${accessToken}` } - } - ) - .catch((error) => { - logger.error(error, "Microsoft Teams Workflow Integration: Failed to fetch channels"); - throw error; - }); + }) + .catch((error) => { + if (axios.isAxiosError(error)) { + logger.error( + error.response?.data, + "getTeamsAndChannels: Axios error, Microsoft Teams Workflow Integration: Failed to fetch channels" + ); + } else { + logger.error( + error, + "getTeamsAndChannels: Microsoft Teams Workflow Integration: Failed to fetch channels" + ); + } + throw error; + }); - const channels = channelsResponse.data.value.map((channel) => ({ + allChannels = allChannels.concat(resp.data.value); + channelNextLink = resp.data["@odata.nextLink"] || ""; + } + + const channels = allChannels.map((channel) => ({ channelName: channel.displayName, channelId: channel.id })); diff --git a/backend/src/services/microsoft-teams/microsoft-teams-service.ts b/backend/src/services/microsoft-teams/microsoft-teams-service.ts index 39ccdd7cc..6c56fe991 100644 --- a/backend/src/services/microsoft-teams/microsoft-teams-service.ts +++ b/backend/src/services/microsoft-teams/microsoft-teams-service.ts @@ -206,6 +206,8 @@ export const microsoftTeamsServiceFactory = ({ status: WorkflowIntegrationStatus.INSTALLED }); } + + return microsoftTeamsIntegration; }; const completeMicrosoftTeamsIntegration = async ({ @@ -556,7 +558,7 @@ export const microsoftTeamsServiceFactory = ({ botAppId: decryptedAppId.toString(), botAppPassword: decryptedAppPassword.toString(), botId: decryptedBotId.toString(), - orgId: actorOrgId, + orgId: microsoftTeamsIntegration.orgId, kmsService, microsoftTeamsIntegrationDAL, microsoftTeamsIntegrationId: microsoftTeamsIntegration.id @@ -568,9 +570,12 @@ export const microsoftTeamsServiceFactory = ({ }); } - const teams = await teamsBot.getTeamsAndChannels(accessToken, microsoftTeamsIntegration.tenantId, internalId); + const teams = await teamsBot.getTeamsAndChannels(accessToken, internalId); - return teams; + return { + ...microsoftTeamsIntegration, + teams + }; }; const handleMessageEndpoint = async (req: FastifyRequest, res: FastifyReply) => { diff --git a/docs/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx b/docs/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx index 7d7080dbe..e0d5c3fbc 100644 --- a/docs/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx +++ b/docs/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx @@ -103,11 +103,15 @@ This guide will provide step by step instructions on how to configure Microsoft Next we need to get the application client ID and create a new client secret. **Save these values for later, as they're required to configure the Microsoft Teams integration in Infisical.** - Copy the application client ID from the overview page. - ![copy-client-id](/images/platform/workflow-integrations/microsoft-teams-integration/azure-copy-client-id.png) + Copy the application client ID from the overview page. + ![copy-client-id](/images/platform/workflow-integrations/microsoft-teams-integration/azure-copy-client-id.png) - Create a new client secret, and save the value. Keep the expiry time in mind when you're creating the secret. - ![create-client-secret](/images/platform/workflow-integrations/microsoft-teams-integration/azure-create-client-secret.png) + Create a new client secret, and save the value. Keep the expiry time in mind when you're creating the secret. + ![create-client-secret](/images/platform/workflow-integrations/microsoft-teams-integration/azure-create-client-secret.png) + + + Remember to rotate your client secret before it expires. Consider setting up a reminder or automated process to replace the secret and update your Infisical configuration before expiration. + @@ -135,7 +139,7 @@ This guide will provide step by step instructions on how to configure Microsoft "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.11/MicrosoftTeams.schema.json", "version": "3.0.0", "manifestVersion": "1.11", - "id": "f86bbe9c-b853-4f96-8512-7ceba7d2a620", // Keep your existing ID here, this field cannot be changed. + "id": "", "name": { "short": "Infisical", "full": "Infisical Bot" @@ -157,7 +161,7 @@ This guide will provide step by step instructions on how to configure Microsoft "accentColor": "#FFFFFF", "bots": [ { - "botId": "01bad7fb-227b-4ac3-b5f3-41e8cdaf65d7", // Replace the botId with the Client ID of the App Registration from the previous step. + "botId": "