diff --git a/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts b/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts index fe571fa50b..af0ec8ba02 100644 --- a/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts +++ b/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts @@ -63,6 +63,7 @@ export interface ISyncFormData { id: string; name: string; credentialId?: number; + integration?: AttributeSyncIntegrations; enabled: boolean; organizationId: number; ruleId: string; @@ -138,7 +139,7 @@ export interface IIntegrationAttributeSyncUpdateParams { integrationAttributeSync: Omit< IntegrationAttributeSync, "attributeSyncRule" | "syncFieldMappings" | "integration" - >; + > & { integration?: AttributeSyncIntegrations }; attributeSyncRule: AttributeSyncRule; fieldMappingsToCreate: Omit[]; fieldMappingsToUpdate: AttributeSyncFieldMapping[]; diff --git a/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts b/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts index 3d3ce37133..50e7a1c4cb 100644 --- a/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts +++ b/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts @@ -217,4 +217,11 @@ export class IntegrationAttributeSyncService { async getAllByCredentialId(credentialId: number) { return this.deps.integrationAttributeSyncRepository.getAllByCredentialId(credentialId); } + + async validateCredentialBelongsToOrg(credentialId: number, organizationId: number) { + return this.deps.credentialRepository.findByIdAndTeamId({ + id: credentialId, + teamId: organizationId, + }); + } } diff --git a/packages/trpc/server/routers/viewer/attribute-sync/updateAttributeSync.handler.ts b/packages/trpc/server/routers/viewer/attribute-sync/updateAttributeSync.handler.ts index 207fa6e480..1ff9b2e497 100644 --- a/packages/trpc/server/routers/viewer/attribute-sync/updateAttributeSync.handler.ts +++ b/packages/trpc/server/routers/viewer/attribute-sync/updateAttributeSync.handler.ts @@ -1,4 +1,8 @@ import { getIntegrationAttributeSyncService } from "@calcom/ee/integration-attribute-sync/di/IntegrationAttributeSyncService.container"; +import { + AttributeSyncIntegrations, + type ISyncFormData, +} from "@calcom/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository"; import { DuplicateAttributeWithinSyncError, DuplicateAttributeAcrossSyncsError, @@ -37,8 +41,29 @@ const updateAttributeSyncHandler = async ({ ctx, input }: UpdateAttributeSyncOpt throw new TRPCError({ code: "UNAUTHORIZED" }); } + // Never trust user-supplied organizationId — override with authenticated user's org + const safeInput: ISyncFormData = { ...input, organizationId: org.id }; + + // Validate credential belongs to the user's organization and derive integration type + if (input.credentialId !== undefined) { + const credential = await integrationAttributeSyncService.validateCredentialBelongsToOrg( + input.credentialId, + org.id + ); + if (!credential) { + throw new TRPCError({ code: "NOT_FOUND", message: "Credential not found" }); + } + + const integrationValue = credential.app?.slug || credential.type; + if (!Object.values(AttributeSyncIntegrations).includes(integrationValue as AttributeSyncIntegrations)) { + throw new TRPCError({ code: "BAD_REQUEST", message: `Unsupported integration type: ${integrationValue}` }); + } + + safeInput.integration = integrationValue as AttributeSyncIntegrations; + } + try { - await integrationAttributeSyncService.updateIncludeRulesAndMappings(input); + await integrationAttributeSyncService.updateIncludeRulesAndMappings(safeInput); } catch (error) { if (error instanceof DuplicateAttributeWithinSyncError) { throw new TRPCError({