fix: requested changes

This commit is contained in:
Daniel Hougaard
2025-04-25 05:22:17 +04:00
parent f5fa57d6c5
commit 50679ba29d
16 changed files with 184 additions and 127 deletions

View File

@@ -23,7 +23,6 @@ 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(),

View File

@@ -1,4 +1,4 @@
import { MicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
import { sendSlackNotification } from "@app/services/slack/slack-fns";
import { logger } from "../logger";
@@ -52,7 +52,9 @@ export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkfl
if (microsoftTeamsConfig) {
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) {
const { success, data } = MicrosoftTeamsChannelsSchema.safeParse(microsoftTeamsConfig.accessRequestChannels);
const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse(
microsoftTeamsConfig.accessRequestChannels
);
if (success && data) {
await microsoftTeamsService
@@ -68,7 +70,9 @@ export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkfl
}
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
if (microsoftTeamsConfig.isSecretRequestNotificationEnabled && microsoftTeamsConfig.secretRequestChannels) {
const { success, data } = MicrosoftTeamsChannelsSchema.safeParse(microsoftTeamsConfig.secretRequestChannels);
const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse(
microsoftTeamsConfig.secretRequestChannels
);
if (success && data) {
await microsoftTeamsService

View File

@@ -30,7 +30,7 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) =
schema: {
body: z.object({
tenantId: z.string(),
slug: z.string()
slug: slugSchema({ max: 64 })
}),
response: {
200: sanitizedMicrosoftTeamsIntegrationSchema

View File

@@ -20,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 { validateMicrosoftTeamsChannelsSchema } 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";
@@ -666,19 +667,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
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
}
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;
}
@@ -686,7 +685,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/:projectId/workflow-integrations/:integration/:integrationId",
url: "/:projectId/workflow-integration/:integration/:integrationId",
config: {
rateLimit: writeLimit
},
@@ -695,7 +694,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim(),
integration: z.nativeEnum(WorkflowIntegration),
integrationId: z.string()
})
}),
response: {
200: z.object({
integrationConfig: z.object({
id: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
@@ -709,42 +715,41 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
integrationId: req.params.integrationId
});
return deletedIntegration;
return {
integrationConfig: deletedIntegration
};
}
});
server.route({
method: "PUT",
url: "/:workspaceId/workflow-integration/:integration",
url: "/:workspaceId/workflow-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()
)
workspaceId: z.string().trim()
}),
body: z.discriminatedUnion("integration", [
z.object({
integration: z.literal(WorkflowIntegration.SLACK),
integrationId: z.string(),
accessRequestChannels: validateSlackChannelsField,
secretRequestChannels: validateSlackChannelsField,
isAccessRequestNotificationEnabled: z.boolean(),
isSecretRequestNotificationEnabled: z.boolean()
}),
z.object({
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
integrationId: z.string(),
accessRequestChannels: validateMicrosoftTeamsChannelsSchema,
secretRequestChannels: validateMicrosoftTeamsChannelsSchema,
isAccessRequestNotificationEnabled: z.boolean(),
isSecretRequestNotificationEnabled: z.boolean()
})
]),
response: {
200: z.discriminatedUnion("integration", [
ProjectSlackConfigsSchema.pick({
@@ -767,8 +772,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
z.object({
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
integrationId: z.string(),
accessRequestChannels: MicrosoftTeamsChannelsSchema,
secretRequestChannels: MicrosoftTeamsChannelsSchema
accessRequestChannels: validateMicrosoftTeamsChannelsSchema,
secretRequestChannels: validateMicrosoftTeamsChannelsSchema
})
)
])
@@ -782,7 +787,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
integration: req.params.integration,
...req.body
});

View File

