mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 06:35:21 +00:00
feat(native-integrations): microsoft teams
This commit is contained in:
Vendored
+2
@@ -98,6 +98,7 @@ import { TUserServiceFactory } from "@app/services/user/user-service";
|
||||
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
||||
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
|
||||
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
@@ -241,6 +242,7 @@ declare module "fastify" {
|
||||
kmipOperation: TKmipOperationServiceFactory;
|
||||
gateway: TGatewayServiceFactory;
|
||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
Vendored
+20
@@ -423,6 +423,16 @@ import {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TMicrosoftTeamsIntegrations,
|
||||
TMicrosoftTeamsIntegrationsInsert,
|
||||
TMicrosoftTeamsIntegrationsUpdate
|
||||
} from "@app/db/schemas/microsoft-teams-integrations";
|
||||
import {
|
||||
TProjectMicrosoftTeamsConfigs,
|
||||
TProjectMicrosoftTeamsConfigsInsert,
|
||||
TProjectMicrosoftTeamsConfigsUpdate
|
||||
} from "@app/db/schemas/project-microsoft-teams-configs";
|
||||
|
||||
declare module "knex" {
|
||||
namespace Knex {
|
||||
@@ -994,5 +1004,15 @@ declare module "knex/types/tables" {
|
||||
TSecretRotationV2SecretMappingsInsert,
|
||||
TSecretRotationV2SecretMappingsUpdate
|
||||
>;
|
||||
[TableName.MicrosoftTeamsIntegrations]: KnexOriginal.CompositeTableType<
|
||||
TMicrosoftTeamsIntegrations,
|
||||
TMicrosoftTeamsIntegrationsInsert,
|
||||
TMicrosoftTeamsIntegrationsUpdate
|
||||
>;
|
||||
[TableName.ProjectMicrosoftTeamsConfigs]: KnexOriginal.CompositeTableType<
|
||||
TProjectMicrosoftTeamsConfigs,
|
||||
TProjectMicrosoftTeamsConfigsInsert,
|
||||
TProjectMicrosoftTeamsConfigsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
const encryptedMicrosoftTeamsAppIdColumn = "encryptedMicrosoftTeamsAppId";
|
||||
const encryptedMicrosoftTeamsClientSecretColumn = "encryptedMicrosoftTeamsClientSecret";
|
||||
const encryptedMicrosoftTeamsBotIdColumn = "encryptedMicrosoftTeamsBotId";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const superAdminHasEncryptedMicrosoftTeamsClientIdColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
encryptedMicrosoftTeamsAppIdColumn
|
||||
);
|
||||
const superAdminHasEncryptedMicrosoftTeamsClientSecretColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
encryptedMicrosoftTeamsClientSecretColumn
|
||||
);
|
||||
const superAdminHasEncryptedMicrosoftTeamsBotIdColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
encryptedMicrosoftTeamsBotIdColumn
|
||||
);
|
||||
|
||||
if (!superAdminHasEncryptedMicrosoftTeamsClientIdColumn || !superAdminHasEncryptedMicrosoftTeamsClientSecretColumn) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (table) => {
|
||||
if (!superAdminHasEncryptedMicrosoftTeamsClientIdColumn) {
|
||||
table.binary(encryptedMicrosoftTeamsAppIdColumn).nullable();
|
||||
}
|
||||
if (!superAdminHasEncryptedMicrosoftTeamsClientSecretColumn) {
|
||||
table.binary(encryptedMicrosoftTeamsClientSecretColumn).nullable();
|
||||
}
|
||||
if (!superAdminHasEncryptedMicrosoftTeamsBotIdColumn) {
|
||||
table.binary(encryptedMicrosoftTeamsBotIdColumn).nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.WorkflowIntegrations, "status"))) {
|
||||
await knex.schema.alterTable(TableName.WorkflowIntegrations, (table) => {
|
||||
table.enu("status", ["pending", "installed", "failed"]).notNullable().defaultTo("installed"); // defaults to installed so we can have backwards compatibility with existing workflow integrations
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.MicrosoftTeamsIntegrations))) {
|
||||
await knex.schema.createTable(TableName.MicrosoftTeamsIntegrations, (table) => {
|
||||
table.uuid("id", { primaryKey: true }).notNullable();
|
||||
table.foreign("id").references("id").inTable(TableName.WorkflowIntegrations).onDelete("CASCADE"); // the ID itself is the workflow integration ID
|
||||
|
||||
table.string("internalTeamsAppId").nullable();
|
||||
table.string("tenantId").notNullable();
|
||||
table.binary("encryptedAccessToken").nullable();
|
||||
table.binary("encryptedBotAccessToken").nullable();
|
||||
|
||||
table.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.MicrosoftTeamsIntegrations);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ProjectMicrosoftTeamsConfigs))) {
|
||||
await knex.schema.createTable(TableName.ProjectMicrosoftTeamsConfigs, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.string("projectId").notNullable().unique();
|
||||
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
tb.uuid("microsoftTeamsIntegrationId").notNullable();
|
||||
tb.foreign("microsoftTeamsIntegrationId")
|
||||
.references("id")
|
||||
.inTable(TableName.MicrosoftTeamsIntegrations)
|
||||
.onDelete("CASCADE");
|
||||
tb.boolean("isAccessRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||
tb.boolean("isSecretRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||
|
||||
tb.jsonb("accessRequestChannels").notNullable(); // {teamId: string, channelIds: string[]}
|
||||
tb.jsonb("secretRequestChannels").notNullable(); // {teamId: string, channelIds: string[]}
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.ProjectMicrosoftTeamsConfigs);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasEncryptedMicrosoftTeamsClientIdColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
encryptedMicrosoftTeamsAppIdColumn
|
||||
);
|
||||
const hasEncryptedMicrosoftTeamsClientSecretColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
encryptedMicrosoftTeamsClientSecretColumn
|
||||
);
|
||||
const hasEncryptedMicrosoftTeamsBotIdColumn = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
encryptedMicrosoftTeamsBotIdColumn
|
||||
);
|
||||
|
||||
if (hasEncryptedMicrosoftTeamsClientIdColumn || hasEncryptedMicrosoftTeamsClientSecretColumn) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (table) => {
|
||||
if (hasEncryptedMicrosoftTeamsClientIdColumn) {
|
||||
table.dropColumn(encryptedMicrosoftTeamsAppIdColumn);
|
||||
}
|
||||
if (hasEncryptedMicrosoftTeamsClientSecretColumn) {
|
||||
table.dropColumn(encryptedMicrosoftTeamsClientSecretColumn);
|
||||
}
|
||||
if (hasEncryptedMicrosoftTeamsBotIdColumn) {
|
||||
table.dropColumn(encryptedMicrosoftTeamsBotIdColumn);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (await knex.schema.hasColumn(TableName.WorkflowIntegrations, "status")) {
|
||||
await knex.schema.alterTable(TableName.WorkflowIntegrations, (table) => {
|
||||
table.dropColumn("status");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.ProjectMicrosoftTeamsConfigs)) {
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectMicrosoftTeamsConfigs);
|
||||
await dropOnUpdateTrigger(knex, TableName.ProjectMicrosoftTeamsConfigs);
|
||||
}
|
||||
if (await knex.schema.hasTable(TableName.MicrosoftTeamsIntegrations)) {
|
||||
await knex.schema.dropTableIfExists(TableName.MicrosoftTeamsIntegrations);
|
||||
await dropOnUpdateTrigger(knex, TableName.MicrosoftTeamsIntegrations);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export const CertificatesSchema = z.object({
|
||||
notAfter: z.date(),
|
||||
revokedAt: z.date().nullable().optional(),
|
||||
revocationReason: z.number().nullable().optional(),
|
||||
altNames: z.string().default("").nullable().optional(),
|
||||
altNames: z.string().nullable().optional(),
|
||||
caCertId: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||
keyUsages: z.string().array().nullable().optional(),
|
||||
|
||||
@@ -57,6 +57,7 @@ export * from "./kms-keys";
|
||||
export * from "./kms-root-config";
|
||||
export * from "./ldap-configs";
|
||||
export * from "./ldap-group-maps";
|
||||
export * from "./microsoft-teams-integrations";
|
||||
export * from "./models";
|
||||
export * from "./oidc-configs";
|
||||
export * from "./org-bots";
|
||||
|
||||
@@ -13,7 +13,7 @@ export const KmipOrgServerCertificatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
commonName: z.string(),
|
||||
altNames: z.string(),
|
||||
altNames: z.string().nullable().optional(),
|
||||
serialNumber: z.string(),
|
||||
keyAlgorithm: z.string(),
|
||||
issuedAt: z.date(),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const MicrosoftTeamsIntegrationsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
internalTeamsAppId: z.string().nullable().optional(),
|
||||
tenantId: z.string(),
|
||||
encryptedAccessToken: zodBuffer.nullable().optional(),
|
||||
encryptedBotAccessToken: zodBuffer.nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TMicrosoftTeamsIntegrations = z.infer<typeof MicrosoftTeamsIntegrationsSchema>;
|
||||
export type TMicrosoftTeamsIntegrationsInsert = Omit<
|
||||
z.input<typeof MicrosoftTeamsIntegrationsSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TMicrosoftTeamsIntegrationsUpdate = Partial<
|
||||
Omit<z.input<typeof MicrosoftTeamsIntegrationsSchema>, TImmutableDBKeys>
|
||||
>;
|
||||
@@ -146,7 +146,9 @@ export enum TableName {
|
||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||
KmipClientCertificates = "kmip_client_certificates",
|
||||
SecretRotationV2 = "secret_rotations_v2",
|
||||
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings"
|
||||
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
|
||||
MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
|
||||
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
||||
@@ -30,9 +30,9 @@ export const OidcConfigsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid(),
|
||||
lastUsed: z.date().nullable().optional(),
|
||||
manageGroupMemberships: z.boolean().default(false),
|
||||
encryptedOidcClientId: zodBuffer,
|
||||
encryptedOidcClientSecret: zodBuffer,
|
||||
manageGroupMemberships: z.boolean().default(false),
|
||||
jwtSignatureAlgorithm: z.string().default("RS256")
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export const OrganizationsSchema = z.object({
|
||||
defaultMembershipRole: z.string().default("member"),
|
||||
enforceMfa: z.boolean().default(false),
|
||||
selectedMfaMethod: z.string().nullable().optional(),
|
||||
secretShareSendToAnyone: z.boolean().default(true).nullable().optional(),
|
||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ProjectMicrosoftTeamsConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
microsoftTeamsIntegrationId: z.string().uuid(),
|
||||
isAccessRequestNotificationEnabled: z.boolean().default(false),
|
||||
isSecretRequestNotificationEnabled: z.boolean().default(false),
|
||||
accessRequestChannels: z.unknown(),
|
||||
secretRequestChannels: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TProjectMicrosoftTeamsConfigs = z.infer<typeof ProjectMicrosoftTeamsConfigsSchema>;
|
||||
export type TProjectMicrosoftTeamsConfigsInsert = Omit<
|
||||
z.input<typeof ProjectMicrosoftTeamsConfigsSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TProjectMicrosoftTeamsConfigsUpdate = Partial<
|
||||
Omit<z.input<typeof ProjectMicrosoftTeamsConfigsSchema>, TImmutableDBKeys>
|
||||
>;
|
||||
@@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretReminderRecipientsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
secretId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
projectId: z.string()
|
||||
});
|
||||
|
||||
export type TSecretReminderRecipients = z.infer<typeof SecretReminderRecipientsSchema>;
|
||||
export type TSecretReminderRecipientsInsert = Omit<z.input<typeof SecretReminderRecipientsSchema>, TImmutableDBKeys>;
|
||||
export type TSecretReminderRecipientsUpdate = Partial<
|
||||
Omit<z.input<typeof SecretReminderRecipientsSchema>, TImmutableDBKeys>
|
||||
>;
|
||||
@@ -26,7 +26,10 @@ export const SuperAdminSchema = z.object({
|
||||
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
||||
authConsentContent: z.string().nullable().optional(),
|
||||
pageFrameContent: z.string().nullable().optional(),
|
||||
adminIdentityIds: z.string().array().nullable().optional()
|
||||
adminIdentityIds: z.string().array().nullable().optional(),
|
||||
encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(),
|
||||
encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(),
|
||||
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||
|
||||
@@ -14,7 +14,8 @@ export const WorkflowIntegrationsSchema = z.object({
|
||||
orgId: z.string().uuid(),
|
||||
description: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
status: z.string().default("installed")
|
||||
});
|
||||
|
||||
export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>;
|
||||
|
||||
+33
-20
@@ -6,13 +6,15 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
||||
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
@@ -67,6 +69,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||
@@ -84,6 +88,8 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
smtpService,
|
||||
userDAL,
|
||||
kmsService,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
projectSlackConfigDAL
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const createAccessApprovalRequest = async ({
|
||||
@@ -219,24 +225,31 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
|
||||
|
||||
await triggerSlackNotification({
|
||||
projectId: project.id,
|
||||
projectSlackConfigDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
notification: {
|
||||
type: SlackTriggerFeature.ACCESS_REQUEST,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
isTemporary,
|
||||
requesterEmail: requestedByUser.email as string,
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
note
|
||||
}
|
||||
console.log("triggering workflow integration notification");
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
notification: {
|
||||
type: TriggerFeature.ACCESS_REQUEST,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
isTemporary,
|
||||
requesterEmail: requestedByUser.email as string,
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
note
|
||||
}
|
||||
},
|
||||
projectId: project.id
|
||||
},
|
||||
dependencies: {
|
||||
projectDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
TSecretSyncRaw,
|
||||
TUpdateSecretSyncDTO
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||
|
||||
import { KmipPermission } from "../kmip/kmip-enum";
|
||||
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||
@@ -246,8 +247,9 @@ export enum EventType {
|
||||
GET_SLACK_INTEGRATION = "get-slack-integration",
|
||||
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
|
||||
|
||||
UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
|
||||
INTEGRATION_SYNCED = "integration-synced",
|
||||
CREATE_CMEK = "create-cmek",
|
||||
UPDATE_CMEK = "update-cmek",
|
||||
@@ -1968,22 +1970,24 @@ interface GetSlackIntegration {
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateProjectSlackConfig {
|
||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
|
||||
interface UpdateProjectWorkflowIntegrationConfig {
|
||||
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
slackIntegrationId: string;
|
||||
integrationId: string;
|
||||
integration: WorkflowIntegration;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
accessRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
secretRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectSlackConfig {
|
||||
type: EventType.GET_PROJECT_SLACK_CONFIG;
|
||||
interface GetProjectWorkflowIntegrationConfig {
|
||||
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
integration: WorkflowIntegration;
|
||||
};
|
||||
}
|
||||
interface IntegrationSyncedEvent {
|
||||
@@ -2668,8 +2672,8 @@ export type Event =
|
||||
| UpdateSlackIntegration
|
||||
| DeleteSlackIntegration
|
||||
| GetSlackIntegration
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig
|
||||
| UpdateProjectWorkflowIntegrationConfig
|
||||
| GetProjectWorkflowIntegrationConfig
|
||||
| IntegrationSyncedEvent
|
||||
| CreateCmekEvent
|
||||
| UpdateCmekEvent
|
||||
|
||||
@@ -29,7 +29,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
auditLogsRetentionDays: 0,
|
||||
auditLogStreams: false,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
samlSSO: true,
|
||||
hsm: false,
|
||||
oidcSSO: false,
|
||||
scim: false,
|
||||
@@ -38,7 +38,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
secretApproval: false,
|
||||
secretApproval: true,
|
||||
secretRotation: false,
|
||||
caCrl: false,
|
||||
instanceUserManagement: false,
|
||||
|
||||
+51
-31
@@ -17,9 +17,13 @@ import { groupBy, pick, unique } from "@app/lib/fn";
|
||||
import { setKnexStringValue } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
@@ -52,8 +56,6 @@ import {
|
||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
||||
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
@@ -126,6 +128,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||
@@ -155,7 +159,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretVersionTagV2BridgeDAL,
|
||||
licenseService,
|
||||
projectSlackConfigDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
@@ -1171,21 +1177,28 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||
await triggerSlackNotification({
|
||||
projectId,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectSlackConfigDAL,
|
||||
notification: {
|
||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
||||
payload: {
|
||||
userEmail: user.email as string,
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
||||
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
projectId,
|
||||
notification: {
|
||||
type: TriggerFeature.SECRET_APPROVAL,
|
||||
payload: {
|
||||
userEmail: user.email as string,
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {
|
||||
projectDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1503,21 +1516,28 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||
await triggerSlackNotification({
|
||||
projectId,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectSlackConfigDAL,
|
||||
notification: {
|
||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
||||
payload: {
|
||||
userEmail: user.email as string,
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
||||
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
projectId,
|
||||
notification: {
|
||||
type: TriggerFeature.SECRET_APPROVAL,
|
||||
payload: {
|
||||
userEmail: user.email as string,
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectSlackConfigDAL,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { MicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
|
||||
import { sendSlackNotification } from "@app/services/slack/slack-fns";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { TriggerFeature, TTriggerWorkflowNotificationDTO } from "./types";
|
||||
|
||||
export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkflowNotificationDTO) => {
|
||||
try {
|
||||
const { projectId, notification } = dto.input;
|
||||
const { projectDAL, projectSlackConfigDAL, kmsService, projectMicrosoftTeamsConfigDAL, microsoftTeamsService } =
|
||||
dto.dependencies;
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||
const slackConfig = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||
|
||||
if (slackConfig) {
|
||||
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||
const targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || [];
|
||||
if (targetChannelIds.length && slackConfig.isAccessRequestNotificationEnabled) {
|
||||
await sendSlackNotification({
|
||||
orgId: project.orgId,
|
||||
notification,
|
||||
kmsService,
|
||||
targetChannelIds,
|
||||
slackIntegration: slackConfig
|
||||
}).catch((error) => {
|
||||
logger.error(error, "Error sending Slack notification");
|
||||
});
|
||||
}
|
||||
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
|
||||
const targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || [];
|
||||
if (targetChannelIds.length && slackConfig.isSecretRequestNotificationEnabled) {
|
||||
await sendSlackNotification({
|
||||
orgId: project.orgId,
|
||||
notification,
|
||||
kmsService,
|
||||
targetChannelIds,
|
||||
slackIntegration: slackConfig
|
||||
}).catch((error) => {
|
||||
logger.error(error, "Error sending Slack notification");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("microsoftTeamsConfig", microsoftTeamsConfig);
|
||||
if (microsoftTeamsConfig) {
|
||||
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||
if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) {
|
||||
const { success, data } = MicrosoftTeamsChannelsSchema.safeParse(microsoftTeamsConfig.accessRequestChannels);
|
||||
|
||||
console.log("success", success);
|
||||
console.log("data", data);
|
||||
|
||||
if (success && data) {
|
||||
await microsoftTeamsService
|
||||
.sendNotification({
|
||||
notification,
|
||||
target: data,
|
||||
tenantId: microsoftTeamsConfig.tenantId
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error, "Error sending Microsoft Teams notification");
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
|
||||
if (microsoftTeamsConfig.isSecretRequestNotificationEnabled && microsoftTeamsConfig.secretRequestChannels) {
|
||||
const { success, data } = MicrosoftTeamsChannelsSchema.safeParse(microsoftTeamsConfig.secretRequestChannels);
|
||||
|
||||
if (success && data) {
|
||||
await microsoftTeamsService
|
||||
.sendNotification({
|
||||
notification,
|
||||
target: data,
|
||||
tenantId: microsoftTeamsConfig.tenantId
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error, "Error sending Microsoft Teams notification");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, "Error triggering workflow integration notification");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||
|
||||
export enum TriggerFeature {
|
||||
SECRET_APPROVAL = "secret-approval",
|
||||
ACCESS_REQUEST = "access-request"
|
||||
}
|
||||
|
||||
export type TNotification =
|
||||
| {
|
||||
type: TriggerFeature.SECRET_APPROVAL;
|
||||
payload: {
|
||||
userEmail: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
secretKeys: string[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: TriggerFeature.ACCESS_REQUEST;
|
||||
payload: {
|
||||
requesterFullName: string;
|
||||
requesterEmail: string;
|
||||
isTemporary: boolean;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectName: string;
|
||||
permissions: string[];
|
||||
approvalUrl: string;
|
||||
note?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TTriggerWorkflowNotificationDTO = {
|
||||
input: {
|
||||
projectId: string;
|
||||
notification: TNotification;
|
||||
};
|
||||
dependencies: {
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
};
|
||||
};
|
||||
@@ -111,6 +111,10 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||
|
||||
if (!authMode) return;
|
||||
|
||||
@@ -171,6 +171,9 @@ import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
||||
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal";
|
||||
import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||
@@ -419,6 +422,8 @@ export const registerRoutes = async (
|
||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||
|
||||
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
||||
const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db);
|
||||
const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
@@ -659,6 +664,15 @@ export const registerRoutes = async (
|
||||
orgDAL,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
});
|
||||
|
||||
const microsoftTeamsService = microsoftTeamsServiceFactory({
|
||||
microsoftTeamsIntegrationDAL,
|
||||
permissionService,
|
||||
workflowIntegrationDAL,
|
||||
kmsService,
|
||||
serverCfgDAL: superAdminDAL
|
||||
});
|
||||
|
||||
const superAdminService = superAdminServiceFactory({
|
||||
userDAL,
|
||||
identityDAL,
|
||||
@@ -672,7 +686,8 @@ export const registerRoutes = async (
|
||||
orgService,
|
||||
keyStore,
|
||||
licenseService,
|
||||
kmsService
|
||||
kmsService,
|
||||
microsoftTeamsService
|
||||
});
|
||||
|
||||
const orgAdminService = orgAdminServiceFactory({
|
||||
@@ -996,6 +1011,8 @@ export const registerRoutes = async (
|
||||
certificateTemplateDAL,
|
||||
projectSlackConfigDAL,
|
||||
slackIntegrationDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
projectTemplateService,
|
||||
groupProjectDAL,
|
||||
smtpService
|
||||
@@ -1179,7 +1196,9 @@ export const registerRoutes = async (
|
||||
accessApprovalPolicyApproverDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService,
|
||||
groupDAL
|
||||
groupDAL,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL
|
||||
});
|
||||
|
||||
const secretReplicationService = secretReplicationServiceFactory({
|
||||
@@ -1576,6 +1595,7 @@ export const registerRoutes = async (
|
||||
await dailyResourceCleanUp.startCleanUp();
|
||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||
await kmsService.startService();
|
||||
await microsoftTeamsService.start();
|
||||
|
||||
// inject all services
|
||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||
@@ -1667,7 +1687,8 @@ export const registerRoutes = async (
|
||||
kmip: kmipService,
|
||||
kmipOperation: kmipOperationService,
|
||||
gateway: gatewayService,
|
||||
secretRotationV2: secretRotationV2Service
|
||||
secretRotationV2: secretRotationV2Service,
|
||||
microsoftTeams: microsoftTeamsService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
@@ -27,7 +27,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
encryptedSlackClientId: true,
|
||||
encryptedSlackClientSecret: true
|
||||
encryptedSlackClientSecret: true,
|
||||
encryptedMicrosoftTeamsAppId: true,
|
||||
encryptedMicrosoftTeamsClientSecret: true,
|
||||
encryptedMicrosoftTeamsBotId: true
|
||||
}).extend({
|
||||
isMigrationModeOn: z.boolean(),
|
||||
defaultAuthOrgSlug: z.string().nullable(),
|
||||
@@ -74,6 +77,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
slackClientId: z.string().optional(),
|
||||
slackClientSecret: z.string().optional(),
|
||||
microsoftTeamsAppId: z.string().optional(),
|
||||
microsoftTeamsClientSecret: z.string().optional(),
|
||||
microsoftTeamsBotId: z.string().optional(),
|
||||
authConsentContent: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -197,15 +203,22 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/integrations/slack/config",
|
||||
url: "/integrations",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
slack: z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
}),
|
||||
microsoftTeams: z.object({
|
||||
appId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
botId: z.string()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -215,9 +228,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
},
|
||||
handler: async () => {
|
||||
const adminSlackConfig = await server.services.superAdmin.getAdminSlackConfig();
|
||||
const adminIntegrationsConfig = await server.services.superAdmin.getAdminIntegrationsConfig();
|
||||
|
||||
return adminSlackConfig;
|
||||
return adminIntegrationsConfig;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import { registerUserEngagementRouter } from "./user-engagement-router";
|
||||
import { registerUserRouter } from "./user-router";
|
||||
import { registerWebhookRouter } from "./webhook-router";
|
||||
import { registerWorkflowIntegrationRouter } from "./workflow-integration-router";
|
||||
import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router";
|
||||
|
||||
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerSsoRouter, { prefix: "/sso" });
|
||||
@@ -79,6 +80,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
async (workflowIntegrationRouter) => {
|
||||
await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter);
|
||||
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
|
||||
await workflowIntegrationRouter.register(registerMicrosoftTeamsRouter, { prefix: "/microsoft-teams" });
|
||||
},
|
||||
{ prefix: "/workflow-integrations" }
|
||||
);
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { MicrosoftTeamsIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { WorkflowIntegrationStatus } from "@app/services/workflow-integration/workflow-integration-types";
|
||||
|
||||
const sanitizedMicrosoftTeamsIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
||||
id: true,
|
||||
description: true,
|
||||
slug: true,
|
||||
integration: true
|
||||
}).merge(
|
||||
MicrosoftTeamsIntegrationsSchema.pick({
|
||||
tenantId: true
|
||||
}).extend({
|
||||
status: z.nativeEnum(WorkflowIntegrationStatus)
|
||||
})
|
||||
);
|
||||
|
||||
export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
tenantId: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const microsoftTeamsIntegration = await server.services.microsoftTeams.createMicrosoftTeamsIntegration({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
return microsoftTeamsIntegration;
|
||||
}
|
||||
});
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
response: {
|
||||
200: sanitizedMicrosoftTeamsIntegrationSchema.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const microsoftTeamsIntegrations = await server.services.microsoftTeams.getMicrosoftTeamsIntegrationsByOrg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return microsoftTeamsIntegrations;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:id/installation-status",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
})
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const deletedMicrosoftTeamsIntegration = await server.services.microsoftTeams.deleteMicrosoftTeamsIntegration({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return deletedMicrosoftTeamsIntegration;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const microsoftTeamsIntegration = await server.services.microsoftTeams.getMicrosoftTeamsIntegrationById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return microsoftTeamsIntegration;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
slug: slugSchema({ max: 64 }).optional(),
|
||||
description: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const microsoftTeamsIntegration = await server.services.microsoftTeams.updateMicrosoftTeamsIntegration({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id,
|
||||
...req.body
|
||||
});
|
||||
|
||||
return microsoftTeamsIntegration;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workflowIntegrationId/teams",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workflowIntegrationId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
teamId: z.string(),
|
||||
teamName: z.string(),
|
||||
channels: z
|
||||
.object({
|
||||
channelName: z.string(),
|
||||
channelId: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const teams = await server.services.microsoftTeams.getTeams({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
workflowIntegrationId: req.params.workflowIntegrationId
|
||||
});
|
||||
|
||||
return teams;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/message-endpoint",
|
||||
schema: {
|
||||
body: z.any(),
|
||||
response: {
|
||||
200: z.any()
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
await server.services.microsoftTeams.handleMessageEndpoint(req, res);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
UserEncryptionKeysSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { ProjectMicrosoftTeamsConfigsSchema } from "@app/db/schemas/project-microsoft-teams-configs";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
@@ -19,8 +20,9 @@ import { re2Validator } from "@app/lib/zod";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
import { MicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
|
||||
import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
|
||||
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
|
||||
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||
|
||||
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
|
||||
@@ -615,88 +617,172 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/slack-config",
|
||||
url: "/:workspaceId/workflow-integration-config/:integration",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
workspaceId: z.string().trim(),
|
||||
integration: z.nativeEnum(WorkflowIntegration)
|
||||
}),
|
||||
response: {
|
||||
200: ProjectSlackConfigsSchema.pick({
|
||||
id: true,
|
||||
slackIntegrationId: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
})
|
||||
200: z.discriminatedUnion("integration", [
|
||||
ProjectSlackConfigsSchema.pick({
|
||||
id: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
}).merge(
|
||||
z.object({
|
||||
integration: z.literal(WorkflowIntegration.SLACK),
|
||||
integrationId: z.string()
|
||||
})
|
||||
),
|
||||
ProjectMicrosoftTeamsConfigsSchema.pick({
|
||||
id: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
}).merge(
|
||||
z.object({
|
||||
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||
integrationId: z.string()
|
||||
})
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackConfig = await server.services.project.getProjectSlackConfig({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
if (slackConfig) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_SLACK_CONFIG,
|
||||
metadata: {
|
||||
id: slackConfig.id
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return slackConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PUT",
|
||||
url: "/:workspaceId/slack-config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
slackIntegrationId: z.string(),
|
||||
isAccessRequestNotificationEnabled: z.boolean(),
|
||||
accessRequestChannels: validateSlackChannelsField,
|
||||
isSecretRequestNotificationEnabled: z.boolean(),
|
||||
secretRequestChannels: validateSlackChannelsField
|
||||
}),
|
||||
response: {
|
||||
200: ProjectSlackConfigsSchema.pick({
|
||||
id: true,
|
||||
slackIntegrationId: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackConfig = await server.services.project.updateProjectSlackConfig({
|
||||
const config = await server.services.project.getProjectWorkflowIntegrationConfig({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
integration: req.params.integration
|
||||
});
|
||||
|
||||
if (config) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
|
||||
metadata: {
|
||||
id: config.id,
|
||||
integration: config.integration
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:projectId/workflow-integrations/:integration/:integrationId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim(),
|
||||
integration: z.nativeEnum(WorkflowIntegration),
|
||||
integrationId: z.string()
|
||||
})
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const deletedIntegration = await server.services.project.deleteProjectWorkflowIntegration({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.projectId,
|
||||
integration: req.params.integration,
|
||||
integrationId: req.params.integrationId
|
||||
});
|
||||
|
||||
return deletedIntegration;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PUT",
|
||||
url: "/:workspaceId/workflow-integration/:integration",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
integration: z.nativeEnum(WorkflowIntegration)
|
||||
}),
|
||||
body: z.object({
|
||||
integrationId: z.string(),
|
||||
isAccessRequestNotificationEnabled: z.boolean(),
|
||||
accessRequestChannels: z.string().or(
|
||||
z
|
||||
.object({
|
||||
teamId: z.string(),
|
||||
channelIds: z.string().array()
|
||||
})
|
||||
.optional()
|
||||
),
|
||||
isSecretRequestNotificationEnabled: z.boolean(),
|
||||
secretRequestChannels: z.string().or(
|
||||
z
|
||||
.object({
|
||||
teamId: z.string(),
|
||||
channelIds: z.string().array()
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
}),
|
||||
response: {
|
||||
200: z.discriminatedUnion("integration", [
|
||||
ProjectSlackConfigsSchema.pick({
|
||||
id: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
}).merge(
|
||||
z.object({
|
||||
integration: z.literal(WorkflowIntegration.SLACK),
|
||||
integrationId: z.string()
|
||||
})
|
||||
),
|
||||
ProjectMicrosoftTeamsConfigsSchema.pick({
|
||||
id: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
isSecretRequestNotificationEnabled: true
|
||||
}).merge(
|
||||
z.object({
|
||||
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||
integrationId: z.string(),
|
||||
accessRequestChannels: MicrosoftTeamsChannelsSchema,
|
||||
secretRequestChannels: MicrosoftTeamsChannelsSchema
|
||||
})
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const workflowIntegrationConfig = await server.services.project.updateProjectWorkflowIntegration({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
integration: req.params.integration,
|
||||
...req.body
|
||||
});
|
||||
|
||||
@@ -704,19 +790,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG,
|
||||
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
|
||||
metadata: {
|
||||
id: slackConfig.id,
|
||||
slackIntegrationId: slackConfig.slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled: slackConfig.isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: slackConfig.accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled: slackConfig.isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: slackConfig.secretRequestChannels
|
||||
id: workflowIntegrationConfig.id,
|
||||
integrationId: workflowIntegrationConfig.integrationId,
|
||||
integration: workflowIntegrationConfig.integration,
|
||||
isAccessRequestNotificationEnabled: workflowIntegrationConfig.isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: workflowIntegrationConfig.accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled: workflowIntegrationConfig.isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: workflowIntegrationConfig.secretRequestChannels
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return slackConfig;
|
||||
return workflowIntegrationConfig;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ const sanitizedWorkflowIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
||||
id: true,
|
||||
description: true,
|
||||
slug: true,
|
||||
integration: true
|
||||
integration: true,
|
||||
status: true
|
||||
});
|
||||
|
||||
export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import axios from "axios";
|
||||
import { TeamsActivityHandler, TurnContext } from "botbuilder";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
|
||||
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||
|
||||
export const getMicrosoftTeamsAccessToken = async ({
|
||||
tenantId,
|
||||
clientId,
|
||||
clientSecret,
|
||||
getBotFrameworkToken = false
|
||||
}: {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
getBotFrameworkToken?: boolean;
|
||||
}) => {
|
||||
const details = getBotFrameworkToken
|
||||
? {
|
||||
uri: "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token",
|
||||
scope: "https://api.botframework.com/.default"
|
||||
}
|
||||
: {
|
||||
uri: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
||||
scope: "https://graph.microsoft.com/.default"
|
||||
};
|
||||
|
||||
const tokenResponse = await axios.post<{ access_token: string }>(
|
||||
details.uri,
|
||||
new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: details.scope,
|
||||
grant_type: "client_credentials"
|
||||
})
|
||||
);
|
||||
|
||||
return tokenResponse.data.access_token;
|
||||
};
|
||||
|
||||
export const isBotInstalledInTenant = async ({
|
||||
tenantId,
|
||||
botAppId,
|
||||
botAppPassword,
|
||||
botId
|
||||
}: {
|
||||
tenantId: string;
|
||||
botAppId: string;
|
||||
botAppPassword: string;
|
||||
botId: string;
|
||||
}) => {
|
||||
try {
|
||||
const botAccessToken = await getMicrosoftTeamsAccessToken({
|
||||
tenantId,
|
||||
clientId: botAppId.toString(),
|
||||
clientSecret: botAppPassword.toString(),
|
||||
getBotFrameworkToken: true
|
||||
}).catch(() => null);
|
||||
|
||||
const accessToken = await getMicrosoftTeamsAccessToken({
|
||||
tenantId,
|
||||
clientId: botAppId.toString(),
|
||||
clientSecret: botAppPassword.toString()
|
||||
}).catch(() => null);
|
||||
|
||||
if (!botAccessToken || !accessToken) {
|
||||
return {
|
||||
accessToken: null,
|
||||
botAccessToken: null,
|
||||
installed: false,
|
||||
internalId: null
|
||||
} as const;
|
||||
}
|
||||
|
||||
const appsResponse = await axios
|
||||
.get<{ value: { id: string; displayName: string; distributionMethod: string; externalId: string }[] }>(
|
||||
"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error(error, "Error fetching installed apps");
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!appsResponse) {
|
||||
return {
|
||||
installed: false,
|
||||
internalId: null,
|
||||
accessToken,
|
||||
botAccessToken
|
||||
} as const;
|
||||
}
|
||||
|
||||
const botInstalledInTenant = appsResponse.data.value.find((a) => a.externalId === botId);
|
||||
|
||||
if (!botInstalledInTenant) {
|
||||
return {
|
||||
installed: false,
|
||||
internalId: null,
|
||||
accessToken,
|
||||
botAccessToken
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
installed: true,
|
||||
internalId: botInstalledInTenant.id,
|
||||
accessToken,
|
||||
botAccessToken
|
||||
} as const;
|
||||
} catch (error) {
|
||||
logger.error(error, "Error fetching installed apps");
|
||||
return {
|
||||
installed: false,
|
||||
internalId: null,
|
||||
accessToken: null,
|
||||
botAccessToken: null
|
||||
} as const;
|
||||
}
|
||||
};
|
||||
|
||||
export const buildTeamsPayload = (notification: TNotification) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
switch (notification.type) {
|
||||
case TriggerFeature.SECRET_APPROVAL: {
|
||||
const { payload } = notification;
|
||||
|
||||
const adaptiveCard = {
|
||||
type: "AdaptiveCard",
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: "1.5",
|
||||
body: [
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "Secret approval request",
|
||||
weight: "Bolder",
|
||||
size: "Large"
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: `A secret approval request has been opened by ${payload.userEmail}.`,
|
||||
wrap: true
|
||||
},
|
||||
{
|
||||
type: "FactSet",
|
||||
facts: [
|
||||
{
|
||||
title: "Environment",
|
||||
value: payload.environment
|
||||
},
|
||||
{
|
||||
title: "Secret path",
|
||||
value: payload.secretPath || "/"
|
||||
},
|
||||
{
|
||||
title: `Secret Key${payload.secretKeys.length > 1 ? "s" : ""}`,
|
||||
value: payload.secretKeys.join(", ")
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "Action.OpenUrl",
|
||||
title: "View request in Infisical",
|
||||
url: `${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${payload.requestId}`
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
adaptiveCard
|
||||
};
|
||||
}
|
||||
|
||||
case TriggerFeature.ACCESS_REQUEST: {
|
||||
const { payload } = notification;
|
||||
|
||||
const adaptiveCard = {
|
||||
type: "AdaptiveCard",
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: "1.5",
|
||||
body: [
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "New access approval request pending for review",
|
||||
weight: "Bolder",
|
||||
size: "Large"
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
||||
payload.isTemporary ? "temporary" : "permanent"
|
||||
} access to path '${payload.secretPath}' in the ${payload.environment} environment of ${
|
||||
payload.projectName
|
||||
} project.`,
|
||||
wrap: true
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: `The following permissions are requested: ${payload.permissions.join(", ")}`,
|
||||
wrap: true
|
||||
},
|
||||
payload.note
|
||||
? {
|
||||
type: "TextBlock",
|
||||
text: `**User Note**: ${payload.note}`,
|
||||
wrap: true
|
||||
}
|
||||
: null
|
||||
].filter(Boolean),
|
||||
actions: [
|
||||
{
|
||||
type: "Action.OpenUrl",
|
||||
title: "View request in Infisical",
|
||||
url: payload.approvalUrl
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
adaptiveCard
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new BadRequestError({
|
||||
message: "Teams notification type not supported."
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class TeamsBot extends TeamsActivityHandler {
|
||||
private botAppId: string;
|
||||
|
||||
private botAppPassword: string;
|
||||
|
||||
private workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "update">;
|
||||
|
||||
private microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne">;
|
||||
|
||||
constructor({
|
||||
botAppId,
|
||||
botAppPassword,
|
||||
workflowIntegrationDAL,
|
||||
microsoftTeamsIntegrationDAL
|
||||
}: {
|
||||
botAppId: string;
|
||||
botAppPassword: string;
|
||||
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "update">;
|
||||
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne">;
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.botAppId = botAppId;
|
||||
this.botAppPassword = botAppPassword;
|
||||
this.workflowIntegrationDAL = workflowIntegrationDAL;
|
||||
this.microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDAL;
|
||||
|
||||
// We know when a bot is added, but we can't know when it's fully removed from the tenant.
|
||||
this.onTeamsMembersAddedEvent(async (membersAdded, _, context) => {
|
||||
const botWasAdded = membersAdded.some((member) => member.id === context.activity.recipient.id);
|
||||
|
||||
if (botWasAdded && context.activity.conversation.tenantId) {
|
||||
const microsoftTeamIntegration = await this.microsoftTeamsIntegrationDAL.findOne({
|
||||
tenantId: context.activity.conversation.tenantId
|
||||
});
|
||||
|
||||
await this.workflowIntegrationDAL
|
||||
.update(
|
||||
{
|
||||
id: microsoftTeamIntegration.id,
|
||||
status: WorkflowIntegrationStatus.PENDING
|
||||
},
|
||||
{
|
||||
status: WorkflowIntegrationStatus.INSTALLED
|
||||
}
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error(error, "Microsoft Teams Workflow Integration: Failed to update workflow integration");
|
||||
});
|
||||
|
||||
// This is required in order for the bot to send proactive messages, which is required for the bot to pass the bot release validation step.
|
||||
await context.sendActivity(
|
||||
"👋 Thanks for installing the Infisical app! You can now use the bot to send notifications to your selected teams."
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(context: TurnContext) {
|
||||
console.log("Running Microsoft Teams bot", {
|
||||
context
|
||||
});
|
||||
await super.run(context);
|
||||
}
|
||||
|
||||
async sendMessageToChannel(tenantId: string, channelId: string, teamId: string, notification: TNotification) {
|
||||
const { adaptiveCard } = buildTeamsPayload(notification);
|
||||
|
||||
const botToken = await getMicrosoftTeamsAccessToken({
|
||||
tenantId,
|
||||
clientId: this.botAppId,
|
||||
clientSecret: this.botAppPassword,
|
||||
getBotFrameworkToken: true
|
||||
});
|
||||
|
||||
const adaptiveCardActivity = {
|
||||
type: "message",
|
||||
attachments: [
|
||||
{
|
||||
contentType: "application/vnd.microsoft.card.adaptive",
|
||||
content: adaptiveCard
|
||||
}
|
||||
],
|
||||
conversation: {
|
||||
id: channelId,
|
||||
isGroup: true
|
||||
},
|
||||
channelData: {
|
||||
channel: {
|
||||
id: channelId
|
||||
},
|
||||
team: {
|
||||
id: teamId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await axios.post(
|
||||
`https://smba.trafficmanager.net/amer/v3/conversations/${channelId}/activities`,
|
||||
adaptiveCardActivity,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${botToken}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// todo: filter out teams that the bot is not a member of
|
||||
async getTeamsAndChannels(tenantId: string, internalAppId: string) {
|
||||
try {
|
||||
const token = await getMicrosoftTeamsAccessToken({
|
||||
tenantId,
|
||||
clientId: this.botAppId,
|
||||
clientSecret: this.botAppPassword
|
||||
});
|
||||
|
||||
const teamsResponse = await axios
|
||||
.get<{ value: { displayName: string; id: string }[] }>(`https://graph.microsoft.com/v1.0/teams`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.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) {
|
||||
try {
|
||||
// Get installed apps for this team
|
||||
const installedAppsResponse = await axios.get<{ value: { teamsAppDefinition: { teamsAppId: string } }[] }>(
|
||||
`https://graph.microsoft.com/v1.0/teams/${team.id}/installedApps?$expand=teamsAppDefinition`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!installedAppsResponse.data.value.some((app) => app.teamsAppDefinition.teamsAppId === internalAppId)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-continue
|
||||
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`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error(error, "Microsoft Teams Workflow Integration: Failed to fetch channels");
|
||||
throw error;
|
||||
});
|
||||
|
||||
const channels = channelsResponse.data.value.map((channel) => ({
|
||||
channelName: channel.displayName,
|
||||
channelId: channel.id
|
||||
}));
|
||||
|
||||
result.push({
|
||||
teamId: team.id,
|
||||
teamName: team.displayName,
|
||||
channels
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(error, "Microsoft Teams Workflow Integration: Error fetching teams and channels");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const MicrosoftTeamsChannelsSchema = z
|
||||
.object({
|
||||
teamId: z.string(),
|
||||
channelIds: z.array(z.string()).min(1)
|
||||
})
|
||||
.optional()
|
||||
.refine((data) => data === undefined || data?.channelIds.length <= 5, {
|
||||
message: "You can only select up to 5 microsoft teams channels"
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TMicrosoftTeamsIntegrations, TWorkflowIntegrations } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TMicrosoftTeamsIntegrationDALFactory = ReturnType<typeof microsoftTeamsIntegrationDALFactory>;
|
||||
|
||||
export const microsoftTeamsIntegrationDALFactory = (db: TDbClient) => {
|
||||
const microsoftTeamsIntegrationOrm = ormify(db, TableName.MicrosoftTeamsIntegrations);
|
||||
|
||||
const findByIdWithWorkflowIntegrationDetails = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
return await (tx || db.replicaNode())(TableName.MicrosoftTeamsIntegrations)
|
||||
.join(
|
||||
TableName.WorkflowIntegrations,
|
||||
`${TableName.MicrosoftTeamsIntegrations}.id`,
|
||||
`${TableName.WorkflowIntegrations}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.MicrosoftTeamsIntegrations))
|
||||
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("status").withSchema(TableName.WorkflowIntegrations))
|
||||
.where(`${TableName.WorkflowIntegrations}.id`, id)
|
||||
.first();
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by ID with Workflow integration details" });
|
||||
}
|
||||
};
|
||||
|
||||
const findWithWorkflowIntegrationDetails = async (
|
||||
filter: Partial<TMicrosoftTeamsIntegrations> & Partial<TWorkflowIntegrations>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
return await (tx || db.replicaNode())(TableName.MicrosoftTeamsIntegrations)
|
||||
.join(
|
||||
TableName.WorkflowIntegrations,
|
||||
`${TableName.MicrosoftTeamsIntegrations}.id`,
|
||||
`${TableName.WorkflowIntegrations}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.MicrosoftTeamsIntegrations))
|
||||
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("status").withSchema(TableName.WorkflowIntegrations))
|
||||
.where(filter);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find with Workflow integration details" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...microsoftTeamsIntegrationOrm,
|
||||
findByIdWithWorkflowIntegrationDetails,
|
||||
findWithWorkflowIntegrationDetails
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,608 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import {
|
||||
CloudAdapter,
|
||||
ConfigurationBotFrameworkAuthentication,
|
||||
ConfigurationServiceClientCredentialFactory,
|
||||
Request,
|
||||
Response
|
||||
} from "botbuilder";
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TSuperAdminDALFactory } from "../super-admin/super-admin-dal";
|
||||
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||
import { isBotInstalledInTenant, TeamsBot } from "./microsoft-teams-fns";
|
||||
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||
import {
|
||||
TCheckInstallationStatusDTO,
|
||||
TCreateMicrosoftTeamsIntegrationDTO,
|
||||
TDeleteMicrosoftTeamsIntegrationIntegrationDTO,
|
||||
TGetMicrosoftTeamsIntegrationByIdDTO,
|
||||
TGetMicrosoftTeamsIntegrationByOrgDTO,
|
||||
TGetTeamsDTO,
|
||||
TSendNotificationDTO,
|
||||
TUpdateMicrosoftTeamsIntegrationDTO
|
||||
} from "./microsoft-teams-types";
|
||||
|
||||
function requestBodyToRecord(body: unknown): Record<string, unknown> {
|
||||
// if body is null or undefined, return an empty object
|
||||
if (body === null || body === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// if body is not an object or is an array, return an empty object
|
||||
if (typeof body !== "object" || Array.isArray(body)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// at this point, we know body is an object, so safe to cast
|
||||
return body as Record<string, unknown>;
|
||||
}
|
||||
|
||||
type TMicrosoftTeamsServiceFactoryDep = {
|
||||
microsoftTeamsIntegrationDAL: Pick<
|
||||
TMicrosoftTeamsIntegrationDALFactory,
|
||||
| "deleteById"
|
||||
| "updateById"
|
||||
| "create"
|
||||
| "findOne"
|
||||
| "findById"
|
||||
| "findByIdWithWorkflowIntegrationDetails"
|
||||
| "findWithWorkflowIntegrationDetails"
|
||||
>;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
|
||||
workflowIntegrationDAL: Pick<
|
||||
TWorkflowIntegrationDALFactory,
|
||||
"transaction" | "create" | "updateById" | "deleteById" | "update" | "findOne"
|
||||
>;
|
||||
serverCfgDAL: Pick<TSuperAdminDALFactory, "findById">;
|
||||
};
|
||||
|
||||
export type TMicrosoftTeamsServiceFactory = ReturnType<typeof microsoftTeamsServiceFactory>;
|
||||
|
||||
const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
export const microsoftTeamsServiceFactory = ({
|
||||
permissionService,
|
||||
serverCfgDAL,
|
||||
kmsService,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
workflowIntegrationDAL
|
||||
}: TMicrosoftTeamsServiceFactoryDep) => {
|
||||
let teamsBot: TeamsBot | null = null;
|
||||
let adapter: CloudAdapter | null = null;
|
||||
|
||||
const initializeTeamsBot = async ({ botAppId, botAppPassword }: { botAppId: string; botAppPassword: string }) => {
|
||||
logger.info("Initializing Microsoft Teams bot");
|
||||
teamsBot = new TeamsBot({
|
||||
botAppId,
|
||||
botAppPassword,
|
||||
workflowIntegrationDAL,
|
||||
microsoftTeamsIntegrationDAL
|
||||
});
|
||||
|
||||
adapter = new CloudAdapter(
|
||||
new ConfigurationBotFrameworkAuthentication(
|
||||
{},
|
||||
new ConfigurationServiceClientCredentialFactory({
|
||||
MicrosoftAppId: botAppId,
|
||||
MicrosoftAppPassword: botAppPassword,
|
||||
MicrosoftAppType: "MultiTenant"
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (
|
||||
serverCfg?.encryptedMicrosoftTeamsAppId &&
|
||||
serverCfg?.encryptedMicrosoftTeamsClientSecret &&
|
||||
serverCfg?.encryptedMicrosoftTeamsBotId
|
||||
) {
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||
|
||||
await initializeTeamsBot({
|
||||
botAppId: decryptedAppId.toString(),
|
||||
botAppPassword: decryptedAppPassword.toString()
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err, "Error initializing Microsoft Teams bot on startup");
|
||||
}
|
||||
};
|
||||
|
||||
const checkInstallationStatus = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
workflowIntegrationId
|
||||
}: TCheckInstallationStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
const microsoftTeamsIntegration =
|
||||
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (!serverCfg) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get server configuration."
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||
});
|
||||
}
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||
const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId);
|
||||
|
||||
const teamsBotInfo = await isBotInstalledInTenant({
|
||||
tenantId: microsoftTeamsIntegration.tenantId,
|
||||
botAppId: decryptedAppId.toString(),
|
||||
botAppPassword: decryptedAppPassword.toString(),
|
||||
botId: decryptedBotId.toString()
|
||||
});
|
||||
|
||||
if (!teamsBotInfo.installed) {
|
||||
if (microsoftTeamsIntegration.status === WorkflowIntegrationStatus.INSTALLED) {
|
||||
await workflowIntegrationDAL.updateById(microsoftTeamsIntegration.id, {
|
||||
status: WorkflowIntegrationStatus.PENDING
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant"
|
||||
});
|
||||
}
|
||||
|
||||
if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) {
|
||||
await workflowIntegrationDAL.updateById(microsoftTeamsIntegration.id, {
|
||||
status: WorkflowIntegrationStatus.INSTALLED
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createMicrosoftTeamsIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
slug,
|
||||
description,
|
||||
tenantId
|
||||
}: TCreateMicrosoftTeamsIntegrationDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (!serverCfg) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get server configuration."
|
||||
});
|
||||
}
|
||||
|
||||
const { encryptedMicrosoftTeamsAppId, encryptedMicrosoftTeamsClientSecret, encryptedMicrosoftTeamsBotId } =
|
||||
serverCfg;
|
||||
|
||||
if (!encryptedMicrosoftTeamsAppId || !encryptedMicrosoftTeamsClientSecret || !encryptedMicrosoftTeamsBotId) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||
});
|
||||
}
|
||||
|
||||
const integration = await workflowIntegrationDAL.transaction(async (tx) => {
|
||||
const workflowIntegration = await workflowIntegrationDAL.create(
|
||||
{
|
||||
description,
|
||||
orgId: actorOrgId,
|
||||
slug,
|
||||
integration: WorkflowIntegration.MICROSOFT_TEAMS,
|
||||
status: WorkflowIntegrationStatus.PENDING
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.create(
|
||||
{
|
||||
// @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID
|
||||
id: workflowIntegration.id,
|
||||
tenantId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const botAppId = decryptWithRoot(encryptedMicrosoftTeamsAppId);
|
||||
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
|
||||
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
|
||||
|
||||
const teamsBotInfo = await isBotInstalledInTenant({
|
||||
tenantId: microsoftTeamsIntegration.tenantId,
|
||||
botAppId: botAppId.toString(),
|
||||
botAppPassword: botAppPassword.toString(),
|
||||
botId: botId.toString()
|
||||
});
|
||||
if (teamsBotInfo.installed) {
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId: workflowIntegration.orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
const { cipherTextBlob: encryptedAccessToken } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(teamsBotInfo.accessToken, "utf8")
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(teamsBotInfo.botAccessToken, "utf8")
|
||||
});
|
||||
await microsoftTeamsIntegrationDAL.updateById(
|
||||
microsoftTeamsIntegration.id,
|
||||
{
|
||||
internalTeamsAppId: teamsBotInfo.internalId,
|
||||
encryptedAccessToken,
|
||||
encryptedBotAccessToken
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await workflowIntegrationDAL.updateById(
|
||||
workflowIntegration.id,
|
||||
{
|
||||
status: WorkflowIntegrationStatus.INSTALLED
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...workflowIntegration,
|
||||
status: workflowIntegration.status as WorkflowIntegrationStatus,
|
||||
tenantId: microsoftTeamsIntegration.tenantId
|
||||
};
|
||||
});
|
||||
|
||||
return integration;
|
||||
};
|
||||
|
||||
const getMicrosoftTeamsIntegrationsByOrg = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TGetMicrosoftTeamsIntegrationByOrgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const microsoftTeamsIntegrations = await microsoftTeamsIntegrationDAL.findWithWorkflowIntegrationDetails({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
return microsoftTeamsIntegrations.map((integration) => ({
|
||||
...integration,
|
||||
status: integration.status as WorkflowIntegrationStatus,
|
||||
tenantId: integration.tenantId
|
||||
}));
|
||||
};
|
||||
|
||||
const getMicrosoftTeamsIntegrationById = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TGetMicrosoftTeamsIntegrationByIdDTO) => {
|
||||
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Microsoft Teams integration not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
microsoftTeamsIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
return {
|
||||
...microsoftTeamsIntegration,
|
||||
status: microsoftTeamsIntegration.status as WorkflowIntegrationStatus
|
||||
};
|
||||
};
|
||||
|
||||
const updateMicrosoftTeamsIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id,
|
||||
slug,
|
||||
description
|
||||
}: TUpdateMicrosoftTeamsIntegrationDTO) => {
|
||||
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID ${id} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
microsoftTeamsIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
const updatedIntegration = await workflowIntegrationDAL.transaction(async (tx) => {
|
||||
await workflowIntegrationDAL.updateById(
|
||||
microsoftTeamsIntegration.id,
|
||||
{
|
||||
slug,
|
||||
description
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const integration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(
|
||||
microsoftTeamsIntegration.id,
|
||||
tx
|
||||
);
|
||||
|
||||
if (!integration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID ${microsoftTeamsIntegration.id} not found`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...integration,
|
||||
status: integration.status as WorkflowIntegrationStatus
|
||||
};
|
||||
});
|
||||
|
||||
return updatedIntegration;
|
||||
};
|
||||
|
||||
const deleteMicrosoftTeamsIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TDeleteMicrosoftTeamsIntegrationIntegrationDTO) => {
|
||||
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID ${id} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
microsoftTeamsIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||
|
||||
await workflowIntegrationDAL.deleteById(id);
|
||||
|
||||
return {
|
||||
...microsoftTeamsIntegration,
|
||||
status: microsoftTeamsIntegration.status as WorkflowIntegrationStatus
|
||||
};
|
||||
};
|
||||
|
||||
const getTeams = async ({ actorId, actor, actorOrgId, actorAuthMethod, workflowIntegrationId }: TGetTeamsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
const microsoftTeamsIntegration =
|
||||
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (!teamsBot || !adapter) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to get teams and channels because the Microsoft Teams bot is uninitialized"
|
||||
});
|
||||
}
|
||||
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (!serverCfg) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get server configuration."
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||
});
|
||||
}
|
||||
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||
const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId);
|
||||
|
||||
const { installed, internalId } = await isBotInstalledInTenant({
|
||||
tenantId: microsoftTeamsIntegration.tenantId,
|
||||
botAppId: decryptedAppId.toString(),
|
||||
botAppPassword: decryptedAppPassword.toString(),
|
||||
botId: decryptedBotId.toString()
|
||||
});
|
||||
|
||||
if (!installed) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant"
|
||||
});
|
||||
}
|
||||
|
||||
const teams = await teamsBot.getTeamsAndChannels(microsoftTeamsIntegration.tenantId, internalId);
|
||||
|
||||
return teams;
|
||||
};
|
||||
|
||||
const handleMessageEndpoint = async (req: FastifyRequest, res: FastifyReply) => {
|
||||
if (!teamsBot || !adapter) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to handle message endpoint because the Microsoft Teams bot is uninitialized"
|
||||
});
|
||||
}
|
||||
|
||||
// We need to manually build a Response object because the BotFrameworkAdapter expects a Response object. We are using FastifyReply as the underlying socket.
|
||||
const response: Response = {
|
||||
socket: res.raw.socket,
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
end(...args: any[]): unknown {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
res.raw.end(...args);
|
||||
return this;
|
||||
},
|
||||
|
||||
header(name: string, value: unknown): unknown {
|
||||
res.raw.setHeader(name, value as string);
|
||||
return this;
|
||||
},
|
||||
|
||||
send(...args: unknown[]): unknown {
|
||||
// For the first argument, which is typically the body
|
||||
if (args.length > 0) {
|
||||
const body = args[0];
|
||||
|
||||
if (typeof body === "string" || Buffer.isBuffer(body)) {
|
||||
res.raw.write(body);
|
||||
} else if (body !== null && body !== undefined) {
|
||||
const json = JSON.stringify(body);
|
||||
if (!res.raw.headersSent && !res.raw.getHeader("content-type")) {
|
||||
res.raw.setHeader("content-type", "application/json");
|
||||
}
|
||||
res.raw.write(json);
|
||||
}
|
||||
}
|
||||
|
||||
const lastArg = args[args.length - 1];
|
||||
if (typeof lastArg === "function") {
|
||||
lastArg();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
status(code: number): unknown {
|
||||
res.raw.statusCode = code;
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
const request: Request = {
|
||||
body: requestBodyToRecord(req.body),
|
||||
headers: req.headers,
|
||||
method: req.method
|
||||
};
|
||||
|
||||
await adapter.process(request, response, async (context) => {
|
||||
await teamsBot?.run(context);
|
||||
});
|
||||
};
|
||||
|
||||
const sendNotification = async ({ tenantId, target, notification }: TSendNotificationDTO) => {
|
||||
if (!teamsBot || !adapter) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to send notification because the Microsoft Teams bot is uninitialized"
|
||||
});
|
||||
}
|
||||
|
||||
for await (const channelId of target.channelIds) {
|
||||
await teamsBot.sendMessageToChannel(tenantId, channelId, target.teamId, notification);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getMicrosoftTeamsIntegrationsByOrg,
|
||||
getMicrosoftTeamsIntegrationById,
|
||||
updateMicrosoftTeamsIntegration,
|
||||
deleteMicrosoftTeamsIntegration,
|
||||
createMicrosoftTeamsIntegration,
|
||||
initializeTeamsBot,
|
||||
getTeams,
|
||||
handleMessageEndpoint,
|
||||
start,
|
||||
sendNotification,
|
||||
checkInstallationStatus
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { TNotification } from "@app/lib/workflow-integrations/types";
|
||||
|
||||
export type TGetMicrosoftTeamsIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TCreateMicrosoftTeamsIntegrationDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
slug: string;
|
||||
description?: string;
|
||||
tenantId: string;
|
||||
};
|
||||
|
||||
export type TCheckInstallationStatusDTO = { workflowIntegrationId: string } & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetMicrosoftTeamsIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TUpdateMicrosoftTeamsIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
|
||||
TOrgPermission,
|
||||
"orgId"
|
||||
>;
|
||||
|
||||
export type TGetTeamsDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
workflowIntegrationId: string;
|
||||
};
|
||||
|
||||
export type TDeleteMicrosoftTeamsIntegrationIntegrationDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TSendNotificationDTO = {
|
||||
tenantId: string;
|
||||
target: {
|
||||
teamId: string;
|
||||
channelIds: string[];
|
||||
};
|
||||
notification: TNotification;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType<typeof projectMicrosoftTeamsConfigDALFactory>;
|
||||
|
||||
export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => {
|
||||
const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs);
|
||||
|
||||
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
||||
return (tx || db.replicaNode())(TableName.ProjectMicrosoftTeamsConfigs)
|
||||
.join(
|
||||
TableName.MicrosoftTeamsIntegrations,
|
||||
`${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`,
|
||||
`${TableName.MicrosoftTeamsIntegrations}.id`
|
||||
)
|
||||
.where("projectId", "=", projectId)
|
||||
.select(
|
||||
selectAllTableCols(TableName.ProjectMicrosoftTeamsConfigs),
|
||||
selectAllTableCols(TableName.MicrosoftTeamsIntegrations)
|
||||
)
|
||||
.first();
|
||||
};
|
||||
|
||||
return { ...projectMicrosoftTeamsConfigOrm, getIntegrationDetailsByProject };
|
||||
};
|
||||
@@ -43,6 +43,9 @@ import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { MicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
|
||||
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||
@@ -60,9 +63,11 @@ import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||
import { validateSlackChannelsField } from "../slack/slack-auth-validators";
|
||||
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
|
||||
import { TProjectDALFactory } from "./project-dal";
|
||||
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
|
||||
import { TProjectQueueFactory } from "./project-queue";
|
||||
@@ -70,9 +75,10 @@ import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
|
||||
import {
|
||||
TCreateProjectDTO,
|
||||
TDeleteProjectDTO,
|
||||
TDeleteProjectWorkflowIntegration,
|
||||
TGetProjectDTO,
|
||||
TGetProjectKmsKey,
|
||||
TGetProjectSlackConfig,
|
||||
TGetProjectWorkflowIntegrationConfig,
|
||||
TListProjectAlertsDTO,
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertificateTemplatesDTO,
|
||||
@@ -91,8 +97,8 @@ import {
|
||||
TUpdateProjectDTO,
|
||||
TUpdateProjectKmsDTO,
|
||||
TUpdateProjectNameDTO,
|
||||
TUpdateProjectSlackConfig,
|
||||
TUpdateProjectVersionLimitDTO,
|
||||
TUpdateProjectWorkflowIntegration,
|
||||
TUpgradeProjectDTO
|
||||
} from "./project-types";
|
||||
|
||||
@@ -121,8 +127,19 @@ type TProjectServiceFactoryDep = {
|
||||
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
|
||||
projectSlackConfigDAL: Pick<
|
||||
TProjectSlackConfigDALFactory,
|
||||
"findOne" | "transaction" | "updateById" | "create" | "delete"
|
||||
>;
|
||||
projectMicrosoftTeamsConfigDAL: Pick<
|
||||
TProjectMicrosoftTeamsConfigDALFactory,
|
||||
"findOne" | "transaction" | "updateById" | "create" | "delete"
|
||||
>;
|
||||
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
|
||||
microsoftTeamsIntegrationDAL: Pick<
|
||||
TMicrosoftTeamsIntegrationDALFactory,
|
||||
"findById" | "findByIdWithWorkflowIntegrationDetails"
|
||||
>;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||
@@ -195,7 +212,9 @@ export const projectServiceFactory = ({
|
||||
kmsService,
|
||||
projectBotDAL,
|
||||
projectSlackConfigDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
slackIntegrationDAL,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
projectTemplateService,
|
||||
groupProjectDAL,
|
||||
smtpService
|
||||
@@ -1327,13 +1346,14 @@ export const projectServiceFactory = ({
|
||||
return { secretManagerKmsKey: kmsKey };
|
||||
};
|
||||
|
||||
const getProjectSlackConfig = async ({
|
||||
const getProjectWorkflowIntegrationConfig = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TGetProjectSlackConfig) => {
|
||||
projectId,
|
||||
integration
|
||||
}: TGetProjectWorkflowIntegrationConfig) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
@@ -1352,23 +1372,56 @@ export const projectServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
|
||||
return projectSlackConfigDAL.findOne({
|
||||
projectId: project.id
|
||||
});
|
||||
if (integration === WorkflowIntegration.SLACK) {
|
||||
const config = await projectSlackConfigDAL.findOne({
|
||||
projectId: project.id
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
throw new NotFoundError({
|
||||
message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
integration,
|
||||
integrationId: config.slackIntegrationId
|
||||
};
|
||||
}
|
||||
|
||||
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||
const config = await projectMicrosoftTeamsConfigDAL.findOne({
|
||||
projectId: project.id
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
throw new NotFoundError({
|
||||
message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
integration,
|
||||
integrationId: config.microsoftTeamsIntegrationId
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const updateProjectSlackConfig = async ({
|
||||
const updateProjectWorkflowIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
slackIntegrationId,
|
||||
integration,
|
||||
integrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
}: TUpdateProjectSlackConfig) => {
|
||||
}: TUpdateProjectWorkflowIntegration) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
@@ -1376,17 +1429,177 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId);
|
||||
if (integration === WorkflowIntegration.SLACK) {
|
||||
const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels);
|
||||
const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels);
|
||||
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Slack integration with ID '${slackIntegrationId}' not found`
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Slack integration with ID '${integrationId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (slackIntegration.orgId !== actorOrgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Selected slack integration is not in the same organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
if (slackIntegration.orgId !== project.orgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Selected slack integration is not in the same organization"
|
||||
});
|
||||
}
|
||||
|
||||
const updatedWorkflowIntegration = await projectSlackConfigDAL.transaction(async (tx) => {
|
||||
const slackConfig = await projectSlackConfigDAL.findOne(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (slackConfig) {
|
||||
return projectSlackConfigDAL.updateById(
|
||||
slackConfig.id,
|
||||
{
|
||||
slackIntegrationId: integrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return projectSlackConfigDAL.create(
|
||||
{
|
||||
projectId,
|
||||
slackIntegrationId: integrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedWorkflowIntegration,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||
integrationId: slackIntegration.id,
|
||||
integration: WorkflowIntegration.SLACK
|
||||
} as const;
|
||||
}
|
||||
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||
if (isAccessRequestNotificationEnabled && !accessRequestChannels) {
|
||||
throw new BadRequestError({
|
||||
message: "Access request channels are required when access request notifications are enabled"
|
||||
});
|
||||
}
|
||||
|
||||
if (isSecretRequestNotificationEnabled && !secretRequestChannels) {
|
||||
throw new BadRequestError({
|
||||
message: "Secret request channels are required when secret request notifications are enabled"
|
||||
});
|
||||
}
|
||||
|
||||
if (!secretRequestChannels && !accessRequestChannels) {
|
||||
throw new BadRequestError({
|
||||
message: "At least one of access request channels or secret request channels is required"
|
||||
});
|
||||
}
|
||||
|
||||
const sanitizedAccessRequestChannels = MicrosoftTeamsChannelsSchema.parse(accessRequestChannels);
|
||||
const sanitizedSecretRequestChannels = MicrosoftTeamsChannelsSchema.parse(secretRequestChannels);
|
||||
|
||||
const microsoftTeamsIntegration =
|
||||
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID '${integrationId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const updatedWorkflowIntegration = await projectMicrosoftTeamsConfigDAL.transaction(async (tx) => {
|
||||
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.findOne(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (microsoftTeamsConfig) {
|
||||
return projectMicrosoftTeamsConfigDAL.updateById(
|
||||
microsoftTeamsConfig.id,
|
||||
{
|
||||
microsoftTeamsIntegrationId: integrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels || {},
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels || {}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return projectMicrosoftTeamsConfigDAL.create(
|
||||
{
|
||||
projectId,
|
||||
microsoftTeamsIntegrationId: integrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels || {},
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels || {}
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedWorkflowIntegration,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||
integrationId: microsoftTeamsIntegration.id,
|
||||
integration: WorkflowIntegration.MICROSOFT_TEAMS
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (slackIntegration.orgId !== actorOrgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Selected slack integration is not in the same organization"
|
||||
throw new BadRequestError({
|
||||
message: `Integration type '${integration as string}' not supported`
|
||||
});
|
||||
};
|
||||
|
||||
const deleteProjectWorkflowIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
integrationId,
|
||||
integration
|
||||
}: TDeleteProjectWorkflowIntegration) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: `Project with ID '${projectId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1399,47 +1612,28 @@ export const projectServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
||||
|
||||
if (slackIntegration.orgId !== project.orgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Selected slack integration is not in the same organization"
|
||||
if (integration === WorkflowIntegration.SLACK) {
|
||||
const [deletedIntegration] = await projectSlackConfigDAL.delete({
|
||||
projectId,
|
||||
slackIntegrationId: integrationId
|
||||
});
|
||||
|
||||
return deletedIntegration;
|
||||
}
|
||||
|
||||
return projectSlackConfigDAL.transaction(async (tx) => {
|
||||
const slackConfig = await projectSlackConfigDAL.findOne(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||
const [deletedIntegration] = await projectMicrosoftTeamsConfigDAL.delete({
|
||||
projectId,
|
||||
microsoftTeamsIntegrationId: integrationId
|
||||
});
|
||||
|
||||
if (slackConfig) {
|
||||
return projectSlackConfigDAL.updateById(
|
||||
slackConfig.id,
|
||||
{
|
||||
slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
return deletedIntegration;
|
||||
}
|
||||
|
||||
return projectSlackConfigDAL.create(
|
||||
{
|
||||
projectId,
|
||||
slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
throw new BadRequestError({
|
||||
message: `Integration with ID '${integrationId}' not found`
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1548,8 +1742,9 @@ export const projectServiceFactory = ({
|
||||
getProjectKmsBackup,
|
||||
loadProjectKmsBackup,
|
||||
getProjectKmsKeys,
|
||||
getProjectSlackConfig,
|
||||
updateProjectSlackConfig,
|
||||
getProjectWorkflowIntegrationConfig,
|
||||
updateProjectWorkflowIntegration,
|
||||
deleteProjectWorkflowIntegration,
|
||||
requestProjectAccess,
|
||||
searchProjects
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
|
||||
|
||||
enum KmsType {
|
||||
External = "external",
|
||||
@@ -159,14 +160,22 @@ export type TListProjectSshCertificatesDTO = {
|
||||
limit: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetProjectSlackConfig = TProjectPermission;
|
||||
export type TGetProjectWorkflowIntegrationConfig = TProjectPermission & {
|
||||
integration: WorkflowIntegration;
|
||||
};
|
||||
|
||||
export type TUpdateProjectSlackConfig = {
|
||||
slackIntegrationId: string;
|
||||
export type TUpdateProjectWorkflowIntegration = {
|
||||
integrationId: string;
|
||||
integration: WorkflowIntegration;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
accessRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
secretRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TDeleteProjectWorkflowIntegration = {
|
||||
integrationId: string;
|
||||
integration: WorkflowIntegration;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TBootstrapSshProjectDTO = {
|
||||
|
||||
@@ -3,12 +3,13 @@ import { WebClient } from "@slack/web-api";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
|
||||
import { SlackTriggerFeature, TSlackNotification } from "./slack-types";
|
||||
import { TSendSlackNotificationDTO } from "./slack-types";
|
||||
|
||||
export const fetchSlackChannels = async (botKey: string) => {
|
||||
const slackChannels: {
|
||||
@@ -41,11 +42,11 @@ export const fetchSlackChannels = async (botKey: string) => {
|
||||
return slackChannels;
|
||||
};
|
||||
|
||||
const buildSlackPayload = (notification: TSlackNotification) => {
|
||||
const buildSlackPayload = (notification: TNotification) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
switch (notification.type) {
|
||||
case SlackTriggerFeature.SECRET_APPROVAL: {
|
||||
case TriggerFeature.SECRET_APPROVAL: {
|
||||
const { payload } = notification;
|
||||
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
||||
*Environment*: ${payload.environment}
|
||||
@@ -79,7 +80,7 @@ View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId
|
||||
payloadBlocks
|
||||
};
|
||||
}
|
||||
case SlackTriggerFeature.ACCESS_REQUEST: {
|
||||
case TriggerFeature.ACCESS_REQUEST: {
|
||||
const { payload } = notification;
|
||||
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
||||
payload.isTemporary ? "temporary" : "permanent"
|
||||
@@ -125,51 +126,24 @@ User Note: ${payload.note}`
|
||||
}
|
||||
};
|
||||
|
||||
export const triggerSlackNotification = async ({
|
||||
projectId,
|
||||
export const sendSlackNotification = async ({
|
||||
orgId,
|
||||
notification,
|
||||
projectSlackConfigDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
notification: TSlackNotification;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const slackIntegration = await projectSlackConfigDAL.getIntegrationDetailsByProject(project.id);
|
||||
|
||||
if (!slackIntegration) {
|
||||
return;
|
||||
}
|
||||
|
||||
let targetChannelIds: string[] = [];
|
||||
if (notification.type === SlackTriggerFeature.ACCESS_REQUEST) {
|
||||
targetChannelIds = slackIntegration.accessRequestChannels?.split(", ") || [];
|
||||
if (!targetChannelIds.length || !slackIntegration.isAccessRequestNotificationEnabled) {
|
||||
return;
|
||||
}
|
||||
} else if (notification.type === SlackTriggerFeature.SECRET_APPROVAL) {
|
||||
targetChannelIds = slackIntegration.secretRequestChannels?.split(", ") || [];
|
||||
if (!targetChannelIds.length || !slackIntegration.isSecretRequestNotificationEnabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
kmsService,
|
||||
targetChannelIds,
|
||||
slackIntegration
|
||||
}: TSendSlackNotificationDTO) => {
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: project.orgId
|
||||
orgId
|
||||
});
|
||||
|
||||
const botKey = orgDataKeyDecryptor({
|
||||
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||
}).toString("utf8");
|
||||
|
||||
const slackWebClient = new WebClient(botKey);
|
||||
|
||||
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
|
||||
|
||||
for await (const conversationId of targetChannelIds) {
|
||||
// we send both text and blocks for compatibility with barebone clients
|
||||
await slackWebClient.chat
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { TSlackIntegrations } from "@app/db/schemas";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { TNotification } from "@app/lib/workflow-integrations/types";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
|
||||
export type TGetSlackInstallUrlDTO = {
|
||||
slug: string;
|
||||
@@ -48,34 +52,10 @@ export type TReinstallSlackIntegrationDTO = {
|
||||
slackBotUserId: string;
|
||||
};
|
||||
|
||||
export enum SlackTriggerFeature {
|
||||
SECRET_APPROVAL = "secret-approval",
|
||||
ACCESS_REQUEST = "access-request"
|
||||
}
|
||||
|
||||
export type TSlackNotification =
|
||||
| {
|
||||
type: SlackTriggerFeature.SECRET_APPROVAL;
|
||||
payload: {
|
||||
userEmail: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
secretKeys: string[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: SlackTriggerFeature.ACCESS_REQUEST;
|
||||
payload: {
|
||||
requesterFullName: string;
|
||||
requesterEmail: string;
|
||||
isTemporary: boolean;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectName: string;
|
||||
permissions: string[];
|
||||
approvalUrl: string;
|
||||
note?: string;
|
||||
};
|
||||
};
|
||||
export type TSendSlackNotificationDTO = {
|
||||
orgId: string;
|
||||
notification: TNotification;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
targetChannelIds: string[];
|
||||
slackIntegration: TSlackIntegrations;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns";
|
||||
import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { RootKeyEncryptionStrategy } from "../kms/kms-types";
|
||||
import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
||||
@@ -47,6 +48,7 @@ type TSuperAdminServiceFactoryDep = {
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
|
||||
};
|
||||
|
||||
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
||||
@@ -77,7 +79,8 @@ export const superAdminServiceFactory = ({
|
||||
licenseService,
|
||||
identityAccessTokenDAL,
|
||||
identityTokenAuthDAL,
|
||||
identityOrgMembershipDAL
|
||||
identityOrgMembershipDAL,
|
||||
microsoftTeamsService
|
||||
}: TSuperAdminServiceFactoryDep) => {
|
||||
const initServerCfg = async () => {
|
||||
// TODO(akhilmhdh): bad pattern time less change this later to me itself
|
||||
@@ -125,7 +128,13 @@ export const superAdminServiceFactory = ({
|
||||
};
|
||||
|
||||
const updateServerCfg = async (
|
||||
data: TSuperAdminUpdate & { slackClientId?: string; slackClientSecret?: string },
|
||||
data: TSuperAdminUpdate & {
|
||||
slackClientId?: string;
|
||||
slackClientSecret?: string;
|
||||
microsoftTeamsAppId?: string;
|
||||
microsoftTeamsClientSecret?: string;
|
||||
microsoftTeamsBotId?: string;
|
||||
},
|
||||
userId: string
|
||||
) => {
|
||||
const updatedData = data;
|
||||
@@ -192,10 +201,46 @@ export const superAdminServiceFactory = ({
|
||||
updatedData.slackClientSecret = undefined;
|
||||
}
|
||||
|
||||
if (data.microsoftTeamsAppId) {
|
||||
const encryptedClientId = encryptWithRoot(Buffer.from(data.microsoftTeamsAppId));
|
||||
|
||||
updatedData.encryptedMicrosoftTeamsAppId = encryptedClientId;
|
||||
updatedData.microsoftTeamsAppId = undefined;
|
||||
}
|
||||
|
||||
if (data.microsoftTeamsClientSecret) {
|
||||
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.microsoftTeamsClientSecret));
|
||||
|
||||
updatedData.encryptedMicrosoftTeamsClientSecret = encryptedClientSecret;
|
||||
updatedData.microsoftTeamsClientSecret = undefined;
|
||||
}
|
||||
|
||||
if (data.microsoftTeamsBotId) {
|
||||
const encryptedBotId = encryptWithRoot(Buffer.from(data.microsoftTeamsBotId));
|
||||
|
||||
updatedData.encryptedMicrosoftTeamsBotId = encryptedBotId;
|
||||
updatedData.microsoftTeamsBotId = undefined;
|
||||
}
|
||||
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
|
||||
|
||||
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
|
||||
|
||||
if (
|
||||
updatedServerCfg.encryptedMicrosoftTeamsAppId &&
|
||||
updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
|
||||
updatedServerCfg.encryptedMicrosoftTeamsBotId
|
||||
) {
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsBotId); // validate that we're able to decrypt the bot ID
|
||||
const decryptedAppId = decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsAppId);
|
||||
const decryptedAppPassword = decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsClientSecret);
|
||||
|
||||
await microsoftTeamsService.initializeTeamsBot({
|
||||
botAppId: decryptedAppId.toString(),
|
||||
botAppPassword: decryptedAppPassword.toString()
|
||||
});
|
||||
}
|
||||
|
||||
return updatedServerCfg;
|
||||
};
|
||||
|
||||
@@ -488,29 +533,40 @@ export const superAdminServiceFactory = ({
|
||||
await userDAL.updateById(userId, { superAdmin: true });
|
||||
};
|
||||
|
||||
const getAdminSlackConfig = async () => {
|
||||
const getAdminIntegrationsConfig = async () => {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (!serverCfg) {
|
||||
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
|
||||
}
|
||||
|
||||
let clientId = "";
|
||||
let clientSecret = "";
|
||||
|
||||
const decrypt = kmsService.decryptWithRootKey();
|
||||
|
||||
if (serverCfg.encryptedSlackClientId) {
|
||||
clientId = decrypt(serverCfg.encryptedSlackClientId).toString();
|
||||
}
|
||||
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
|
||||
const slackClientSecret = serverCfg.encryptedSlackClientSecret
|
||||
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
||||
: "";
|
||||
|
||||
if (serverCfg.encryptedSlackClientSecret) {
|
||||
clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString();
|
||||
}
|
||||
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
||||
: "";
|
||||
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
|
||||
: "";
|
||||
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
clientId,
|
||||
clientSecret
|
||||
slack: {
|
||||
clientSecret: slackClientSecret,
|
||||
clientId: slackClientId
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: microsoftAppId,
|
||||
clientSecret: microsoftClientSecret,
|
||||
botId: microsoftBotId
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -578,7 +634,7 @@ export const superAdminServiceFactory = ({
|
||||
getUsers,
|
||||
deleteUser,
|
||||
getIdentities,
|
||||
getAdminSlackConfig,
|
||||
getAdminIntegrationsConfig,
|
||||
updateRootEncryptionStrategy,
|
||||
getConfiguredEncryptionStrategies,
|
||||
grantServerAdminAccessToUser,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export enum WorkflowIntegration {
|
||||
SLACK = "slack"
|
||||
SLACK = "slack",
|
||||
MICROSOFT_TEAMS = "microsoft-teams"
|
||||
}
|
||||
|
||||
export enum WorkflowIntegrationStatus {
|
||||
PENDING = "pending",
|
||||
INSTALLED = "installed",
|
||||
FAILED = "failed"
|
||||
}
|
||||
|
||||
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
Reference in New Issue
Block a user