Files
calendar/packages/lib/server/repository/delegationCredential.ts
T
4fcf4081d0 feat: Send emails to potentially affected Organization Members on disabling delegation credential (#21276)
* mvp

* Improve UI

* wip

* self-review

* self-review

---------

Co-authored-by: Omar López <zomars@me.com>
2025-05-22 03:28:21 +00:00

335 lines
9.6 KiB
TypeScript

import type { Prisma } from "@prisma/client";
import logger from "@calcom/lib/logger";
import {
serviceAccountKeySchema,
type ServiceAccountKey,
type EncryptedServiceAccountKey,
encryptServiceAccountKey,
decryptServiceAccountKey,
} from "@calcom/lib/server/serviceAccountKey";
import { prisma } from "@calcom/prisma";
import { OrganizationRepository } from "./organization";
export type { ServiceAccountKey };
const repositoryLogger = logger.getSubLogger({ prefix: ["DelegationCredentialRepository"] });
const delegationCredentialSafeSelect = {
id: true,
enabled: true,
domain: true,
createdAt: true,
updatedAt: true,
organizationId: true,
lastEnabledAt: true,
lastDisabledAt: true,
workspacePlatform: {
select: {
name: true,
slug: true,
},
},
};
const delegationCredentialSelectIncludesServiceAccountKey = {
...delegationCredentialSafeSelect,
serviceAccountKey: true,
};
function doesEmailMatchDelegationCredentialDomain({
memberEmail,
delegationCredentialEmailDomain,
}: {
memberEmail: string | null;
delegationCredentialEmailDomain: string;
}) {
if (!memberEmail) return false;
const memberEmailDomain = memberEmail.split("@")[1];
return memberEmailDomain === delegationCredentialEmailDomain;
}
export class DelegationCredentialRepository {
private static encryptServiceAccountKey(serviceAccountKey: ServiceAccountKey): EncryptedServiceAccountKey {
return encryptServiceAccountKey(serviceAccountKey);
}
private static decryptServiceAccountKey(encryptedServiceAccountKey: Prisma.JsonValue): ServiceAccountKey {
return decryptServiceAccountKey(encryptedServiceAccountKey);
}
private static withParsedServiceAccountKey<T extends { serviceAccountKey: Prisma.JsonValue } | null>(
delegationCredential: T
) {
if (!delegationCredential) {
return null;
}
const { serviceAccountKey, ...rest } = delegationCredential;
// Decrypt the service account key if it exists
const decryptedKey = this.decryptServiceAccountKey(serviceAccountKey);
const parsedServiceAccountKey = serviceAccountKeySchema.safeParse(decryptedKey);
return {
...rest,
serviceAccountKey: parsedServiceAccountKey.success ? parsedServiceAccountKey.data : null,
};
}
static async create(data: {
domain: string;
enabled: boolean;
organizationId: number;
workspacePlatformId: number;
serviceAccountKey: ServiceAccountKey;
}) {
const encryptedKey = this.encryptServiceAccountKey(data.serviceAccountKey);
return await prisma.delegationCredential.create({
data: {
workspacePlatform: {
connect: {
id: data.workspacePlatformId,
},
},
domain: data.domain,
enabled: data.enabled,
organization: {
connect: {
id: data.organizationId,
},
},
serviceAccountKey: encryptedKey,
},
select: delegationCredentialSafeSelect,
});
}
static async findById({ id }: { id: string }) {
return await prisma.delegationCredential.findUnique({
where: { id },
select: delegationCredentialSafeSelect,
});
}
static async findUniqueByOrganizationIdAndDomainIncludeSensitiveServiceAccountKey({
organizationId,
domain,
}: {
organizationId: number;
domain: string;
}) {
const delegationCredential = await prisma.delegationCredential.findUnique({
where: { organizationId_domain: { organizationId, domain } },
select: delegationCredentialSelectIncludesServiceAccountKey,
});
return DelegationCredentialRepository.withParsedServiceAccountKey(delegationCredential);
}
static async findByIdIncludeSensitiveServiceAccountKey({ id }: { id: string }) {
const delegationCredential = await prisma.delegationCredential.findUnique({
where: { id },
select: delegationCredentialSelectIncludesServiceAccountKey,
});
if (!delegationCredential) return null;
return DelegationCredentialRepository.withParsedServiceAccountKey(delegationCredential);
}
static async findUniqueByOrgMemberEmailIncludeSensitiveServiceAccountKey({ email }: { email: string }) {
const log = repositoryLogger.getSubLogger({
prefix: ["findUniqueByOrgMemberEmailIncludeSensitiveServiceAccountKey"],
});
log.debug("called with", { email });
const organization = await OrganizationRepository.findByMemberEmail({ email });
if (!organization) {
log.debug("Email not found in any organization:", email);
return null;
}
const emailDomain = email.split("@")[1];
const delegationCredential = await prisma.delegationCredential.findUnique({
where: {
organizationId_domain: {
organizationId: organization.id,
domain: emailDomain,
},
},
select: delegationCredentialSelectIncludesServiceAccountKey,
});
return DelegationCredentialRepository.withParsedServiceAccountKey(delegationCredential);
}
static async findAllByDomain({ domain }: { domain: string }) {
return await prisma.delegationCredential.findMany({
where: { domain },
select: delegationCredentialSafeSelect,
});
}
static async updateById({
id,
data,
}: {
id: string;
data: Partial<{
workspacePlatformId: number;
domain: string;
enabled: boolean;
organizationId: number;
lastEnabledAt: Date;
lastDisabledAt: Date;
}>;
}) {
const { workspacePlatformId, organizationId, ...rest } = data;
return await prisma.delegationCredential.update({
where: { id },
data: {
...(workspacePlatformId && {
workspacePlatform: {
connect: {
id: workspacePlatformId,
},
},
}),
...(organizationId && {
organization: {
connect: {
id: organizationId,
},
},
}),
...rest,
},
select: delegationCredentialSafeSelect,
});
}
static async deleteById({ id }: { id: string }) {
return await prisma.delegationCredential.delete({
where: { id },
});
}
static async findByOrgIdIncludeSensitiveServiceAccountKey({ organizationId }: { organizationId: number }) {
return await prisma.delegationCredential.findMany({
where: { organizationId },
select: {
...delegationCredentialSelectIncludesServiceAccountKey,
workspacePlatform: {
select: {
id: true,
name: true,
slug: true,
},
},
},
});
}
static async findAllEnabledIncludeDelegatedMembers() {
const delegationCredentials = await prisma.delegationCredential.findMany({
where: {
enabled: true,
},
include: {
workspacePlatform: {
select: {
slug: true,
},
},
organization: {
include: {
members: {
select: {
id: true,
userId: true,
user: {
select: {
id: true,
email: true,
},
},
accepted: true,
},
where: {
// Note: We don't maintain anything on Membership that can identify if a Membership has been processed, so we send all and let it be on the caller to filter out
accepted: true,
},
},
},
},
},
});
return delegationCredentials.map((delegationCredential) => {
// Members for whom delegation credential is applicable
const delegatedMembers = delegationCredential.organization.members.filter((member) => {
return doesEmailMatchDelegationCredentialDomain({
memberEmail: member.user.email,
delegationCredentialEmailDomain: delegationCredential.domain,
});
});
return {
...delegationCredential,
organization: {
...delegationCredential.organization,
delegatedMembers,
},
};
});
}
static async findAllDisabledAndIncludeNextBatchOfMembersToProcess() {
const delegationCredentials = await prisma.delegationCredential.findMany({
where: {
enabled: false,
},
include: {
workspacePlatform: {
select: {
slug: true,
},
},
organization: {
include: {
members: {
select: {
id: true,
userId: true,
user: {
select: {
id: true,
email: true,
},
},
accepted: true,
},
where: {
// Note: We don't maintain anything on Membership that can identify if a Membership has been processed, so we send all and let it be on the caller to filter out
accepted: true,
},
},
},
},
},
});
return delegationCredentials.map((delegationCredential) => {
const members = delegationCredential.organization.members;
const membersThatAreActuallyPartOfDelegationCredential = members.filter((member) => {
return doesEmailMatchDelegationCredentialDomain({
memberEmail: member.user.email,
delegationCredentialEmailDomain: delegationCredential.domain,
});
});
return {
...delegationCredential,
organization: {
...delegationCredential.organization,
members: membersThatAreActuallyPartOfDelegationCredential,
},
};
});
}
}