fix: CalDAV credential verification between multiple instances (#22331)

Co-authored-by: zomars@cal.com <zomars@me.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Omar López
2025-07-15 01:32:52 +01:00
committed by GitHub
co-authored by Devin AI
parent 9631f083b8
commit f117b2df32
4 changed files with 888 additions and 18 deletions
+452
View File
@@ -0,0 +1,452 @@
import type { DestinationCalendar } from "@prisma/client";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import type { CredentialForCalendarService } from "@calcom/types/Credential";
import EventManager from "./EventManager";
vi.mock("@calcom/lib/crypto", () => ({
symmetricDecrypt: vi.fn(),
}));
const mockedSymmetricDecrypt = vi.mocked(symmetricDecrypt);
function buildCalDAVCredential(data: {
id: number;
key: string;
userId?: number;
}): CredentialForCalendarService {
return {
id: data.id,
type: "caldav_calendar",
key: data.key,
userId: data.userId || 1,
user: { email: "test@example.com" },
teamId: null,
appId: "caldav-calendar",
invalid: false,
delegatedTo: null,
delegationCredentialId: null,
};
}
function buildDestinationCalendar(data: {
id: number;
integration: string;
externalId: string | null;
}): DestinationCalendar {
return {
id: data.id,
integration: data.integration,
externalId: data.externalId || "",
userId: 1,
eventTypeId: null,
credentialId: null,
primaryEmail: null,
createdAt: new Date(),
updatedAt: new Date(),
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
};
}
describe("EventManager CalDAV credential validation", () => {
let eventManager: EventManager;
beforeEach(() => {
vi.clearAllMocks();
eventManager = new EventManager({
credentials: [],
destinationCalendar: null,
});
});
describe("extractServerUrlFromCredential", () => {
it("should extract server URL from valid CalDAV credential", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "https://caldav.example.com/dav/calendars/user/",
})
);
const result = (eventManager as any).extractServerUrlFromCredential(credential);
expect(result).toBe("https://caldav.example.com");
expect(mockedSymmetricDecrypt).toHaveBeenCalledWith(
"encrypted_key",
process.env.CALENDSO_ENCRYPTION_KEY || ""
);
});
it("should return null for non-CalDAV credential", () => {
const credential = {
...buildCalDAVCredential({ id: 1, key: "encrypted_key" }),
type: "google_calendar",
} as CredentialForCalendarService;
const result = (eventManager as any).extractServerUrlFromCredential(credential);
expect(result).toBeNull();
expect(mockedSymmetricDecrypt).not.toHaveBeenCalled();
});
it("should return null when decryption fails", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "invalid_key",
});
mockedSymmetricDecrypt.mockImplementation(() => {
throw new Error("Decryption failed");
});
const result = (eventManager as any).extractServerUrlFromCredential(credential);
expect(result).toBeNull();
});
it("should return null when decrypted data has no URL", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
})
);
const result = (eventManager as any).extractServerUrlFromCredential(credential);
expect(result).toBeNull();
});
it("should return null when URL is invalid", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "invalid-url",
})
);
const result = (eventManager as any).extractServerUrlFromCredential(credential);
expect(result).toBeNull();
});
it("should handle URLs with different ports", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "https://caldav.example.com:8443/dav/calendars/user/",
})
);
const result = (eventManager as any).extractServerUrlFromCredential(credential);
expect(result).toBe("https://caldav.example.com:8443");
});
it("should handle HTTP URLs", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "http://caldav.internal.com/dav/calendars/user/",
})
);
const result = (eventManager as any).extractServerUrlFromCredential(credential);
expect(result).toBe("http://caldav.internal.com");
});
});
describe("extractServerUrlFromDestination", () => {
it("should extract server URL from CalDAV destination calendar", () => {
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "https://caldav.example.com/dav/calendars/user/calendar1/",
});
const result = (eventManager as any).extractServerUrlFromDestination(destination);
expect(result).toBe("https://caldav.example.com");
});
it("should return null for non-CalDAV destination", () => {
const destination = buildDestinationCalendar({
id: 1,
integration: "google_calendar",
externalId: "calendar@gmail.com",
});
const result = (eventManager as any).extractServerUrlFromDestination(destination);
expect(result).toBeNull();
});
it("should return null when externalId is null", () => {
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: null,
});
const result = (eventManager as any).extractServerUrlFromDestination(destination);
expect(result).toBeNull();
});
it("should return null when externalId is invalid URL", () => {
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "invalid-url",
});
const result = (eventManager as any).extractServerUrlFromDestination(destination);
expect(result).toBeNull();
});
it("should handle URLs with different ports", () => {
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "https://caldav.example.com:8443/dav/calendars/user/calendar1/",
});
const result = (eventManager as any).extractServerUrlFromDestination(destination);
expect(result).toBe("https://caldav.example.com:8443");
});
it("should handle HTTP URLs", () => {
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "http://caldav.internal.com/dav/calendars/user/calendar1/",
});
const result = (eventManager as any).extractServerUrlFromDestination(destination);
expect(result).toBe("http://caldav.internal.com");
});
});
describe("credentialMatchesDestination", () => {
it("should return true for matching CalDAV server URLs", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "https://caldav.example.com/dav/calendars/user/calendar1/",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "https://caldav.example.com/dav/calendars/user/",
})
);
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
expect(result).toBe(true);
});
it("should return false for non-matching CalDAV server URLs", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "https://different.example.com/dav/calendars/user/calendar1/",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "https://caldav.example.com/dav/calendars/user/",
})
);
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
expect(result).toBe(false);
});
it("should return true for non-CalDAV credentials", () => {
const credential = {
...buildCalDAVCredential({ id: 1, key: "encrypted_key" }),
type: "google_calendar",
} as CredentialForCalendarService;
const destination = buildDestinationCalendar({
id: 1,
integration: "google_calendar",
externalId: "calendar@gmail.com",
});
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
expect(result).toBe(true);
expect(symmetricDecrypt).not.toHaveBeenCalled();
});
it("should return true when credential is CalDAV but destination is not", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
const destination = buildDestinationCalendar({
id: 1,
integration: "google_calendar",
externalId: "calendar@gmail.com",
});
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
expect(result).toBe(true);
expect(symmetricDecrypt).not.toHaveBeenCalled();
});
it("should return false when credential URL extraction fails", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "invalid_key",
});
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "https://caldav.example.com/dav/calendars/user/calendar1/",
});
mockedSymmetricDecrypt.mockImplementation(() => {
throw new Error("Decryption failed");
});
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
expect(result).toBe(false);
});
it("should return false when destination URL extraction fails", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "invalid-url",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "https://caldav.example.com/dav/calendars/user/",
})
);
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
expect(result).toBe(false);
});
it("should handle URLs with different paths but same server", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "https://caldav.example.com/different/path/calendar1/",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "https://caldav.example.com/dav/calendars/user/",
})
);
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
expect(result).toBe(true);
});
it("should handle URLs with different ports as non-matching", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "https://caldav.example.com:8443/dav/calendars/user/calendar1/",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "https://caldav.example.com:9443/dav/calendars/user/",
})
);
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
expect(result).toBe(false);
});
it("should handle HTTP vs HTTPS as non-matching", () => {
const credential = buildCalDAVCredential({
id: 1,
key: "encrypted_key",
});
const destination = buildDestinationCalendar({
id: 1,
integration: "caldav_calendar",
externalId: "https://caldav.example.com/dav/calendars/user/calendar1/",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "http://caldav.example.com/dav/calendars/user/",
})
);
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
expect(result).toBe(false);
});
});
});
+100 -6
View File
@@ -12,6 +12,7 @@ import getApps from "@calcom/app-store/utils";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { getUid } from "@calcom/lib/CalEventParser";
import CRMScheduler from "@calcom/lib/crmManager/tasker/crmScheduler";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import logger from "@calcom/lib/logger";
import {
getPiiFreeDestinationCalendar,
@@ -40,6 +41,8 @@ import { isDelegationCredential } from "./delegationCredential/clientAndServer";
import { createMeeting, updateMeeting, deleteMeeting } from "./videoClient";
const log = logger.getSubLogger({ prefix: ["EventManager"] });
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
const CALDAV_CALENDAR_TYPE = "caldav_calendar";
export const isDedicatedIntegration = (location: string): boolean => {
return location !== MeetLocationType && location.includes("integrations:");
};
@@ -172,6 +175,82 @@ export default class EventManager {
this.appOptions = eventTypeAppMetadata;
}
private extractServerUrlFromCredential(credential: CredentialForCalendarService): string | null {
try {
if (credential.type !== CALDAV_CALENDAR_TYPE) {
return null;
}
const decryptedData = JSON.parse(symmetricDecrypt(credential.key as string, CALENDSO_ENCRYPTION_KEY));
if (!decryptedData.url) {
return null;
}
const url = new URL(decryptedData.url);
return `${url.protocol}//${url.host}`;
} catch (error) {
log.warn("Failed to extract server URL from CalDAV credential", {
credentialId: credential.id,
error: error instanceof Error ? error.message : "Unknown error",
});
return null;
}
}
private extractServerUrlFromDestination(destination: DestinationCalendar): string | null {
try {
if (destination.integration !== CALDAV_CALENDAR_TYPE || !destination.externalId) {
return null;
}
const url = new URL(destination.externalId);
return `${url.protocol}//${url.host}`;
} catch (error) {
log.warn("Failed to extract server URL from destination calendar", {
destinationId: destination.id,
externalId: destination.externalId,
error: error instanceof Error ? error.message : "Unknown error",
});
return null;
}
}
private credentialMatchesDestination(
credential: CredentialForCalendarService,
destination: DestinationCalendar
): boolean {
if (credential.type !== CALDAV_CALENDAR_TYPE || destination.integration !== CALDAV_CALENDAR_TYPE) {
return true;
}
const credentialServerUrl = this.extractServerUrlFromCredential(credential);
const destinationServerUrl = this.extractServerUrlFromDestination(destination);
if (!credentialServerUrl || !destinationServerUrl) {
log.warn("Could not extract server URLs for CalDAV credential validation", {
credentialId: credential.id,
destinationId: destination.id,
credentialServerUrl,
destinationServerUrl,
});
return false;
}
const matches = credentialServerUrl === destinationServerUrl;
if (!matches) {
log.warn("CalDAV credential server URL does not match destination calendar server URL", {
credentialId: credential.id,
destinationId: destination.id,
credentialServerUrl,
destinationServerUrl,
});
}
return matches;
}
/**
* Takes a CalendarEvent and creates all necessary integration entries for it.
* When a video integration is chosen as the event's location, a video integration
@@ -774,18 +853,33 @@ export default class EventManager {
}
}
} else {
const destinationCalendarCredentials = this.calendarCredentials.filter(
(c) => c.type === destination.integration
);
const destinationCalendarCredentials = this.calendarCredentials.filter((c) => {
if (c.type !== destination.integration) return false;
if (c.type === CALDAV_CALENDAR_TYPE) {
return this.credentialMatchesDestination(c, destination);
}
return true;
});
// It might not be the first connected calendar as it seems that the order is not guaranteed to be ascending of credentialId.
const firstCalendarCredential = destinationCalendarCredentials[0] as
| (typeof destinationCalendarCredentials)[number]
| undefined;
if (!firstCalendarCredential) {
log.warn(
"No other credentials found of the same type as the destination calendar. Falling back to first connected calendar"
);
if (destination.integration === CALDAV_CALENDAR_TYPE) {
log.warn(
"No CalDAV credentials found with matching server URL for destination calendar. This prevents credential leakage.",
safeStringify({
destination: getPiiFreeDestinationCalendar(destination),
})
);
} else {
log.warn(
"No other credentials found of the same type as the destination calendar. Falling back to first connected calendar"
);
}
await fallbackToFirstCalendarInTheList();
} else {
log.warn(
+231 -1
View File
@@ -7,7 +7,17 @@ import logger from "@calcom/lib/logger";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
import getCalendarsEvents, { getCalendarsEventsWithTimezones } from "./getCalendarsEvents";
import { symmetricDecrypt } from "./crypto";
import getCalendarsEvents, {
getCalendarsEventsWithTimezones,
filterSelectedCalendarsForCredential,
} from "./getCalendarsEvents";
vi.mock("./crypto", () => ({
symmetricDecrypt: vi.fn(),
}));
const mockedSymmetricDecrypt = vi.mocked(symmetricDecrypt);
function buildDelegationCredential(credential: CredentialPayload): CredentialForCalendarService {
return {
@@ -49,6 +59,12 @@ function buildSelectedCalendar(credential: {
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
error: null,
createdAt: new Date(),
updatedAt: new Date(),
lastErrorAt: null,
watchAttempts: 0,
unwatchAttempts: 0,
maxAttempts: 3,
...credential,
};
}
@@ -73,6 +89,8 @@ describe("getCalendarsEvents", () => {
teamId: null,
appId: "exampleApp",
invalid: false,
delegationCredentialId: null,
user: null,
};
});
@@ -324,6 +342,7 @@ describe("getCalendarsEventsWithTimezones", () => {
user: null,
appId: "exampleApp",
invalid: false,
delegationCredentialId: null,
};
});
@@ -471,3 +490,214 @@ describe("getCalendarsEventsWithTimezones", () => {
});
});
});
// CalDAV Credential Leak Prevention Tests
describe("CalDAV credential leak prevention", () => {
function buildCalDAVCredential(data: {
id: number;
key: string;
userId?: number;
}): CredentialForCalendarService {
return {
id: data.id,
type: "caldav_calendar",
key: data.key,
userId: data.userId || 1,
user: { email: "test@example.com" },
teamId: null,
appId: "caldav-calendar",
invalid: false,
delegatedTo: null,
delegationCredentialId: null,
};
}
function buildCalDAVSelectedCalendar(data: {
id: string;
externalId: string;
credentialId?: number;
}): SelectedCalendar {
return {
id: data.id,
userId: 1,
integration: "caldav_calendar",
externalId: data.externalId,
credentialId: data.credentialId || null,
createdAt: new Date(),
updatedAt: new Date(),
googleChannelId: null,
googleChannelKind: null,
googleChannelResourceId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
error: null,
lastErrorAt: null,
watchAttempts: 0,
unwatchAttempts: 0,
maxAttempts: 3,
eventTypeId: null,
};
}
beforeEach(() => {
vi.clearAllMocks();
});
describe("filterSelectedCalendarsForCredential", () => {
it("prevents CalDAV credential leak by matching server URLs", () => {
// Setup: Two CalDAV servers with different URLs
const serverACredential = buildCalDAVCredential({
id: 1,
key: "encrypted_server_a_key",
});
const serverBCredential = buildCalDAVCredential({
id: 2,
key: "encrypted_server_b_key",
});
// Mock encrypted credential data for different servers
mockedSymmetricDecrypt
.mockReturnValueOnce(
JSON.stringify({
username: "user_a",
password: "pass_a",
url: "https://server-a.example.com/dav/calendars/user/",
})
)
.mockReturnValueOnce(
JSON.stringify({
username: "user_b",
password: "pass_b",
url: "https://server-b.example.com/dav/calendars/user/",
})
);
// Selected calendars from both servers
const selectedCalendars = [
buildCalDAVSelectedCalendar({
id: "cal_1",
externalId: "https://server-a.example.com/dav/calendars/user/calendar1/",
credentialId: 1,
}),
buildCalDAVSelectedCalendar({
id: "cal_2",
externalId: "https://server-b.example.com/dav/calendars/user/calendar2/",
credentialId: 2,
}),
];
// Test Server A credential - should only return Server A calendars
const serverACalendars = filterSelectedCalendarsForCredential(selectedCalendars, serverACredential);
expect(serverACalendars).toHaveLength(1);
expect(serverACalendars[0].externalId).toBe(
"https://server-a.example.com/dav/calendars/user/calendar1/"
);
// Test Server B credential - should only return Server B calendars
const serverBCalendars = filterSelectedCalendarsForCredential(selectedCalendars, serverBCredential);
expect(serverBCalendars).toHaveLength(1);
expect(serverBCalendars[0].externalId).toBe(
"https://server-b.example.com/dav/calendars/user/calendar2/"
);
});
it("demonstrates the credential leak that existed before the fix", () => {
// This test shows what WOULD happen with naive filtering (integration type only)
const serverACredential = buildCalDAVCredential({
id: 1,
key: "encrypted_server_a_key",
});
const selectedCalendars = [
buildCalDAVSelectedCalendar({
id: "cal_1",
externalId: "https://server-a.example.com/dav/calendars/user/calendar1/",
credentialId: 1,
}),
buildCalDAVSelectedCalendar({
id: "cal_2",
externalId: "https://server-b.example.com/dav/calendars/user/calendar2/",
credentialId: 2,
}),
];
// Legacy filtering (type-only) would return ALL CalDAV calendars
const legacyFiltering = selectedCalendars.filter((sc) => sc.integration === "caldav_calendar");
expect(legacyFiltering).toHaveLength(2); // This demonstrates the leak - both calendars returned
// Our new filtering prevents this
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user_a",
password: "pass_a",
url: "https://server-a.example.com/dav/calendars/user/",
})
);
const secureFiltering = filterSelectedCalendarsForCredential(selectedCalendars, serverACredential);
expect(secureFiltering).toHaveLength(1); // Only calendars from matching server
expect(secureFiltering[0].externalId).toContain("server-a.example.com");
});
it("handles non-CalDAV calendars normally", () => {
const googleCredential: CredentialForCalendarService = {
id: 1,
type: "google_calendar",
key: "google_key",
userId: 1,
user: { email: "test@example.com" },
teamId: null,
appId: "google-calendar",
invalid: false,
delegatedTo: null,
delegationCredentialId: null,
};
const selectedCalendars = [
buildCalDAVSelectedCalendar({
id: "cal_1",
externalId: "https://server-a.example.com/dav/calendars/user/calendar1/",
}),
{
...buildCalDAVSelectedCalendar({
id: "cal_2",
externalId: "primary",
}),
integration: "google_calendar",
},
];
const googleCalendars = filterSelectedCalendarsForCredential(selectedCalendars, googleCredential);
expect(googleCalendars).toHaveLength(1);
expect(googleCalendars[0].integration).toBe("google_calendar");
});
it("handles invalid CalDAV credential URLs gracefully", () => {
const invalidCredential = buildCalDAVCredential({
id: 1,
key: "encrypted_invalid_key",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "invalid-url-format",
})
);
const selectedCalendars = [
buildCalDAVSelectedCalendar({
id: "cal_1",
externalId: "https://server-a.example.com/dav/calendars/user/calendar1/",
}),
];
const result = filterSelectedCalendarsForCredential(selectedCalendars, invalidCredential);
expect(result).toHaveLength(0); // Should return empty array for safety
});
});
});
+105 -11
View File
@@ -7,7 +7,11 @@ import { performance } from "@calcom/lib/server/perfObserver";
import type { EventBusyDate, SelectedCalendar } from "@calcom/types/Calendar";
import type { CredentialForCalendarService } from "@calcom/types/Credential";
import { symmetricDecrypt } from "./crypto";
const log = logger.getSubLogger({ prefix: ["getCalendarsEvents"] });
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
// only for Google Calendar for now
export const getCalendarsEventsWithTimezones = async (
withCredentials: CredentialForCalendarService[],
@@ -35,15 +39,16 @@ export const getCalendarsEventsWithTimezones = async (
if (!c) return [];
/** We rely on the index so we can match credentials with calendars */
const { type } = calendarCredentials[i];
const credential = calendarToCredentialMap.get(c);
/** We just pass the calendars that matched the credential type,
* TODO: Migrate credential type or appId
*/
const passedSelectedCalendars = selectedCalendars
.filter((sc) => sc.integration === type)
// Needed to ensure cache keys are consistent
.sort((a, b) => (a.externalId < b.externalId ? -1 : a.externalId > b.externalId ? 1 : 0));
const credential = calendarToCredentialMap.get(c);
const passedSelectedCalendars = credential
? filterSelectedCalendarsForCredential(selectedCalendars, credential)
: selectedCalendars
.filter((sc) => sc.integration === type)
// Needed to ensure cache keys are consistent
.sort((a, b) => (a.externalId < b.externalId ? -1 : a.externalId > b.externalId ? 1 : 0));
const isADelegationCredential = credential && isDelegationCredential({ credentialId: credential.id });
// We want to fallback to primary calendar when no selectedCalendars are passed
// Default behaviour for Google Calendar is to use all available calendars, which isn't good default.
@@ -103,15 +108,17 @@ const getCalendarsEvents = async (
if (!calendarService) return [];
/** We rely on the index so we can match credentials with calendars */
const { type, appId } = calendarCredentials[i];
const credential = calendarToCredentialMap.get(calendarService);
/** We just pass the calendars that matched the credential type,
* TODO: Migrate credential type or appId
*/
// Important to have them unique so that
const passedSelectedCalendars = selectedCalendars
.filter((sc) => sc.integration === type)
// Needed to ensure cache keys are consistent
.sort((a, b) => (a.externalId < b.externalId ? -1 : a.externalId > b.externalId ? 1 : 0));
const credential = calendarToCredentialMap.get(calendarService);
const passedSelectedCalendars = credential
? filterSelectedCalendarsForCredential(selectedCalendars, credential)
: selectedCalendars
.filter((sc) => sc.integration === type)
// Needed to ensure cache keys are consistent
.sort((a, b) => (a.externalId < b.externalId ? -1 : a.externalId > b.externalId ? 1 : 0));
const isADelegationCredential = credential && isDelegationCredential({ credentialId: credential.id });
// We want to fallback to primary calendar when no selectedCalendars are passed
// Default behaviour for Google Calendar is to use all available calendars, which isn't good default.
@@ -179,3 +186,90 @@ const getCalendarsEvents = async (
};
export default getCalendarsEvents;
/**
* Extract server URL from CalDAV calendar externalId
*/
function getServerUrlFromCalendarExternalId(externalId: string): string | null {
try {
const url = new URL(externalId);
return `${url.protocol}//${url.host}`;
} catch (error) {
return null;
}
}
/**
* Extract server URL from CalDAV credential
*/
function getServerUrlFromCredential(credential: CredentialForCalendarService): string | null {
try {
if (credential.type !== "caldav_calendar") {
return null;
}
const decryptedData = JSON.parse(symmetricDecrypt(credential.key as string, CALENDSO_ENCRYPTION_KEY));
if (!decryptedData.url) {
return null;
}
const url = new URL(decryptedData.url);
return `${url.protocol}//${url.host}`;
} catch (error) {
return null;
}
}
/**
* Filter selected calendars for the specific credential, handling CalDAV server URL matching
*/
export function filterSelectedCalendarsForCredential(
selectedCalendars: SelectedCalendar[],
credential: CredentialForCalendarService
): SelectedCalendar[] {
const { type } = credential;
// For all other calendar types, use the existing logic
if (type !== "caldav_calendar") {
return selectedCalendars.filter((sc) => sc.integration === type);
}
const credentialServerUrl = getServerUrlFromCredential(credential);
if (!credentialServerUrl) {
log.warn("Could not extract server URL from CalDAV credential", {
credentialId: credential.id,
});
return [];
}
return selectedCalendars.filter((sc) => {
if (sc.integration !== type) {
return false;
}
const calendarServerUrl = getServerUrlFromCalendarExternalId(sc.externalId);
if (!calendarServerUrl) {
log.warn("Could not extract server URL from calendar externalId", {
externalId: sc.externalId,
integration: sc.integration,
});
return false;
}
const matches = credentialServerUrl === calendarServerUrl;
if (!matches) {
log.debug("CalDAV calendar server URL does not match credential server URL", {
credentialId: credential.id,
credentialServerUrl,
calendarServerUrl,
calendarExternalId: sc.externalId,
});
}
return matches;
});
}