refactor: migrate MembershipRole usages to PBAC permission checks (#26871)
* refactor: migrate MembershipRole usages to PBAC permission checks - Refactor get.handler.ts to use PermissionCheckService for canUpdateTeams - Refactor checkForInvalidAppCredentials.ts to use getTeamIdsWithPermission - Refactor outOfOffice.utils.ts to use checkPermission for ooo.update - Refactor checkIfOrgNeedsUpgrade.handler.ts to use organization.manageBilling - Refactor getActiveOnOptions.handler.ts to use eventType.update permission - Refactor WorkflowRepository.ts to use workflow.update permission - Refactor organization.tsx to use team.update permission - Refactor getEventTypesByViewer.ts to use eventType.update permission - Refactor getPublicEvent.ts to use team.read permission for private teams - Update CreateNewOutOfOfficeEntryButton.tsx to use canUpdateOOO prop Co-Authored-By: sean@cal.com <Sean@brydon.io> * fix: use organization.update permission instead of team.update for org management Co-Authored-By: sean@cal.com <Sean@brydon.io> * fix: rename teamsWithEventTypeManagePermission to teamsWithEventTypeUpdatePermission Renamed variable to match the permission string being used (eventType.update) Co-Authored-By: sean@cal.com <Sean@brydon.io> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
sean@cal.com <Sean@brydon.io>
sean@cal.com <Sean@brydon.io>
sean@cal.com <Sean@brydon.io>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
3a8fe8c7af
commit
9ea71d2ba5
@@ -23,7 +23,7 @@ const CreateNewOutOfOfficeEntryButton = ({
|
||||
const me = useMeQuery();
|
||||
const { data: orgData } = trpc.viewer.organizations.listCurrent.useQuery();
|
||||
const isOrgAdminOrOwner = orgData && checkAdminOrOwner(orgData.user.role);
|
||||
const hasTeamOOOAdminAccess = isOrgAdminOrOwner || me?.data?.isTeamAdminOrOwner;
|
||||
const hasTeamOOOAdminAccess = isOrgAdminOrOwner || me?.data?.canUpdateTeams;
|
||||
|
||||
const params = useCompatSearchParams();
|
||||
const selectedTab = params?.get("type") ?? OutOfOfficeTab.MINE;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
export const getServerSideProps = async ({ req }: GetServerSidePropsContext) => {
|
||||
@@ -24,19 +25,15 @@ export const getServerSideProps = async ({ req }: GetServerSidePropsContext) =>
|
||||
} as const;
|
||||
}
|
||||
|
||||
// Check if logged in user has OWNER/ADMIN role in organization
|
||||
const membership = await prisma.membership.findUnique({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId: session?.user.id,
|
||||
teamId: session?.user.profile.organizationId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
const canManageOrganization = await permissionCheckService.checkPermission({
|
||||
userId: session.user.id,
|
||||
teamId: session.user.profile.organizationId,
|
||||
permission: "organization.update",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
});
|
||||
if (!membership?.role || membership?.role === MembershipRole.MEMBER) {
|
||||
|
||||
if (!canManageOrganization) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { deleteScheduledSMSReminder } from "@calcom/ee/workflows/lib/reminders/s
|
||||
import type { WorkflowListType as WorkflowType } from "@calcom/ee/workflows/lib/types";
|
||||
import type { WorkflowStep } from "@calcom/ee/workflows/lib/types";
|
||||
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -242,6 +243,15 @@ export class WorkflowRepository {
|
||||
|
||||
const filtered = filters && hasFilter(filters);
|
||||
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
const teamIdsWithWorkflowUpdatePermission = userId
|
||||
? await permissionCheckService.getTeamIdsWithPermission({
|
||||
userId,
|
||||
permission: "workflow.update",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
})
|
||||
: [];
|
||||
|
||||
const allWorkflows = await prisma.workflow.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
@@ -273,9 +283,9 @@ export class WorkflowRepository {
|
||||
|
||||
if (!filtered) {
|
||||
const workflowsWithReadOnly: WorkflowType[] = allWorkflows.map((workflow) => {
|
||||
const readOnly = !!workflow.team?.members?.find(
|
||||
(member) => member.userId === userId && member.role === MembershipRole.MEMBER
|
||||
);
|
||||
const readOnly = workflow.teamId
|
||||
? !teamIdsWithWorkflowUpdatePermission.includes(workflow.teamId)
|
||||
: false;
|
||||
|
||||
return { readOnly, isOrg: workflow.team?.isOrganization ?? false, ...workflow };
|
||||
});
|
||||
@@ -325,9 +335,9 @@ export class WorkflowRepository {
|
||||
});
|
||||
|
||||
const workflowsWithReadOnly: WorkflowType[] = filteredWorkflows.map((workflow) => {
|
||||
const readOnly = !!workflow.team?.members?.find(
|
||||
(member) => member.userId === userId && member.role === MembershipRole.MEMBER
|
||||
);
|
||||
const readOnly = workflow.teamId
|
||||
? !teamIdsWithWorkflowUpdatePermission.includes(workflow.teamId)
|
||||
: false;
|
||||
|
||||
return { readOnly, isOrg: workflow.team?.isOrganization ?? false, ...workflow };
|
||||
});
|
||||
|
||||
@@ -52,13 +52,19 @@ export const getEventTypesByViewer = async (user: User, filters?: Filters, forRo
|
||||
shouldListUserEvents = true;
|
||||
}
|
||||
|
||||
// Get teams where user has eventType.read permission for PBAC readonly check
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
const teamsWithEventTypeReadPermission = await permissionCheckService.getTeamIdsWithPermission({
|
||||
userId: user.id,
|
||||
permission: "eventType.read",
|
||||
fallbackRoles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
});
|
||||
const [teamsWithEventTypeReadPermission, teamsWithEventTypeUpdatePermission] = await Promise.all([
|
||||
permissionCheckService.getTeamIdsWithPermission({
|
||||
userId: user.id,
|
||||
permission: "eventType.read",
|
||||
fallbackRoles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
}),
|
||||
permissionCheckService.getTeamIdsWithPermission({
|
||||
userId: user.id,
|
||||
permission: "eventType.update",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
}),
|
||||
]);
|
||||
|
||||
const eventTypeRepo = new EventTypeRepository(prisma);
|
||||
const [profileMemberships, profileEventTypes] = await Promise.all([
|
||||
@@ -295,7 +301,7 @@ export const getEventTypesByViewer = async (user: User, filters?: Filters, forRo
|
||||
return res;
|
||||
})
|
||||
.filter((evType) =>
|
||||
membership.role === MembershipRole.MEMBER
|
||||
!teamsWithEventTypeUpdatePermission.includes(team.id)
|
||||
? evType.schedulingType !== SchedulingType.MANAGED
|
||||
: true
|
||||
)
|
||||
|
||||
@@ -7,7 +7,9 @@ import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/
|
||||
import { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync";
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getDefaultEvent, getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
@@ -517,25 +519,27 @@ export const getPublicEvent = async (
|
||||
length: eventWithUserProfiles.length,
|
||||
});
|
||||
}
|
||||
const isTeamAdminOrOwner = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: currentUserId ?? -1,
|
||||
teamId: event.teamId ?? -1,
|
||||
accepted: true,
|
||||
role: { in: ["ADMIN", "OWNER"] },
|
||||
},
|
||||
});
|
||||
let canViewPrivateTeamMembers = false;
|
||||
if (currentUserId && event.teamId) {
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
canViewPrivateTeamMembers = await permissionCheckService.checkPermission({
|
||||
userId: currentUserId,
|
||||
teamId: event.teamId,
|
||||
permission: "team.read",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
});
|
||||
|
||||
const isOrgAdminOrOwner = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: currentUserId ?? -1,
|
||||
teamId: event.team?.parentId ?? -1,
|
||||
accepted: true,
|
||||
role: { in: ["ADMIN", "OWNER"] },
|
||||
},
|
||||
});
|
||||
if (!canViewPrivateTeamMembers && event.team?.parentId) {
|
||||
canViewPrivateTeamMembers = await permissionCheckService.checkPermission({
|
||||
userId: currentUserId,
|
||||
teamId: event.team.parentId,
|
||||
permission: "team.read",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.team?.isPrivate && !isTeamAdminOrOwner && !isOrgAdminOrOwner) {
|
||||
if (event.team?.isPrivate && !canViewPrivateTeamMembers) {
|
||||
users = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
|
||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
|
||||
import { PrismaRoutingFormRepository } from "@calcom/features/routing-forms/repositories/PrismaRoutingFormRepository";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
@@ -55,12 +56,14 @@ const fetchEventTypeGroups = async ({
|
||||
parentOrgHasLockedEventTypes,
|
||||
skipEventTypes,
|
||||
teamId,
|
||||
teamIdsWithEventTypeUpdatePermission,
|
||||
}: {
|
||||
ctx: { user: NonNullable<TrpcSessionUser>; prisma: PrismaClient };
|
||||
profile: NonNullable<Awaited<ReturnType<typeof ProfileRepository.findByUpIdWithAuth>>>;
|
||||
parentOrgHasLockedEventTypes: boolean | undefined;
|
||||
skipEventTypes: boolean;
|
||||
teamId?: number;
|
||||
teamIdsWithEventTypeUpdatePermission: number[];
|
||||
}): Promise<EventTypeGroup[]> => {
|
||||
const user = ctx.user;
|
||||
const userProfile = ctx.user.profile;
|
||||
@@ -132,12 +135,11 @@ const fetchEventTypeGroups = async ({
|
||||
metadata: teamMetadataSchema.parse(membership.team.metadata),
|
||||
};
|
||||
|
||||
const canUpdateEventTypes = teamIdsWithEventTypeUpdatePermission.includes(team.id);
|
||||
const eventTypes = team.eventTypes
|
||||
?.filter((evType) => evType.userId === null || evType.userId === user.id)
|
||||
?.filter((evType) =>
|
||||
membership.role === MembershipRole.MEMBER
|
||||
? evType.schedulingType !== SchedulingType.MANAGED
|
||||
: true
|
||||
!canUpdateEventTypes ? evType.schedulingType !== SchedulingType.MANAGED : true
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -222,12 +224,20 @@ export const getActiveOnOptions = async ({ ctx, input }: GetActiveOnOptions) =>
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||||
}
|
||||
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
const teamIdsWithEventTypeUpdatePermission = await permissionCheckService.getTeamIdsWithPermission({
|
||||
userId: user.id,
|
||||
permission: "eventType.update",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
});
|
||||
|
||||
const eventTypeGroups = await fetchEventTypeGroups({
|
||||
ctx,
|
||||
profile,
|
||||
parentOrgHasLockedEventTypes,
|
||||
skipEventTypes: shouldSkipEventTypes,
|
||||
teamId,
|
||||
teamIdsWithEventTypeUpdatePermission,
|
||||
});
|
||||
|
||||
const teamOptions = await fetchTeamOptions({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { type InvalidAppCredentialBannerProps } from "@calcom/features/users/types/invalidAppCredentials";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
@@ -13,21 +14,16 @@ type checkInvalidAppCredentialsOptions = {
|
||||
export const checkInvalidAppCredentials = async ({ ctx }: checkInvalidAppCredentialsOptions) => {
|
||||
const userId = ctx.user.id;
|
||||
|
||||
// First get the teams where user is admin/owner
|
||||
const userTeamIds = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] },
|
||||
},
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
const userTeamIds = await permissionCheckService.getTeamIdsWithPermission({
|
||||
userId,
|
||||
permission: "team.update",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
});
|
||||
|
||||
const apps = await prisma.credential.findMany({
|
||||
where: {
|
||||
OR: [{ userId }, { teamId: { in: userTeamIds.map((membership) => membership.teamId) } }],
|
||||
OR: [{ userId }, { teamId: { in: userTeamIds } }],
|
||||
invalid: true,
|
||||
},
|
||||
select: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
@@ -94,17 +95,13 @@ export const getHandler = async ({ ctx, input }: MeOptions) => {
|
||||
organizationSettings: user?.profile?.organization?.organizationSettings,
|
||||
};
|
||||
|
||||
const isTeamAdminOrOwner =
|
||||
(await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
accepted: true,
|
||||
role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})) !== null;
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
const teamsWithWritePermission = await permissionCheckService.getTeamIdsWithPermission({
|
||||
userId: user.id,
|
||||
permission: "team.update",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
});
|
||||
const canUpdateTeams = teamsWithWritePermission.length > 0;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
@@ -145,6 +142,6 @@ export const getHandler = async ({ ctx, input }: MeOptions) => {
|
||||
secondaryEmails,
|
||||
isPremium: userMetadataPrased?.isPremium,
|
||||
...(passwordAdded ? { passwordAdded } : {}),
|
||||
isTeamAdminOrOwner,
|
||||
canUpdateTeams,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
export const isAdminForUser = async (adminUserId: number, memberUserId: number) => {
|
||||
const adminTeams = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: adminUserId,
|
||||
accepted: true,
|
||||
role: {
|
||||
in: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
},
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
const adminTeamIds = await permissionCheckService.getTeamIdsWithPermission({
|
||||
userId: adminUserId,
|
||||
permission: "ooo.update",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
});
|
||||
|
||||
const adminTeamIds = adminTeams?.map((team) => team.teamId);
|
||||
if (adminTeamIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const member = await prisma.membership.findFirst({
|
||||
where: {
|
||||
|
||||
+13
-6
@@ -1,3 +1,4 @@
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
@@ -14,16 +15,22 @@ type GetUpgradeableOptions = {
|
||||
export async function checkIfOrgNeedsUpgradeHandler({ ctx }: GetUpgradeableOptions) {
|
||||
if (!IS_TEAM_BILLING_ENABLED) return [];
|
||||
|
||||
// Get all teams/orgs where the user is an owner
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
const teamIdsWithBillingPermission = await permissionCheckService.getTeamIdsWithPermission({
|
||||
userId: ctx.user.id,
|
||||
permission: "organization.manageBilling",
|
||||
fallbackRoles: [MembershipRole.OWNER],
|
||||
});
|
||||
|
||||
if (teamIdsWithBillingPermission.length === 0) return [];
|
||||
|
||||
let teams = await prisma.membership.findMany({
|
||||
where: {
|
||||
user: {
|
||||
id: ctx.user.id,
|
||||
},
|
||||
role: MembershipRole.OWNER,
|
||||
userId: ctx.user.id,
|
||||
teamId: { in: teamIdsWithBillingPermission },
|
||||
team: {
|
||||
isPlatform: false,
|
||||
parentId: null, // Since ORGS relay on their parent's subscription, we don't need to return them
|
||||
parentId: null,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
|
||||
Reference in New Issue
Block a user