fix: improve error handling in delegation credential workspace configuration (#26985)

- Changed checkIfSuccessfullyConfiguredInWorkspace to throw detailed errors instead of returning boolean
- Updated testDelegationCredentialSetup in Google Calendar and Office365 Calendar services to throw specific errors
- Updated assertWorkspaceConfigured to let errors propagate naturally
- Updated Calendar interface type to reflect Promise<void> return type
- Updated CalendarCacheWrapper and CalendarTelemetryWrapper to match new interface

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Morgan
2026-02-04 21:48:56 +00:00
committed by GitHub
co-authored by Devin AI
parent 04e828358a
commit 2f16758de3
7 changed files with 123 additions and 113 deletions
+35 -34
View File
@@ -301,20 +301,17 @@ async function _getDelegationCredentialsMapPerUser({
return credentialsByUserId;
}
export async function checkIfSuccessfullyConfiguredInWorkspace({
export async function assertSuccessfullyConfiguredInWorkspace({
delegationCredential,
user,
}: {
delegationCredential: DelegationCredentialWithSensitiveServiceAccountKey;
user: User;
}) {
}): Promise<void> {
if (!isValidWorkspaceSlug(delegationCredential.workspacePlatform.slug)) {
log.warn(
`Only ${WORKSPACE_PLATFORM_SLUGS.toString()} Platforms are supported here, skipping ${
delegationCredential.workspacePlatform.slug
}`
);
return false;
const errorMessage = `Unsupported workspace platform: ${delegationCredential.workspacePlatform.slug}. Only ${WORKSPACE_PLATFORM_SLUGS.toString()} are supported.`;
log.warn(errorMessage);
throw new Error(errorMessage);
}
const credential = _buildDelegatedCalendarCredentialWithServiceAccountKey({
@@ -325,9 +322,10 @@ export async function checkIfSuccessfullyConfiguredInWorkspace({
const calendar = await getCalendar(credential, "none");
if (!calendar) {
throw new Error("Google Calendar App not found");
throw new Error("Calendar App not found for the workspace platform");
}
return await calendar?.testDelegationCredentialSetup?.();
await calendar.testDelegationCredentialSetup?.();
}
export async function getAllDelegationCredentialsForUserByAppType({
@@ -383,26 +381,29 @@ export const buildAllCredentials = ({
...buildNonDelegationCredentials(nonDelegationCredentials),
];
const uniqueAllCredentials = allCredentials.reduce((acc, credential) => {
if (!credential.delegatedToId) {
// Regular credential go as is
acc.push(credential);
const uniqueAllCredentials = allCredentials.reduce(
(acc, credential) => {
if (!credential.delegatedToId) {
// Regular credential go as is
acc.push(credential);
return acc;
}
const existingDelegationCredential = acc.find(
(c) => c.delegatedToId === credential.delegatedToId && c.appId === credential.appId
);
if (!existingDelegationCredential) {
acc.push(credential);
}
return acc;
}
const existingDelegationCredential = acc.find(
(c) => c.delegatedToId === credential.delegatedToId && c.appId === credential.appId
);
if (!existingDelegationCredential) {
acc.push(credential);
}
return acc;
}, [] as typeof allCredentials);
},
[] as typeof allCredentials
);
return uniqueAllCredentials;
};
export async function enrichUsersWithDelegationCredentials<
TUser extends { id: number; email: string; credentials: CredentialPayload[] }
TUser extends { id: number; email: string; credentials: CredentialPayload[] },
>({ orgId, users }: { orgId: number | null; users: TUser[] }) {
const delegationCredentialsMap = await _getDelegationCredentialsMapPerUser({
organizationId: orgId,
@@ -426,7 +427,7 @@ export async function enrichUsersWithDelegationCredentials<
export const enrichHostsWithDelegationCredentials = async <
THost extends Host<TUser>,
TUser extends { id: number; email: string; credentials: CredentialPayload[] }
TUser extends { id: number; email: string; credentials: CredentialPayload[] },
>({
orgId,
hosts,
@@ -467,7 +468,7 @@ export const enrichHostsWithDelegationCredentials = async <
};
export const enrichUserWithDelegationCredentialsIncludeServiceAccountKey = async <
TUser extends { id: number; email: string; credentials: CredentialPayload[] }
TUser extends { id: number; email: string; credentials: CredentialPayload[] },
>({
user,
}: {
@@ -487,7 +488,7 @@ export const enrichUserWithDelegationCredentialsIncludeServiceAccountKey = async
};
export const enrichUserWithDelegationCredentials = async <
TUser extends { id: number; email: string; credentials: CredentialPayload[] }
TUser extends { id: number; email: string; credentials: CredentialPayload[] },
>({
user,
}: {
@@ -503,7 +504,7 @@ export const enrichUserWithDelegationCredentials = async <
};
export async function enrichUserWithDelegationConferencingCredentialsWithoutOrgId<
TUser extends { id: number; email: string; credentials: CredentialPayload[] }
TUser extends { id: number; email: string; credentials: CredentialPayload[] },
>({ user }: { user: TUser }) {
const { credentials, ...restUser } = await enrichUserWithDelegationCredentialsIncludeServiceAccountKey({
user,
@@ -518,7 +519,7 @@ export async function enrichUserWithDelegationConferencingCredentialsWithoutOrgI
* Either get Delegation credential from delegationCredentials or find regular credential from Credential table
*/
export async function getDelegationCredentialOrFindRegularCredential<
TDelegationCredential extends { delegatedToId?: string | null }
TDelegationCredential extends { delegatedToId?: string | null },
>({
id,
delegationCredentials,
@@ -532,17 +533,17 @@ export async function getDelegationCredentialOrFindRegularCredential<
return id.delegationCredentialId
? delegationCredentials.find((cred) => cred.delegatedToId === id.delegationCredentialId)
: id.credentialId
? await CredentialRepository.findCredentialForCalendarServiceById({
id: id.credentialId,
})
: null;
? await CredentialRepository.findCredentialForCalendarServiceById({
id: id.credentialId,
})
: null;
}
/**
* Utility function to find a credential from a list of credentials, supporting both regular and DelegationCredential credentials
*/
export function getDelegationCredentialOrRegularCredential<
TCredential extends { delegatedToId?: string | null; id: number }
TCredential extends { delegatedToId?: string | null; id: number },
>({
credentials,
id,
@@ -1,19 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { calendar_v3 } from "@googleapis/calendar";
import type { GaxiosResponse } from "googleapis-common";
import { RRule } from "rrule";
import { MeetLocationType } from "@calcom/app-store/constants";
import { getDestinationCalendarRepository } from "@calcom/features/di/containers/DestinationCalendar";
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getDestinationCalendarRepository } from "@calcom/features/di/containers/DestinationCalendar";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import type { Prisma } from "@calcom/prisma/client";
import type {
Calendar,
CalendarServiceEvent,
CalendarEvent,
CalendarServiceEvent,
EventBusyDate,
GetAvailabilityParams,
IntegrationCalendar,
@@ -21,6 +19,9 @@ import type {
SelectedCalendarEventTypeIds,
} from "@calcom/types/Calendar";
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
import type { calendar_v3 } from "@googleapis/calendar";
import type { GaxiosResponse } from "googleapis-common";
import { RRule } from "rrule";
import { AxiosLikeResponseToFetchResponse } from "../../_utils/oauth/AxiosLikeResponseToFetchResponse";
import { CalendarAuth } from "./CalendarAuth";
@@ -68,7 +69,8 @@ interface GoogleCalError extends Error {
code?: number;
}
const isGaxiosResponse= (error: unknown): error is GaxiosResponse<calendar_v3.Schema$Event> =>
const isGaxiosResponse = (error: unknown): error is GaxiosResponse<calendar_v3.Schema$Event> =>
// biome-ignore lint/suspicious/noPrototypeBuiltins: Object.hasOwn not available in all build targets
typeof error === "object" && !!error && Object.prototype.hasOwnProperty.call(error, "config");
class GoogleCalendarService implements Calendar {
@@ -497,9 +499,7 @@ class GoogleCalendarService implements Calendar {
return apiResponse.json;
}
async getFreeBusyResult(
args: FreeBusyArgs,
): Promise<calendar_v3.Schema$FreeBusyResponse> {
async getFreeBusyResult(args: FreeBusyArgs): Promise<calendar_v3.Schema$FreeBusyResponse> {
return await this.fetchAvailability(args);
}
@@ -516,22 +516,23 @@ class GoogleCalendarService implements Calendar {
return validCals[0];
}
async getFreeBusyData(
args: FreeBusyArgs,
): Promise<(EventBusyDate & { id: string })[] | null> {
async getFreeBusyData(args: FreeBusyArgs): Promise<(EventBusyDate & { id: string })[] | null> {
const freeBusyResult = await this.getFreeBusyResult(args);
if (!freeBusyResult.calendars) return null;
const result = Object.entries(freeBusyResult.calendars).reduce((c, [id, i]) => {
i.busy?.forEach((busyTime) => {
c.push({
id,
start: busyTime.start || "",
end: busyTime.end || "",
const result = Object.entries(freeBusyResult.calendars).reduce(
(c, [id, i]) => {
i.busy?.forEach((busyTime) => {
c.push({
id,
start: busyTime.start || "",
end: busyTime.end || "",
});
});
});
return c;
}, [] as (EventBusyDate & { id: string })[]);
return c;
},
[] as (EventBusyDate & { id: string })[]
);
return result;
}
@@ -646,7 +647,7 @@ class GoogleCalendarService implements Calendar {
private async fetchAvailabilityData(
calendarIds: string[],
dateFrom: string,
dateTo: string,
dateTo: string
): Promise<EventBusyDate[]> {
// More efficient date difference calculation using native Date objects
// Use Math.floor to match dayjs diff behavior (truncates, doesn't round up)
@@ -657,13 +658,11 @@ class GoogleCalendarService implements Calendar {
// Google API only allows a date range of 90 days for /freebusy
if (diff <= 90) {
const freeBusyData = await this.getFreeBusyData(
{
timeMin: dateFrom,
timeMax: dateTo,
items: calendarIds.map((id) => ({ id })),
}
);
const freeBusyData = await this.getFreeBusyData({
timeMin: dateFrom,
timeMax: dateTo,
items: calendarIds.map((id) => ({ id })),
});
if (!freeBusyData) throw new Error("No response from google calendar");
return freeBusyData.map((freeBusy) => ({ start: freeBusy.start, end: freeBusy.end }));
@@ -685,13 +684,11 @@ class GoogleCalendarService implements Calendar {
currentEndTime = originalEndTime;
}
const chunkData = await this.getFreeBusyData(
{
timeMin: new Date(currentStartTime).toISOString(),
timeMax: new Date(currentEndTime).toISOString(),
items: calendarIds.map((id) => ({ id })),
}
);
const chunkData = await this.getFreeBusyData({
timeMin: new Date(currentStartTime).toISOString(),
timeMax: new Date(currentEndTime).toISOString(),
items: calendarIds.map((id) => ({ id })),
});
if (chunkData) {
busyData.push(...chunkData.map((freeBusy) => ({ start: freeBusy.start, end: freeBusy.end })));
@@ -757,7 +754,7 @@ class GoogleCalendarService implements Calendar {
primary: cal.primary ?? false,
readOnly: !(cal.accessRole === "writer" || cal.accessRole === "owner") && true,
email: cal.id ?? "",
} satisfies IntegrationCalendar)
}) satisfies IntegrationCalendar
);
} catch (error) {
this.log.error("There was an error getting calendars: ", safeStringify(error));
@@ -765,12 +762,28 @@ class GoogleCalendarService implements Calendar {
}
}
// It would error if the delegation credential is not set up correctly
async testDelegationCredentialSetup() {
async testDelegationCredentialSetup(): Promise<void> {
log.debug("Testing delegation credential setup");
const calendar = await this.authedCalendar();
const cals = await calendar.calendarList.list({ fields: "items(id)" });
return !!cals.data.items;
try {
const calendar = await this.authedCalendar();
const cals = await calendar.calendarList.list({ fields: "items(id)" });
if (!cals.data.items) {
throw new Error("No calendars found - delegation credential may not have proper access");
}
} catch (error) {
const googleError = error as { code?: number; message?: string };
if (googleError.code === 401) {
throw new Error(
`Google API authentication failed: ${googleError.message || "Invalid credentials or insufficient permissions"}`
);
}
if (googleError.code === 403) {
throw new Error(
`Google API access denied: ${googleError.message || "The service account may not have domain-wide delegation enabled or the required scopes"}`
);
}
throw error;
}
}
async createSelectedCalendar(
@@ -885,9 +898,7 @@ class GoogleCalendarService implements Calendar {
* from leaking into the emitted .d.ts file, which would cause TypeScript to load
* all Google API SDK declaration files when type-checking dependent packages.
*/
export default function BuildCalendarService(
credential: CredentialForCalendarServiceWithEmail
): Calendar {
export default function BuildCalendarService(credential: CredentialForCalendarServiceWithEmail): Calendar {
return new GoogleCalendarService(credential);
}
@@ -1,14 +1,10 @@
import type { Calendar as OfficeCalendar, User, Event } from "@microsoft/microsoft-graph-types-beta";
import type { DefaultBodyType } from "msw";
import { findIana } from "windows-iana";
import { MSTeamsLocationType } from "@calcom/app-store/constants";
import dayjs from "@calcom/dayjs";
import { triggerDelegationCredentialErrorWebhook } from "@calcom/features/webhooks/lib/triggerDelegationCredentialErrorWebhook";
import { getLocation, getRichDescriptionHTML } from "@calcom/lib/CalEventParser";
import {
CalendarAppDelegationCredentialInvalidGrantError,
CalendarAppDelegationCredentialConfigurationError,
CalendarAppDelegationCredentialInvalidGrantError,
} from "@calcom/lib/CalendarAppError";
import { handleErrorsJson, handleErrorsRaw } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
@@ -22,9 +18,11 @@ import type {
NewCalendarEventType,
} from "@calcom/types/Calendar";
import type { CredentialForCalendarServiceWithTenantId } from "@calcom/types/Credential";
import { OAuthManager } from "../../_utils/oauth/OAuthManager";
import type { Event, Calendar as OfficeCalendar, User } from "@microsoft/microsoft-graph-types-beta";
import type { DefaultBodyType } from "msw";
import { findIana } from "windows-iana";
import { getTokenObjectFromCredential } from "../../_utils/oauth/getTokenObjectFromCredential";
import { OAuthManager } from "../../_utils/oauth/OAuthManager";
import { oAuthManagerHelper } from "../../_utils/oauth/oAuthManagerHelper";
import metadata from "../_metadata";
import { getOfficeAppKeys } from "./getOfficeAppKeys";
@@ -102,12 +100,12 @@ class Office365CalendarService implements Calendar {
body: new URLSearchParams(bodyParams),
});
},
isTokenObjectUnusable: async function () {
isTokenObjectUnusable: async () => {
// TODO: Implement this. As current implementation of CalendarService doesn't handle it. It hasn't been handled in the OAuthManager implementation as well.
// This is a placeholder for future implementation.
return null;
},
isAccessTokenUnusable: async function () {
isAccessTokenUnusable: async () => {
// TODO: Implement this
return null;
},
@@ -251,8 +249,7 @@ class Office365CalendarService implements Calendar {
return this.azureUserId;
}
// It would error if the delegation credential is not set up correctly
async testDelegationCredentialSetup(): Promise<boolean> {
async testDelegationCredentialSetup(): Promise<void> {
const delegationCredentialClientId = this.credential.delegatedTo?.serviceAccountKey?.client_id;
const delegationCredentialClientSecret = this.credential.delegatedTo?.serviceAccountKey?.private_key;
const url = await this.getAuthUrl(
@@ -261,7 +258,9 @@ class Office365CalendarService implements Calendar {
);
if (!delegationCredentialClientId || !delegationCredentialClientSecret) {
return false;
throw new CalendarAppDelegationCredentialConfigurationError(
"Delegation credential is missing client_id or client_secret"
);
}
const loginResponse = await fetch(url, {
method: "POST",
@@ -274,7 +273,13 @@ class Office365CalendarService implements Calendar {
}),
});
const parsedLoginResponse = await loginResponse.json();
return Boolean(parsedLoginResponse?.access_token);
if (!parsedLoginResponse?.access_token) {
const errorDescription =
parsedLoginResponse?.error_description || parsedLoginResponse?.error || "Unknown error";
throw new CalendarAppDelegationCredentialInvalidGrantError(
`Failed to obtain access token from Microsoft: ${errorDescription}`
);
}
}
async getUserEndpoint(): Promise<string> {
@@ -284,8 +289,8 @@ class Office365CalendarService implements Calendar {
async createEvent(event: CalendarServiceEvent, credentialId: number): Promise<NewCalendarEventType> {
const mainHostDestinationCalendar = event.destinationCalendar
? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ??
event.destinationCalendar[0]
? (event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ??
event.destinationCalendar[0])
: undefined;
try {
const eventsUrl = mainHostDestinationCalendar?.externalId
@@ -502,8 +507,8 @@ class Office365CalendarService implements Calendar {
organizer: {
emailAddress: {
address: event.destinationCalendar
? event.destinationCalendar.find((cal) => cal.userId === event.organizer.id)?.externalId ??
event.organizer.email
? (event.destinationCalendar.find((cal) => cal.userId === event.organizer.id)?.externalId ??
event.organizer.email)
: event.organizer.email,
name: event.organizer.name,
},
@@ -794,8 +799,6 @@ class Office365CalendarService implements Calendar {
* from leaking into the emitted .d.ts file, which would cause TypeScript to load
* all Microsoft Graph SDK declaration files when type-checking dependent packages.
*/
export default function BuildCalendarService(
credential: CredentialForCalendarServiceWithTenantId
): Calendar {
export default function BuildCalendarService(credential: CredentialForCalendarServiceWithTenantId): Calendar {
return new Office365CalendarService(credential);
}
@@ -214,7 +214,7 @@ export class CalendarCacheWrapper implements Calendar {
return this.deps.originalCalendar.listCalendars(event);
}
testDelegationCredentialSetup?(): Promise<boolean> {
return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve(false);
testDelegationCredentialSetup?(): Promise<void> {
return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve();
}
}
@@ -190,7 +190,7 @@ export class CalendarTelemetryWrapper implements Calendar {
return this.deps.originalCalendar.listCalendars(event);
}
testDelegationCredentialSetup?(): Promise<boolean> {
return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve(false);
testDelegationCredentialSetup?(): Promise<void> {
return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve();
}
}
@@ -1,11 +1,10 @@
import type { z } from "zod";
import { checkIfSuccessfullyConfiguredInWorkspace } from "@calcom/app-store/delegationCredential";
import { assertSuccessfullyConfiguredInWorkspace } from "@calcom/app-store/delegationCredential";
import { sendDelegationCredentialDisabledEmail } from "@calcom/emails/integration-email-service";
import { DelegationCredentialRepository } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
import type { ServiceAccountKey } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
import { DelegationCredentialRepository } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { z } from "zod";
import { getAffectedMembersForDisable } from "./getAffectedMembersForDisable.handler";
import type { DelegationCredentialToggleEnabledSchema } from "./schema";
@@ -142,14 +141,10 @@ const assertWorkspaceConfigured = async ({
throw new Error("Domain wide delegation doesn't have service account key");
}
const isSuccessfullyConfigured = await checkIfSuccessfullyConfiguredInWorkspace({
await assertSuccessfullyConfiguredInWorkspace({
delegationCredential,
user,
});
if (!isSuccessfullyConfigured) {
throw new Error("Workspace not successfully configured");
}
};
function hasServiceAccountKey<T extends { serviceAccountKey: ServiceAccountKey | null }>(
+1 -1
View File
@@ -316,7 +316,7 @@ export interface Calendar {
listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]>;
testDelegationCredentialSetup?(): Promise<boolean>;
testDelegationCredentialSetup?(): Promise<void>;
}
/**