@@ -276,23 +276,27 @@ export class TeamsBot extends TeamsActivityHandler {
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
});
const microsoftTeamIntegration = await this.microsoftTeamsIntegrationDAL
.findOne({
tenantId: context.activity.conversation.tenantId
})
.catch(() => null);
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");
});
if (microsoftTeamIntegration) {
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(
@@ -308,47 +312,55 @@ export class TeamsBot extends TeamsActivityHandler {
}
async sendMessageToChannel(tenantId: string, channelId: string, teamId: string, notification: TNotification) {
const { adaptiveCard } = buildTeamsPayload(notification);
try {
const { adaptiveCard } = buildTeamsPayload(notification);
const botToken = await getMicrosoftTeamsAccessToken({
tenantId,
clientId: this.botAppId,
clientSecret: this.botAppPassword,
getBotFrameworkToken: true
});
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
const adaptiveCardActivity = {
type: "message",
attachments: [
{
contentType: "application/vnd.microsoft.card.adaptive",
content: adaptiveCard
}
],
conversation: {
id: channelId,
isGroup: true
},
team: {
id: teamId
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"
await axios.post(
`https://smba.trafficmanager.net/amer/v3/conversations/${channelId}/activities`,
adaptiveCardActivity,
{
headers: {
Authorization: `Bearer ${botToken}`,
"Content-Type": "application/json"
}
}
}
);
);
} catch (error) {
logger.error(
error,
`sendMessageToChannel: Microsoft Teams Workflow Integration: Failed to send message to channel [channelId=${channelId}] [teamId=${teamId}] [tenantId=${tenantId}]`
);
throw error;
}
}
// todo: filter out teams that the bot is not a member of
@@ -429,7 +441,7 @@ export class TeamsBot extends TeamsActivityHandler {
}
}
export const MicrosoftTeamsChannelsSchema = z
export const validateMicrosoftTeamsChannelsSchema = z
.object({
teamId: z.string(),
channelIds: z.array(z.string()).min(1)

View File

@@ -23,7 +23,7 @@ import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integrat
import {
TCheckInstallationStatusDTO,
TCreateMicrosoftTeamsIntegrationDTO,
TDeleteMicrosoftTeamsIntegrationIntegrationDTO,
TDeleteMicrosoftTeamsIntegrationDTO,
TGetMicrosoftTeamsIntegrationByIdDTO,
TGetMicrosoftTeamsIntegrationByOrgDTO,
TGetTeamsDTO,
@@ -424,7 +424,7 @@ export const microsoftTeamsServiceFactory = ({
actorOrgId,
actorAuthMethod,
id
}: TDeleteMicrosoftTeamsIntegrationIntegrationDTO) => {
}: TDeleteMicrosoftTeamsIntegrationDTO) => {
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!microsoftTeamsIntegration) {
throw new NotFoundError({

View File

@@ -22,7 +22,7 @@ export type TGetTeamsDTO = Omit<TOrgPermission, "orgId"> & {
workflowIntegrationId: string;
};
export type TDeleteMicrosoftTeamsIntegrationIntegrationDTO = {
export type TDeleteMicrosoftTeamsIntegrationDTO = {
id: string;
} & Omit<TOrgPermission, "orgId">;

View File

@@ -43,7 +43,7 @@ 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 { validateMicrosoftTeamsChannelsSchema } 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";
@@ -1407,6 +1407,10 @@ export const projectServiceFactory = ({
integrationId: config.microsoftTeamsIntegrationId
};
}
throw new BadRequestError({
message: `Integration type '${integration as string}' not supported`
});
};
const updateProjectWorkflowIntegration = async ({
@@ -1526,9 +1530,6 @@ export const projectServiceFactory = ({
});
}
const sanitizedAccessRequestChannels = MicrosoftTeamsChannelsSchema.parse(accessRequestChannels);
const sanitizedSecretRequestChannels = MicrosoftTeamsChannelsSchema.parse(secretRequestChannels);
const microsoftTeamsIntegration =
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
@@ -1538,6 +1539,32 @@ export const projectServiceFactory = ({
});
}
if (microsoftTeamsIntegration.orgId !== actorOrgId) {
throw new ForbiddenRequestError({
message: "Selected Microsoft Teams 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 (microsoftTeamsIntegration.orgId !== project.orgId) {
throw new ForbiddenRequestError({
message: "Selected Microsoft Teams integration is not in the same organization"
});
}
const sanitizedAccessRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(accessRequestChannels);
const sanitizedSecretRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(secretRequestChannels);
const updatedWorkflowIntegration = await projectMicrosoftTeamsConfigDAL.transaction(async (tx) => {
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.findOne(
{

View File

@@ -164,14 +164,25 @@ export type TGetProjectWorkflowIntegrationConfig = TProjectPermission & {
integration: WorkflowIntegration;
};
export type TUpdateProjectWorkflowIntegration = {
integrationId: string;
integration: WorkflowIntegration;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels?: string | { teamId: string; channelIds: string[] };
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels?: string | { teamId: string; channelIds: string[] };
} & TProjectPermission;
export type TUpdateProjectWorkflowIntegration = (
| {
integrationId: string;
integration: WorkflowIntegration.SLACK;
isAccessRequestNotificationEnabled: boolean;
isSecretRequestNotificationEnabled: boolean;
accessRequestChannels?: string;
secretRequestChannels?: string;
}
| {
integrationId: string;
integration: WorkflowIntegration.MICROSOFT_TEAMS;
isAccessRequestNotificationEnabled: boolean;
isSecretRequestNotificationEnabled: boolean;
accessRequestChannels?: { teamId: string; channelIds: string[] };
secretRequestChannels?: { teamId: string; channelIds: string[] };
}
) &
TProjectPermission;
export type TDeleteProjectWorkflowIntegration = {
integrationId: string;

View File

@@ -7,7 +7,7 @@ import { workflowIntegrationKeys } from "./queries";
import {
TCheckMicrosoftTeamsIntegrationInstallationStatusDTO,
TCreateMicrosoftTeamsIntegrationDTO,
TDeleteMicrosoftTeamsIntegrationIntegrationDTO,
TDeleteMicrosoftTeamsIntegrationDTO,
TDeleteProjectWorkflowIntegrationDTO,
TDeleteSlackIntegrationDTO,
TUpdateMicrosoftTeamsIntegrationDTO,
@@ -93,7 +93,7 @@ export const useDeleteSlackIntegration = () => {
export const useDeleteMicrosoftTeamsIntegration = () => {
const queryClient = useQueryClient();
return useMutation<object, object, TDeleteMicrosoftTeamsIntegrationIntegrationDTO>({
return useMutation<object, object, TDeleteMicrosoftTeamsIntegrationDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.delete(
`/api/v1/workflow-integrations/microsoft-teams/${dto.id}`
@@ -118,7 +118,7 @@ export const useUpdateProjectWorkflowIntegrationConfig = () => {
return useMutation({
mutationFn: async (dto: TUpdateProjectWorkflowIntegrationConfigDTO) => {
const { data } = await apiRequest.put(
`/api/v1/workspace/${dto.workspaceId}/workflow-integration/${dto.integration}`,
`/api/v1/workspace/${dto.workspaceId}/workflow-integration`,
dto
);
@@ -138,7 +138,7 @@ export const useDeleteProjectWorkflowIntegration = () => {
return useMutation({
mutationFn: async (dto: TDeleteProjectWorkflowIntegrationDTO) => {
const { data } = await apiRequest.delete(
`/api/v1/workspace/${dto.projectId}/workflow-integrations/${dto.integration}/${dto.integrationId}`
`/api/v1/workspace/${dto.projectId}/workflow-integration/${dto.integration}/${dto.integrationId}`
);
return data;

View File

@@ -70,7 +70,7 @@ export type TDeleteSlackIntegrationDTO = {
orgId: string;
};
export type TDeleteMicrosoftTeamsIntegrationIntegrationDTO = {
export type TDeleteMicrosoftTeamsIntegrationDTO = {
id: string;
orgId: string;
};

View File

@@ -141,7 +141,7 @@ export const MicrosoftTeamsIntegrationForm = ({ adminIntegrationsConfig }: Props
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bot ID"
tooltipClassName="You can find the bot ID in your bot's manifest.json file as the `id` field."
tooltipText="You can find the bot ID in your bot's manifest.json file as the `id` field."
className="w-96"
isError={Boolean(error)}
errorText={error?.message}

View File

@@ -49,7 +49,7 @@ export const MicrosoftTeamsIntegrationForm = ({ id, onClose }: Props) => {
}
}, [microsoftTeamsIntegration]);
const handleSlackFormSubmit = async ({
const handleMicrosoftTeamsFormSubmit = async ({
slug,
description,
tenantId
@@ -97,7 +97,7 @@ export const MicrosoftTeamsIntegrationForm = ({ id, onClose }: Props) => {
};
return (
<form onSubmit={handleSubmit(handleSlackFormSubmit)} autoComplete="off">
<form onSubmit={handleSubmit(handleMicrosoftTeamsFormSubmit)} autoComplete="off">
<Controller
control={control}
name="slug"

View File

@@ -214,7 +214,7 @@ export const OrgWorkflowIntegrationTab = withPermission(
onClick={async (e) => {
e.stopPropagation();
await triggerSlackReinstall(workflowIntegration.integration);
await triggerSlackReinstall(workflowIntegration.id);
}}
>
Reinstall

View File

@@ -84,7 +84,7 @@ export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => {
(platform === WorkflowIntegrationPlatform.SLACK && slackConfigured);
return (
<div className="relative">
<div className="relative" key={platform}>
<div
key={platform}
className={twMerge(

View File

@@ -78,7 +78,7 @@ export const MicrosoftTeamsConfigRow = ({ handlePopUpOpen }: Props) => {
<Spinner size="xs" />
) : (
<Badge>
{microsoftTeamsConfig.accessRequestChannels?.channelIds
{(microsoftTeamsConfig.accessRequestChannels?.channelIds || [])
?.map((channel) => microsoftTeamsChannelIdToName[channel])
.join(", ")}
</Badge>
@@ -89,7 +89,7 @@ export const MicrosoftTeamsConfigRow = ({ handlePopUpOpen }: Props) => {
<Spinner size="xs" />
) : (
<Badge>
{microsoftTeamsConfig.secretRequestChannels?.channelIds
{(microsoftTeamsConfig.secretRequestChannels?.channelIds || [])
?.map((channel) => microsoftTeamsChannelIdToName[channel])
.join(", ")}
</Badge>