Files
calendar/packages/features/pbac/lib/team-member-permissions.ts
08a2b0bfb5 feat: add organization.passwordReset PBAC permission (#27377)
* feat: add organization.passwordReset PBAC permission

Allow org admins/owners to reset passwords for members of their
organization via a new PBAC permission. Previously this was only
available to system-level admins.

- Add PasswordReset to CustomAction enum and PERMISSION_REGISTRY
- Create migration to grant permission to admin_role (owner has wildcard)
- Add org-scoped tRPC endpoint using createOrgPbacProcedure
- Handler validates org membership, prevents self-targeting, and blocks
  resetting owner passwords
- Wire permission through MemberPermissions, getOrgMembersPageData, and
  the org members table UI dropdown

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: use targeted select in org password reset to avoid over-fetching

Replace findById with findForPasswordReset repository method that only
selects email, name, and locale instead of the full userSelect which
includes sensitive fields like twoFactorSecret and backupCodes.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* test: add unit tests for sendPasswordReset handler

Co-Authored-By: Claude Opus 4.5 <[email protected]>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-01-29 11:18:02 +00:00

90 lines
2.6 KiB
TypeScript

import { MembershipRole } from "@calcom/prisma/enums";
import { CustomAction, Resource } from "../domain/types/permission-registry";
import { getSpecificPermissions } from "./resource-permissions";
export interface MemberPermissions {
canListMembers: boolean;
canInvite: boolean;
canChangeMemberRole: boolean;
canRemove: boolean;
canImpersonate: boolean;
canResetPassword?: boolean;
canEditAttributesForUser?: boolean;
canViewAttributes?: boolean;
}
interface TeamWithMembership {
id: number;
isPrivate: boolean;
membership: {
role: MembershipRole;
accepted: boolean;
};
}
interface GetTeamMemberPermissionsOptions {
userId: number;
team: TeamWithMembership;
}
/**
* Gets team member permissions using PBAC or fallback to role-based permissions.
*/
export async function getTeamMemberPermissions({
userId,
team,
}: GetTeamMemberPermissionsOptions): Promise<MemberPermissions> {
// Determine fallback roles for ListMembers based on team privacy
const fallbackRolesCanListMembers: MembershipRole[] = [MembershipRole.ADMIN, MembershipRole.OWNER];
if (!team.isPrivate) {
fallbackRolesCanListMembers.push(MembershipRole.MEMBER);
}
// Get specific PBAC permissions for team member actions
const permissions = await getSpecificPermissions({
userId,
teamId: team.id,
resource: Resource.Team,
userRole: team.membership.role,
actions: [
CustomAction.Invite,
CustomAction.ChangeMemberRole,
CustomAction.Remove,
CustomAction.ListMembers,
CustomAction.ListMembersPrivate,
CustomAction.Impersonate,
],
fallbackRoles: {
[CustomAction.Invite]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.ChangeMemberRole]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.Remove]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.ListMembers]: {
roles: fallbackRolesCanListMembers,
},
[CustomAction.Impersonate]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.ListMembersPrivate]: {
roles: fallbackRolesCanListMembers,
},
},
});
// Map specific permissions to member actions
return {
canListMembers: team.isPrivate
? permissions[CustomAction.ListMembersPrivate]
: permissions[CustomAction.ListMembers],
canInvite: permissions[CustomAction.Invite],
canChangeMemberRole: permissions[CustomAction.ChangeMemberRole],
canRemove: permissions[CustomAction.Remove],
canImpersonate: permissions[CustomAction.Impersonate],
};
}