Skip to content

Commit df75df7

Browse files
fix: DelegationCredential - Duplicate GoogleMeet and unnecessary serviceAccount key access (calcom#21269)
* Remove unused return statement * Fix return from fn instead of continuing the loop * Ensure userId is set for existing records on update * ensure that serviceAccountKey is only retrieved whenb assked
1 parent deb55c0 commit df75df7

32 files changed

Lines changed: 196 additions & 113 deletions

packages/app-store/server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ export async function getLocationGroupedOptions(
125125
: {}),
126126
};
127127
if (apps[groupByCategory]) {
128-
apps[groupByCategory] = [...apps[groupByCategory], option];
128+
const existingOption = apps[groupByCategory].find((o) => o.value === option.value);
129+
if (!existingOption) {
130+
apps[groupByCategory] = [...apps[groupByCategory], option];
131+
}
129132
} else {
130133
apps[groupByCategory] = [option];
131134
}

packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ import { UserRepository } from "@calcom/lib/server/repository/user";
1515
// }
1616
// })
1717

18-
describe("getAllCredentials", () => {
18+
describe("getAllCredentialsIncludeServiceAccountKey", () => {
1919
test("Get an individual's credentials", async () => {
2020
vi.spyOn(UserRepository, "enrichUserWithItsProfile").mockReturnValue({
2121
profile: null,
2222
});
2323

24-
const getAllCredentials = (await import("./getAllCredentials")).getAllCredentials;
24+
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
25+
.getAllCredentialsIncludeServiceAccountKey;
2526

2627
const userCredential = {
2728
id: 1,
@@ -38,7 +39,7 @@ describe("getAllCredentials", () => {
3839
{ type: "team-credential", teamId: 1, key: {} },
3940
]);
4041

41-
const credentials = await getAllCredentials(
42+
const credentials = await getAllCredentialsIncludeServiceAccountKey(
4243
{
4344
id: 1,
4445
username: "test",
@@ -65,7 +66,8 @@ describe("getAllCredentials", () => {
6566
profile: null,
6667
});
6768

68-
const getAllCredentials = (await import("./getAllCredentials")).getAllCredentials;
69+
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
70+
.getAllCredentialsIncludeServiceAccountKey;
6971

7072
const crmCredential = {
7173
id: 1,
@@ -101,7 +103,7 @@ describe("getAllCredentials", () => {
101103
},
102104
]);
103105

104-
const credentials = await getAllCredentials(
106+
const credentials = await getAllCredentialsIncludeServiceAccountKey(
105107
{
106108
id: 1,
107109
username: "test",
@@ -138,7 +140,8 @@ describe("getAllCredentials", () => {
138140
profile: null,
139141
});
140142

141-
const getAllCredentials = (await import("./getAllCredentials")).getAllCredentials;
143+
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
144+
.getAllCredentialsIncludeServiceAccountKey;
142145

143146
const crmCredential = {
144147
id: 1,
@@ -170,7 +173,7 @@ describe("getAllCredentials", () => {
170173
},
171174
]);
172175

173-
const credentials = await getAllCredentials(
176+
const credentials = await getAllCredentialsIncludeServiceAccountKey(
174177
{
175178
id: 1,
176179
username: "test",
@@ -204,7 +207,8 @@ describe("getAllCredentials", () => {
204207
profile: null,
205208
});
206209

207-
const getAllCredentials = (await import("./getAllCredentials")).getAllCredentials;
210+
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
211+
.getAllCredentialsIncludeServiceAccountKey;
208212

209213
const teamId = 1;
210214
const crmCredential = {
@@ -261,7 +265,7 @@ describe("getAllCredentials", () => {
261265

262266
console.log(testEventType);
263267

264-
const credentials = await getAllCredentials(
268+
const credentials = await getAllCredentialsIncludeServiceAccountKey(
265269
{
266270
id: 1,
267271
username: "test",
@@ -291,7 +295,8 @@ describe("getAllCredentials", () => {
291295
expect(credentials).toContainEqual(expect.objectContaining({ teamId, type: "salesforce_crm" }));
292296
});
293297
test("For an org user", async () => {
294-
const getAllCredentials = (await import("./getAllCredentials")).getAllCredentials;
298+
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
299+
.getAllCredentialsIncludeServiceAccountKey;
295300
const orgId = 3;
296301
vi.spyOn(UserRepository, "enrichUserWithItsProfile").mockReturnValue({
297302
profile: { organizationId: orgId },
@@ -357,7 +362,7 @@ describe("getAllCredentials", () => {
357362
},
358363
]);
359364

360-
const credentials = await getAllCredentials(
365+
const credentials = await getAllCredentialsIncludeServiceAccountKey(
361366
{
362367
id: 1,
363368
username: "test",
@@ -394,7 +399,8 @@ describe("getAllCredentials", () => {
394399
});
395400
describe("Default with _other_calendar credentials", () => {
396401
test("For users", async () => {
397-
const getAllCredentials = (await import("./getAllCredentials")).getAllCredentials;
402+
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
403+
.getAllCredentialsIncludeServiceAccountKey;
398404

399405
const crmCredential = {
400406
id: 1,
@@ -430,7 +436,7 @@ describe("getAllCredentials", () => {
430436
},
431437
]);
432438

433-
const credentials = await getAllCredentials(
439+
const credentials = await getAllCredentialsIncludeServiceAccountKey(
434440
{
435441
id: 1,
436442
username: "test",
@@ -459,7 +465,8 @@ describe("getAllCredentials", () => {
459465
);
460466
});
461467
test("For teams", async () => {
462-
const getAllCredentials = (await import("./getAllCredentials")).getAllCredentials;
468+
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
469+
.getAllCredentialsIncludeServiceAccountKey;
463470

464471
const crmCredential = {
465472
id: 1,
@@ -491,7 +498,7 @@ describe("getAllCredentials", () => {
491498
},
492499
]);
493500

494-
const credentials = await getAllCredentials(
501+
const credentials = await getAllCredentialsIncludeServiceAccountKey(
495502
{
496503
id: 1,
497504
username: "test",
@@ -517,7 +524,8 @@ describe("getAllCredentials", () => {
517524
);
518525
});
519526
test("For child of managed event type", async () => {
520-
const getAllCredentials = (await import("./getAllCredentials")).getAllCredentials;
527+
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
528+
.getAllCredentialsIncludeServiceAccountKey;
521529

522530
const teamId = 1;
523531
const crmCredential = {
@@ -574,7 +582,7 @@ describe("getAllCredentials", () => {
574582

575583
console.log(testEventType);
576584

577-
const credentials = await getAllCredentials(
585+
const credentials = await getAllCredentialsIncludeServiceAccountKey(
578586
{
579587
id: 1,
580588
username: "test",
@@ -600,7 +608,8 @@ describe("getAllCredentials", () => {
600608
);
601609
});
602610
test("For an org user", async () => {
603-
const getAllCredentials = (await import("./getAllCredentials")).getAllCredentials;
611+
const getAllCredentialsIncludeServiceAccountKey = (await import("./getAllCredentials"))
612+
.getAllCredentialsIncludeServiceAccountKey;
604613
const orgId = 3;
605614
vi.spyOn(UserRepository, "enrichUserWithItsProfile").mockReturnValue({
606615
profile: { organizationId: orgId },
@@ -666,7 +675,7 @@ describe("getAllCredentials", () => {
666675
},
667676
]);
668677

669-
const credentials = await getAllCredentials(
678+
const credentials = await getAllCredentialsIncludeServiceAccountKey(
670679
{
671680
id: 1,
672681
username: "test",

packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type z from "zod";
22

3-
import { enrichUserWithDelegationCredentialsWithoutOrgId } from "@calcom/lib/delegationCredential/server";
3+
import { enrichUserWithDelegationCredentialsIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server";
44
import { UserRepository } from "@calcom/lib/server/repository/user";
55
import prisma from "@calcom/prisma";
66
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
@@ -19,7 +19,7 @@ export type EventType = {
1919
* Gets credentials from the user, team, and org if applicable
2020
*
2121
*/
22-
export const getAllCredentials = async (
22+
export const getAllCredentialsIncludeServiceAccountKey = async (
2323
user: { id: number; username: string | null; email: string; credentials: CredentialPayload[] },
2424
eventType: EventType
2525
) => {
@@ -120,7 +120,7 @@ export const getAllCredentials = async (
120120
}
121121
});
122122

123-
const userWithDelegationCredentials = await enrichUserWithDelegationCredentialsWithoutOrgId({
123+
const userWithDelegationCredentials = await enrichUserWithDelegationCredentialsIncludeServiceAccountKey({
124124
user: { ...user, credentials: allCredentials },
125125
});
126126

packages/features/bookings/lib/handleCancelBooking.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
3737
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
3838
import type { CalendarEvent } from "@calcom/types/Calendar";
3939

40-
import { getAllCredentials } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
40+
import { getAllCredentialsIncludeServiceAccountKey } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
4141
import { getBookingToDelete } from "./getBookingToDelete";
4242
import { handleInternalNote } from "./handleInternalNote";
4343
import cancelAttendeeSeat from "./handleSeats/cancel/cancelAttendeeSeat";
@@ -467,7 +467,7 @@ async function handler(input: CancelBookingInput) {
467467

468468
const bookingToDeleteEventTypeMetadata = bookingToDeleteEventTypeMetadataParsed.data;
469469

470-
const credentials = await getAllCredentials(bookingToDelete.user, {
470+
const credentials = await getAllCredentialsIncludeServiceAccountKey(bookingToDelete.user, {
471471
...bookingToDelete.eventType,
472472
metadata: bookingToDeleteEventTypeMetadata,
473473
});

packages/features/bookings/lib/handleNewBooking.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ import type { CredentialForCalendarService } from "@calcom/types/Credential";
8181
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
8282

8383
import type { EventPayloadType, EventTypeInfo } from "../../webhooks/lib/sendPayload";
84-
import { getAllCredentials } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
84+
import { getAllCredentialsIncludeServiceAccountKey } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
8585
import { refreshCredentials } from "./getAllCredentialsForUsersOnEvent/refreshCredentials";
8686
import getBookingDataSchema from "./getBookingDataSchema";
8787
import { addVideoCallDataToEvent } from "./handleNewBooking/addVideoCallDataToEvent";
@@ -820,7 +820,7 @@ async function handler(
820820
: users[0];
821821

822822
const tOrganizer = await getTranslation(organizerUser?.locale ?? "en", "common");
823-
const allCredentials = await getAllCredentials(organizerUser, eventType);
823+
const allCredentials = await getAllCredentialsIncludeServiceAccountKey(organizerUser, eventType);
824824

825825
// If the Organizer himself is rescheduling, the booker should be sent the communication in his timezone and locale.
826826
const attendeeInfoOnReschedule =

packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
22
import { sendCancelledSeatEmailsAndSMS } from "@calcom/emails";
33
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
44
import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
5-
import { getAllDelegationCredentialsForUser } from "@calcom/lib/delegationCredential/server";
5+
import { getAllDelegationCredentialsForUserIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server";
66
import { getDelegationCredentialOrFindRegularCredential } from "@calcom/lib/delegationCredential/server";
77
import { HttpError } from "@calcom/lib/http-error";
88
import logger from "@calcom/lib/logger";
@@ -71,7 +71,8 @@ async function cancelAttendeeSeat(
7171
const attendee = bookingToDelete?.attendees.find((attendee) => attendee.id === seatReference.attendeeId);
7272
const bookingToDeleteUser = bookingToDelete.user ?? null;
7373
const delegationCredentials = bookingToDeleteUser
74-
? await getAllDelegationCredentialsForUser({
74+
? // We fetch delegation credentials with ServiceAccount key as CalendarService instance created later in the flow needs it
75+
await getAllDelegationCredentialsForUserIncludeServiceAccountKey({
7576
user: { email: bookingToDeleteUser.email, id: bookingToDeleteUser.id },
7677
})
7778
: [];

packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Attendee } from "@prisma/client";
22

33
// eslint-disable-next-line no-restricted-imports
44
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
5-
import { getAllDelegationCredentialsForUser } from "@calcom/lib/delegationCredential/server";
5+
import { getAllDelegationCredentialsForUserIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server";
66
import { getDelegationCredentialOrFindRegularCredential } from "@calcom/lib/delegationCredential/server";
77
import { deleteMeeting } from "@calcom/lib/videoClient";
88
import prisma from "@calcom/prisma";
@@ -21,7 +21,8 @@ const lastAttendeeDeleteBooking = async (
2121
let deletedReferences = false;
2222
const bookingUser = originalRescheduledBooking?.user;
2323
const delegationCredentials = bookingUser
24-
? await getAllDelegationCredentialsForUser({
24+
? // We fetch delegation credentials with ServiceAccount key as CalendarService instance created later in the flow needs it
25+
await getAllDelegationCredentialsForUserIncludeServiceAccountKey({
2526
user: { email: bookingUser.email, id: bookingUser.id },
2627
})
2728
: [];

packages/features/bookings/lib/handleSeats/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type NewSeatedBookingObject = {
1818
bookerUrl: string;
1919
};
2020
invitee: Invitee;
21-
allCredentials: Awaited<ReturnType<typeof getAllCredentials>>;
21+
allCredentials: Awaited<ReturnType<typeof getAllCredentialsIncludeServiceAccountKey>>;
2222
organizerUser: OrganizerUser;
2323
originalRescheduledBooking: OriginalRescheduledBooking;
2424
bookerEmail: string;

apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/delegation-credential/delegation-credential.md renamed to packages/features/delegation-credentials/README.md

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,7 @@ Step 6: Enable Delegation Credential(To Be taken By Cal.com organization Owner/A
9595
### Terminology
9696

9797
- Delegation Credential: A Delegation Credential service account key along with user's email becomes the Delegation Credential which is an alternative to regular Credential in DB.
98-
- DWD: Domain Wide Delegation
99-
- non-dwd credential: Regular credentials that are stored in Credentials table
98+
- Delegation User Credential: A Delegation User Credential is a Credential record in DB that uses DelegationCredential record to actually access the user's calendar. A Credential record with delegationCredentialId set is a Delegation User Credential.
10099

101100
### How Delegation Credential works
102101

@@ -108,6 +107,13 @@ Step 6: Enable Delegation Credential(To Be taken By Cal.com organization Owner/A
108107
- A Delegation Credential service account key along with user's email becomes the Delegation Credential which is an alternative to regular Credential in DB.
109108
- Delegation Credential doesn't completely replace the regular credentials. Delegation Credential gives access to the cal.com user's email in Google Calendar. So, if the user needs to connect to some other email's calendar, we need to use the regular credentials.
110109

110+
### Cron Jobs
111+
112+
Cron jobs ensure that for each and every member of the organization that has Delegation Credential enabled, corresponding SelectedCalendar records are there. These crons currently run every 5 minutes, look at vercel.json for the up-to-date schedule.
113+
114+
- `credentials` cron job creates Delegation User Credential records for all the members of the organization who don't have Delegation User Credentials yet. It also ensures that on disabling Delegation Credential, the Delegation User Credentials are deleted which automatically deletes the SelectedCalendars through DB cascade.
115+
- `selected-calendars` cron job creates SelectedCalendar records for all the Delegation User Credentials of the organization who don't have Selected Calendars yet.
116+
111117
### Important Points
112118

113119
- No Credential table entry is created when enabling Delegation Credential. The workspace platform's related apps will be considered as "installed" for the users with email matching dwd domain. An in-memory credential like object is created for this purpose. It allows avoiding creation of thousands of records for all the members of the organization when Delegation Credential is enabled.
@@ -122,27 +128,26 @@ Step 6: Enable Delegation Credential(To Be taken By Cal.com organization Owner/A
122128
1. Identify the logged in user's email
123129
2. Identify the domainWideDelegations for that email's domain
124130
3. Build in-memory credentials for the domainWideDelegations and use them along with the actual credentials(that user might have connected) of the user
125-
4. We don't show the non-dwd connected calendar(if there is a corresponding dwd connected calendar). Though we use the non-dwd credentials to identify the selected calendars, for the dwd connected calendar.
131+
4. We don't show the non DelegationCredential connected calendar(if there is a corresponding DelegationCredential connected calendar). Though we use the non DelegationCredential credentials to identify the selected calendars, for the DelegationCredential connected calendar.
126132

127133
### Impact of disabling Delegation Credential
128134

129-
Disabling effectively stops generating in-memory delegation user credentials. So, any members who haven't manually connected their Calendar and thus their calendar connections were working only because of Delegation Credential, would have their connections broken.
130-
131-
#### What would not work correctly ?
132-
133-
- Calendar won't be checked for conflicts. So, they could get booked at a time when they are marked busy in their calendar.
134-
- Cal.com bookings would still be checked for conflicts.
135-
- Bookings might not appear in the attendee and host's Google Calendar. Because we would be unable to use the API to create the events in calendar and Google doesn't always add events to calendar automatically based on .ics file alone.
136-
- Cal Video would be used as the booking location instead of Google Meet.
135+
Disabling effectively stops generating in-memory delegation user credentials. So, any members who haven't manually connected their Calendar and thus their calendar connections were working only because of Delegation Credential, would have their calendar connections broken.
137136

138-
#### What would work correctly ?
139-
140-
- Bookings would still go through. People relying on Salesforce for booking details, would face no issues.
141-
- Cal.com bookings would still be checked for conflicts.
137+
### Impact of enabling Delegation Credential
138+
- Existing calendar-cache records are re-used as we identify the relevant record by userId and key of CalendarCache record.
139+
- Any updates to those calendar-cache records keep on working by using the non-delegation credential attached with the SelectedCalendar record.
140+
- For any new members, we create Credential records and SelectedCalendar records through cron jobs and thus their calendar-cache records will also be created.
142141

143142
### Notes when testing locally
144143

145144
- You need to enable the feature through feature flag.
146145
- You could use Acme org and login as <owner1-acme@example.com>
147146
- Make sure to change the email of the user above to your workspace owner's email(other member's email might also work). This is necessary otherwise you won't be able to enable Delegation Credential for the organization.
148147
- Note: After changing the email, you would have to logout and login again
148+
149+
150+
151+
## TODO
152+
- Test what happens when credential expires that was used in CalendarCache/SelectedCalendar
153+
- It seems that if refresh token is valid then it would still be refreshed but if it becomes invalid then it ends up causing the calendar-cache updates to break because it isn't able to renew the access token. How do you fix it?

0 commit comments

Comments
 (0)