Skip to content

Commit a7aabc8

Browse files
CarinaWolliCarinaWollikeithwillcode
authored
fix: toggleEnabled handler (calcom#25325)
* fix toggle enabled handler * add tests --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
1 parent c941192 commit a7aabc8

2 files changed

Lines changed: 161 additions & 0 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { describe, it, beforeEach, vi, expect } from "vitest";
2+
3+
import { DelegationCredentialRepository } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
4+
5+
import { toggleDelegationCredentialEnabled } from "./toggleEnabled.handler";
6+
7+
// Mock the repository
8+
vi.mock("@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository", () => ({
9+
DelegationCredentialRepository: {
10+
findById: vi.fn(),
11+
updateById: vi.fn(),
12+
findByIdIncludeSensitiveServiceAccountKey: vi.fn(),
13+
},
14+
}));
15+
16+
// Mock other dependencies
17+
vi.mock("@calcom/app-store/delegationCredential", () => ({
18+
checkIfSuccessfullyConfiguredInWorkspace: vi.fn().mockResolvedValue(true),
19+
}));
20+
21+
vi.mock("@calcom/emails/integration-email-service", () => ({
22+
sendDelegationCredentialDisabledEmail: vi.fn(),
23+
}));
24+
25+
vi.mock("./getAffectedMembersForDisable.handler", () => ({
26+
getAffectedMembersForDisable: vi.fn().mockResolvedValue([]),
27+
}));
28+
29+
vi.mock("./utils", () => ({
30+
ensureNoServiceAccountKey: vi.fn((credential) => credential),
31+
}));
32+
33+
describe("toggleDelegationCredentialEnabled - Security Fix", () => {
34+
beforeEach(() => {
35+
vi.clearAllMocks();
36+
});
37+
38+
it("should prevent users without organizationId from accessing any credentials", async () => {
39+
const userWithoutOrg = {
40+
id: 1,
41+
email: "user@example.com",
42+
organizationId: null,
43+
};
44+
45+
const input = {
46+
id: "any-credential",
47+
enabled: true,
48+
};
49+
50+
const mockCredential = {
51+
id: "any-credential",
52+
organizationId: 1,
53+
enabled: false,
54+
workspacePlatform: { slug: "google" },
55+
};
56+
57+
vi.mocked(DelegationCredentialRepository.findById).mockResolvedValue(mockCredential);
58+
59+
await expect(
60+
toggleDelegationCredentialEnabled(userWithoutOrg, input)
61+
).rejects.toThrow("You must be part of an organization to toggle a delegation credential");
62+
63+
expect(DelegationCredentialRepository.updateById).not.toHaveBeenCalled();
64+
});
65+
66+
it("should prevent cross-organization access", async () => {
67+
const userFromOrg1 = {
68+
id: 1,
69+
email: "user@org1.com",
70+
organizationId: 1,
71+
};
72+
73+
const input = {
74+
id: "org2-credential",
75+
enabled: false,
76+
};
77+
78+
const org2Credential = {
79+
id: "org2-credential",
80+
organizationId: 2, // Different organization
81+
enabled: true,
82+
workspacePlatform: { slug: "google" },
83+
};
84+
85+
vi.mocked(DelegationCredentialRepository.findById).mockResolvedValue(org2Credential);
86+
87+
await expect(
88+
toggleDelegationCredentialEnabled(userFromOrg1, input)
89+
).rejects.toThrow("Delegation credential not found");
90+
91+
expect(DelegationCredentialRepository.updateById).not.toHaveBeenCalled();
92+
});
93+
94+
it("should allow same-organization access", async () => {
95+
const userFromOrg1 = {
96+
id: 1,
97+
email: "admin@org1.com",
98+
organizationId: 1,
99+
};
100+
101+
const input = {
102+
id: "org1-credential",
103+
enabled: false,
104+
};
105+
106+
const org1Credential = {
107+
id: "org1-credential",
108+
organizationId: 1, // Same organization
109+
enabled: true,
110+
workspacePlatform: { slug: "google" },
111+
};
112+
113+
const updatedCredential = {
114+
...org1Credential,
115+
enabled: false,
116+
lastDisabledAt: new Date(),
117+
};
118+
119+
vi.mocked(DelegationCredentialRepository.findById).mockResolvedValue(org1Credential);
120+
vi.mocked(DelegationCredentialRepository.updateById).mockResolvedValue(updatedCredential);
121+
122+
const result = await toggleDelegationCredentialEnabled(userFromOrg1, input);
123+
124+
expect(result).toEqual(updatedCredential);
125+
expect(DelegationCredentialRepository.updateById).toHaveBeenCalledWith({
126+
id: "org1-credential",
127+
data: {
128+
enabled: false,
129+
lastEnabledAt: undefined,
130+
lastDisabledAt: expect.any(Date),
131+
},
132+
});
133+
});
134+
135+
it("should handle nonexistent credentials", async () => {
136+
const user = {
137+
id: 1,
138+
email: "user@org1.com",
139+
organizationId: 1,
140+
};
141+
142+
const input = {
143+
id: "nonexistent-credential",
144+
enabled: true,
145+
};
146+
147+
vi.mocked(DelegationCredentialRepository.findById).mockResolvedValue(null);
148+
149+
await expect(
150+
toggleDelegationCredentialEnabled(user, input)
151+
).rejects.toThrow("Delegation credential not found");
152+
});
153+
});

packages/trpc/server/routers/viewer/delegationCredential/toggleEnabled.handler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ export async function toggleDelegationCredentialEnabled(
5353
throw new Error("Delegation credential not found");
5454
}
5555

56+
if (!loggedInUser.organizationId) {
57+
throw new Error("You must be part of an organization to toggle a delegation credential");
58+
}
59+
60+
if (currentDelegationCredential.organizationId !== loggedInUser.organizationId) {
61+
throw new Error("Delegation credential not found");
62+
}
63+
5664
const shouldBeEnabled = input.enabled;
5765

5866
if (shouldBeEnabled === currentDelegationCredential.enabled) {

0 commit comments

Comments
 (0)