feat: pbac - private teams (#23998)

* Add permission + migration for listMembersPrivate + depends on lsitMember

* Add legacy list members PBAC private logic

* Add checks in team pages + handler

* add list memeber checks for private orgs

* Add permision visibility scope to loading permissions

* getMembers handler

* fix fallback permission

* Verify private teams work with private org

* Apply suggestion from @eunjae-lee

Co-authored-by: Eunjae Lee <hey@eunjae.dev>

* Remove and private scope function and add param to old

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
This commit is contained in:
sean-brydon
2025-09-29 11:30:54 +01:00
committed by GitHub
co-authored by Eunjae Lee
parent b17eab3aeb
commit 239e6085ba
16 changed files with 294 additions and 63 deletions
@@ -22,6 +22,7 @@ interface AdvancedPermissionGroupProps {
onChange: (permissions: string[]) => void;
disabled?: boolean;
scope?: Scope;
isPrivate?: boolean;
}
const INTERNAL_DATAACCESS_KEY = "_resource";
@@ -32,10 +33,11 @@ export function AdvancedPermissionGroup({
onChange,
disabled,
scope = Scope.Organization,
isPrivate = false,
}: AdvancedPermissionGroupProps) {
const { t } = useLocale();
const { toggleSinglePermission, toggleResourcePermissionLevel } = usePermissions(scope);
const scopedRegistry = getPermissionsForScope(scope);
const scopedRegistry = getPermissionsForScope(scope, isPrivate);
const resourceConfig = scopedRegistry[resource];
const [isExpanded, setIsExpanded] = useState(false);
@@ -56,9 +56,17 @@ interface RoleSheetProps {
onOpenChange: (open: boolean) => void;
teamId: number;
scope?: Scope;
isPrivate?: boolean; // Add isPrivate prop to control permission visibility
}
export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Organization }: RoleSheetProps) {
export function RoleSheet({
role,
open,
onOpenChange,
teamId,
scope = Scope.Organization,
isPrivate = false
}: RoleSheetProps) {
const { t } = useLocale();
const router = useRouter();
const isEditing = Boolean(role);
@@ -108,7 +116,8 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
const { isAdvancedMode, permissions, color } = form.watch();
const { filteredResources, scopedRegistry } = useMemo(() => {
const scopedRegistry = getPermissionsForScope(scope);
// Use privacy-aware filtering if we have privacy information
const scopedRegistry = getPermissionsForScope(scope, isPrivate);
const filteredResources = Object.keys(scopedRegistry).filter((resource) =>
t(
scopedRegistry[resource as Resource][CrudAction.All as keyof (typeof scopedRegistry)[Resource]]
@@ -118,7 +127,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
.includes(searchQuery.toLowerCase())
);
return { filteredResources, scopedRegistry };
}, [searchQuery, t, scope]);
}, [searchQuery, t, scope, isPrivate]);
const createMutation = trpc.viewer.pbac.createRole.useMutation({
onSuccess: async () => {
@@ -221,6 +230,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
onChange={(newPermissions) => form.setValue("permissions", newPermissions)}
disabled={isSystemRole}
scope={scope}
isPrivate={isPrivate}
/>
))}{" "}
</div>
@@ -245,6 +255,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
onChange={(newPermissions) => form.setValue("permissions", newPermissions)}
disabled={isSystemRole}
scope={scope}
isPrivate={isPrivate}
/>
))}
</div>{" "}
@@ -48,6 +48,7 @@ interface RolesListProps {
initialSheetOpen?: boolean;
teamId: number;
scope?: Scope;
isPrivate?: boolean;
}
export function RolesList({
@@ -57,6 +58,7 @@ export function RolesList({
initialSheetOpen,
teamId,
scope,
isPrivate,
}: RolesListProps) {
const { t } = useLocale();
const { isOpen, setIsOpen, selectedRoleId, setSelectedRoleId, handleSheetOpenChange } = useRoleStates(
@@ -113,6 +115,7 @@ export function RolesList({
onOpenChange={handleSheetOpenChange}
teamId={teamId}
scope={scope}
isPrivate={isPrivate}
/>
</>
);
@@ -18,6 +18,7 @@ interface SimplePermissionItemProps {
onChange: (permissions: string[]) => void;
disabled?: boolean;
scope?: Scope;
isPrivate?: boolean;
}
export function SimplePermissionItem({
@@ -26,10 +27,11 @@ export function SimplePermissionItem({
onChange,
disabled,
scope = Scope.Organization,
isPrivate = false,
}: SimplePermissionItemProps) {
const { t } = useLocale();
const { getResourcePermissionLevel, toggleResourcePermissionLevel } = usePermissions(scope);
const scopedRegistry = getPermissionsForScope(scope);
const scopedRegistry = getPermissionsForScope(scope, isPrivate);
const registry = scopedRegistry || PERMISSION_REGISTRY;
const isAllResources = resource === "*";
@@ -44,6 +44,18 @@ const getCachedResourcePermissions = unstable_cache(
{ revalidate: 3600 }
);
const getCachedTeamPrivacy = unstable_cache(
async (teamId: number) => {
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { isPrivate: true },
});
return team?.isPrivate ?? false;
},
["team-privacy"],
{ revalidate: 3600 }
);
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("roles_and_permissions"),
@@ -69,9 +81,10 @@ const Page = async ({ searchParams }: { searchParams: Record<string, string | st
roleSearchParamsCache.parse(searchParams);
const [roles, rolePermissions] = await Promise.all([
const [roles, rolePermissions, isPrivate] = await Promise.all([
getCachedTeamRoles(session.user.org.id),
getCachedResourcePermissions(session.user.id, session.user.org.id, Resource.Role),
getCachedTeamPrivacy(session.user.org.id),
]);
// NOTE: this approach of fetching permssions per resource does not account for fall back roles.
@@ -108,6 +121,7 @@ const Page = async ({ searchParams }: { searchParams: Record<string, string | st
}}
initialSelectedRole={selectedRole}
initialSheetOpen={isSheetOpen}
isPrivate={isPrivate}
/>
</SettingsHeader>
);
@@ -102,6 +102,7 @@ const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
CustomAction.ChangeMemberRole,
CustomAction.Remove,
CustomAction.ListMembers,
CustomAction.ListMembersPrivate,
CustomAction.Impersonate,
],
fallbackRoles: {
@@ -120,12 +121,17 @@ const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
[CustomAction.Impersonate]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.ListMembersPrivate]: {
roles: fallbackRolesCanListMembers,
},
},
});
// Map specific permissions to member actions
const memberPermissions = {
canListMembers: permissions[CustomAction.ListMembers],
canListMembers: team.isPrivate
? permissions[CustomAction.ListMembersPrivate]
: permissions[CustomAction.ListMembers],
canInvite: permissions[CustomAction.Invite],
canChangeMemberRole: permissions[CustomAction.ChangeMemberRole],
canRemove: permissions[CustomAction.Remove],
@@ -4,6 +4,7 @@ import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import type { AppFlags } from "@calcom/features/flags/config";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
@@ -11,7 +12,6 @@ import { Resource, CrudAction, Scope } from "@calcom/features/pbac/domain/types/
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { RoleService } from "@calcom/features/pbac/services/role.service";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -51,6 +51,19 @@ const getCachedResourcePermissions = (userId: number, teamId: number, resource:
{ revalidate: 3600, tags: [`resource-permissions-${teamId}`] }
);
const getCachedTeamPrivacy = (teamId: number) =>
unstable_cache(
async () => {
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { isPrivate: true },
});
return team?.isPrivate ?? false;
},
[`team-privacy-${teamId}`],
{ revalidate: 3600, tags: [`team-privacy-${teamId}`] }
);
const getCachedTeam = (teamId: string, userId: number) =>
unstable_cache(
async () => {
@@ -111,9 +124,10 @@ const Page = async ({
roleSearchParamsCache.parse(searchParams);
const [roles, rolePermissions] = await Promise.all([
const [roles, rolePermissions, isPrivate] = await Promise.all([
getCachedTeamRoles(team.id)(),
getCachedResourcePermissions(session.user.id, team.id, Resource.Role)(),
getCachedTeamPrivacy(team.id)(),
]);
// NOTE: this approach of fetching permssions per resource does not account for fall back roles.
@@ -153,6 +167,7 @@ const Page = async ({
initialSelectedRole={selectedRole}
initialSheetOpen={isSheetOpen}
scope={Scope.Team}
isPrivate={isPrivate}
/>
</SettingsHeader>
);
@@ -70,6 +70,7 @@ const Page = async () => {
userRole: session.user.org.role,
actions: [
CustomAction.ListMembers,
CustomAction.ListMembersPrivate,
CustomAction.Invite,
CustomAction.ChangeMemberRole,
CustomAction.Remove,
@@ -79,6 +80,9 @@ const Page = async () => {
[CustomAction.ListMembers]: {
roles: fallbackRolesThatCanSeeMembers,
},
[CustomAction.ListMembersPrivate]: {
roles: fallbackRolesThatCanSeeMembers,
},
[CustomAction.Invite]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
@@ -96,7 +100,9 @@ const Page = async () => {
// Map specific permissions to member actions
const memberPermissions = {
canListMembers: permissions[CustomAction.ListMembers],
canListMembers: org.isPrivate
? permissions[CustomAction.ListMembersPrivate]
: permissions[CustomAction.ListMembers],
canInvite: permissions[CustomAction.Invite],
canChangeMemberRole: permissions[CustomAction.ChangeMemberRole],
canRemove: permissions[CustomAction.Remove],
@@ -5,13 +5,13 @@ import {
getOrganizationSettings,
getVerifiedDomain,
} from "@calcom/features/ee/organizations/lib/orgSettings";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { IS_CALCOM } from "@calcom/lib/constants";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import logger from "@calcom/lib/logger";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import slugify from "@calcom/lib/slugify";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import prisma from "@calcom/prisma";
@@ -27,6 +27,7 @@ export enum CustomAction {
Remove = "remove",
ChangeMemberRole = "changeMemberRole",
ListMembers = "listMembers",
ListMembersPrivate = "listMembersPrivate",
ManageBilling = "manageBilling",
ReadTeamBookings = "readTeamBookings",
ReadOrgBookings = "readOrgBookings",
@@ -46,6 +47,9 @@ export interface PermissionDetails {
descriptionI18nKey: string;
scope?: Scope[]; // Optional for backward compatibility
dependsOn?: PermissionString[]; // Dependencies that must be enabled when this permission is enabled
visibleWhen?: {
teamPrivacy?: "private" | "public" | "both"; // Control visibility based on team privacy setting
};
}
export type ResourceConfig = {
@@ -122,12 +126,14 @@ export const filterResourceConfig = (config: ResourceConfig): Omit<ResourceConfi
};
/**
* Filter resources and actions based on scope
* Filter resources and actions based on scope and optionally team privacy settings
* @param scope The scope to filter by (Team or Organization)
* @param isPrivate Whether the team/organization is private (optional)
* @returns Filtered permission registry
*/
export const getPermissionsForScope = (scope: Scope): PermissionRegistry => {
export const getPermissionsForScope = (scope: Scope, isPrivate?: boolean): PermissionRegistry => {
const filteredRegistry: Partial<PermissionRegistry> = {};
const teamPrivacy = isPrivate !== undefined ? (isPrivate ? "private" : "public") : undefined;
Object.entries(PERMISSION_REGISTRY).forEach(([resource, config]) => {
const filteredConfig: ResourceConfig = { _resource: config._resource };
@@ -136,9 +142,17 @@ export const getPermissionsForScope = (scope: Scope): PermissionRegistry => {
if (action === "_resource") return;
const permissionDetails = details as PermissionDetails;
// If no scope is defined, include in both Team and Organization (backward compatibility)
// If scope is defined, only include if it matches the requested scope
if (!permissionDetails.scope || permissionDetails.scope.includes(scope)) {
// Check scope
const scopeMatches = !permissionDetails.scope || permissionDetails.scope.includes(scope);
// Check privacy visibility (only if isPrivate is provided)
const privacyMatches = teamPrivacy === undefined ||
!permissionDetails.visibleWhen?.teamPrivacy ||
permissionDetails.visibleWhen.teamPrivacy === "both" ||
permissionDetails.visibleWhen.teamPrivacy === teamPrivacy;
if (scopeMatches && privacyMatches) {
filteredConfig[action as CrudAction | CustomAction] = permissionDetails;
}
});
@@ -282,6 +296,19 @@ export const PERMISSION_REGISTRY: PermissionRegistry = {
category: "team",
i18nKey: "pbac_action_list_members",
descriptionI18nKey: "pbac_desc_list_team_members",
visibleWhen: {
teamPrivacy: "public", // Only show for public teams
},
},
[CustomAction.ListMembersPrivate]: {
description: "List private team members",
category: "team",
i18nKey: "pbac_action_list_members", // Use same UI label as listMembers for consistency
descriptionI18nKey: "pbac_desc_list_team_members", // Use same description as listMembers
dependsOn: ["team.read"],
visibleWhen: {
teamPrivacy: "private", // Only show for private teams
},
},
[CustomAction.ChangeMemberRole]: {
description: "Change role of team members",
@@ -330,6 +357,20 @@ export const PERMISSION_REGISTRY: PermissionRegistry = {
descriptionI18nKey: "pbac_desc_list_organization_members",
scope: [Scope.Organization],
dependsOn: ["organization.read"],
visibleWhen: {
teamPrivacy: "public", // Only show for public orgs
},
},
[CustomAction.ListMembersPrivate]: {
description: "List private organization members",
category: "org",
i18nKey: "pbac_action_list_members", // Same UI label as listMembers for consistency
descriptionI18nKey: "pbac_desc_list_organization_members", // Same description as listMembers
scope: [Scope.Organization],
dependsOn: ["organization.read"],
visibleWhen: {
teamPrivacy: "private", // Only show for private orgs
},
},
[CustomAction.Invite]: {
description: "Invite organization members",
@@ -0,0 +1,16 @@
-- This permission allows admin users to view members of private teams and organizations
INSERT INTO "RolePermission" (id, "roleId", resource, action, "createdAt")
SELECT
gen_random_uuid(), 'admin_role', resource, action, NOW()
FROM (
VALUES
-- Team listMembersPrivate permission
('team', 'listMembersPrivate'),
-- Organization listMembersPrivate permission
('organization', 'listMembersPrivate')
) AS permissions(resource, action)
ON CONFLICT ("roleId", resource, action) DO NOTHING;
-- Note: Owner role already has wildcard permissions (*.*) so it inherits all listMembersPrivate permissions automatically
@@ -1,4 +1,6 @@
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "../../../types";
import type { TGetMembersInputSchema } from "./getMembers.schema";
@@ -16,9 +18,21 @@ export const getMembersHandler = async ({ input, ctx }: CreateOptions) => {
if (!ctx.user.organizationId) return [];
const isOrgPrivate = ctx.user.organization.isPrivate;
const isOrgAdmin = ctx.user.organization.isOrgAdmin;
if (isOrgPrivate && !isOrgAdmin) return [];
const permissionCheckService = new PermissionCheckService();
const hasPermissionToViewMembers = await permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: ctx.user.organizationId,
permission: ctx.user.organization.isPrivate
? "organization.listMembersPrivate"
: "organization.listMembers",
fallbackRoles: ctx.user.organization.isPrivate
? [MembershipRole.ADMIN, MembershipRole.OWNER]
: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
});
if (!hasPermissionToViewMembers) return [];
const teamQuery = await prisma.team.findUnique({
where: {
@@ -8,6 +8,7 @@ import { listMembersHandler } from "./listMembers.handler";
vi.mock("@calcom/prisma", () => ({
prisma,
default: prisma, // Add default export for db
}));
// Mock FeaturesRepository
@@ -22,9 +23,18 @@ vi.mock("@calcom/features/flags/features.repository", () => ({
vi.mock("@calcom/features/pbac/lib/resource-permissions", () => ({
getSpecificPermissions: vi.fn().mockResolvedValue({
listMembers: true,
listMembersPrivate: true,
}),
}));
// Mock PermissionCheckService
const mockCheckPermission = vi.fn().mockResolvedValue(true);
vi.mock("@calcom/features/pbac/services/permission-check.service", () => ({
PermissionCheckService: vi.fn().mockImplementation(() => ({
checkPermission: mockCheckPermission,
})),
}));
// Mock UserRepository
vi.mock("@calcom/lib/server/repository/user", () => ({
UserRepository: vi.fn().mockImplementation(() => ({
@@ -65,6 +75,8 @@ describe("listMembersHandler", () => {
prisma.membership.findMany.mockResolvedValue([]);
prisma.membership.count.mockResolvedValue(0);
prisma.membership.findFirst.mockResolvedValue({ role: "ADMIN" });
// Mock team.findUnique to return only isPrivate field since that's what the handler selects
prisma.team.findUnique.mockResolvedValue({ isPrivate: false } as any);
});
it("should filter by customRoleId when PBAC is enabled", async () => {
@@ -234,4 +246,72 @@ describe("listMembersHandler", () => {
})
);
});
it("should check listMembersPrivate permission for private organizations", async () => {
// Clear previous calls
mockCheckPermission.mockClear();
mockCheckPermission.mockResolvedValue(true);
// Mock private organization
// eslint-disable-next-line @typescript-eslint/no-explicit-any
prisma.team.findUnique.mockResolvedValue({ isPrivate: true } as any);
const privateOrgUser = {
...mockUser,
organization: {
...mockUser.organization,
isPrivate: true,
},
};
await listMembersHandler({
ctx: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
user: privateOrgUser as any,
},
input: {
limit: 25,
offset: 0,
filters: [],
},
});
// Verify that checkPermission was called with listMembersPrivate
expect(mockCheckPermission).toHaveBeenCalledWith(
expect.objectContaining({
permission: "organization.listMembersPrivate",
fallbackRoles: ["ADMIN", "OWNER"],
})
);
});
it("should check listMembers permission for public organizations", async () => {
// Clear previous calls
mockCheckPermission.mockClear();
mockCheckPermission.mockResolvedValue(true);
// Mock public organization (default)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
prisma.team.findUnique.mockResolvedValue({ isPrivate: false } as any);
await listMembersHandler({
ctx: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
user: mockUser as any,
},
input: {
limit: 25,
offset: 0,
filters: [],
},
});
// Verify that checkPermission was called with listMembers
expect(mockCheckPermission).toHaveBeenCalledWith(
expect.objectContaining({
permission: "organization.listMembers",
fallbackRoles: ["MEMBER", "ADMIN", "OWNER"],
})
);
});
});
@@ -3,6 +3,7 @@ import { type TypedColumnFilter, ColumnFilterType } from "@calcom/features/data-
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { Resource, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry";
import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
@@ -87,23 +88,20 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
throw new TRPCError({ code: "UNAUTHORIZED", message: "User is not a member of this organization." });
}
// Check PBAC permissions for listing organization members
const permissions = await getSpecificPermissions({
const permissionCheckService = new PermissionCheckService();
const hasPermission = await permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: organizationId,
resource: Resource.Organization,
userRole: membership.role,
actions: [CustomAction.ListMembers],
fallbackRoles: {
[CustomAction.ListMembers]: {
roles: ctx.user.organization.isPrivate
? [MembershipRole.ADMIN, MembershipRole.OWNER]
: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
permission: ctx.user.organization.isPrivate
? "organization.listMembersPrivate"
: "organization.listMembers",
fallbackRoles: ctx.user.organization.isPrivate
? [MembershipRole.ADMIN, MembershipRole.OWNER]
: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
});
if (!permissions[CustomAction.ListMembers]) {
if (!hasPermission) {
return {
canUserGetMembers: false,
rows: [],
@@ -1,3 +1,6 @@
import { Resource, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry";
import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { UserRepository } from "@calcom/lib/server/repository/user";
import type { PrismaClient } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
@@ -16,14 +19,18 @@ type ListMembersOptions = {
export const legacyListMembers = async ({ ctx, input }: ListMembersOptions) => {
const { prisma } = ctx;
const { isOrgAdmin } = ctx.user.organization;
const hasPermsToView = !ctx.user.organization.isPrivate || isOrgAdmin;
const orgId = ctx.user.organizationId;
if (!hasPermsToView) {
return {
members: [],
nextCursor: undefined,
};
// Check PBAC permissions for the organization if it's private
if (orgId) {
const hasPermsToView = await checkCanAccessOrgMembers(ctx, orgId);
if (!hasPermsToView) {
return {
members: [],
nextCursor: undefined,
};
}
}
const limit = input.limit ?? 10;
@@ -129,4 +136,30 @@ export const legacyListMembers = async ({ ctx, input }: ListMembersOptions) => {
};
};
const checkCanAccessOrgMembers = async (ctx: ListMembersOptions["ctx"], orgId: number): Promise<boolean> => {
const { prisma } = ctx;
// Get organization info to verify it's private
const org = await prisma.team.findUnique({
where: { id: orgId },
select: { isPrivate: true },
});
if (!org) return false;
// Check PBAC permissions for listing members
const permissionCheckService = new PermissionCheckService();
const hasPermission = await permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: orgId,
permission: org.isPrivate ? "organization.listMembersPrivate" : "organization.listMembers",
fallbackRoles: org.isPrivate
? [MembershipRole.ADMIN, MembershipRole.OWNER]
: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
});
return hasPermission;
};
export default legacyListMembers;
@@ -1,5 +1,10 @@
import { Resource, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry";
import {
Resource,
CustomAction,
PermissionString,
} from "@calcom/features/pbac/domain/types/permission-registry";
import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import { TeamRepository } from "@calcom/lib/server/repository/team";
@@ -157,37 +162,22 @@ const checkCanAccessMembers = async (ctx: ListMembersHandlerOptions["ctx"], team
if (!team) return false;
// Get user's membership in the team
const membership = await prisma.membership.findFirst({
where: {
teamId,
userId: ctx.user.id,
accepted: true,
},
});
if (!membership) return false;
// Determine the resource type based on whether this is an org or team
const resource = isTargetingOrg ? Resource.Organization : Resource.Team;
const targetAction = team.isPrivate ? CustomAction.ListMembersPrivate : CustomAction.ListMembers;
const permissionString = `${resource}.${targetAction}` as PermissionString;
// Check PBAC permissions for listing members
const permissions = await getSpecificPermissions({
const permissionCheckService = new PermissionCheckService();
return permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: teamId,
resource: resource,
userRole: membership.role,
actions: [CustomAction.ListMembers],
fallbackRoles: {
[CustomAction.ListMembers]: {
roles: team.isPrivate
? [MembershipRole.ADMIN, MembershipRole.OWNER]
: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
permission: permissionString,
fallbackRoles: team.isPrivate
? [MembershipRole.ADMIN, MembershipRole.OWNER]
: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
});
return permissions[CustomAction.ListMembers];
};
export default listMembersHandler;