diff --git a/apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx b/apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx
index 43f400905c..116c8ff97f 100644
--- a/apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx
@@ -8,6 +8,10 @@ import { z } from "zod";
import { EventTypeWebWrapper } from "@calcom/atoms/event-types/wrappers/EventTypeWebWrapper";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
+import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
+import { prisma } from "@calcom/prisma";
+import { MembershipRole } from "@calcom/prisma/enums";
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -40,6 +44,95 @@ const getCachedEventType = unstable_cache(
{ revalidate: 3600 } // Cache for 1 hour
);
+const getEventPermissions = async (userId: number, teamId: number | null) => {
+ // Personal event - has all perms
+ if (!teamId)
+ return {
+ eventTypes: {
+ canRead: true,
+ canCreate: true,
+ canUpdate: true,
+ canDelete: true,
+ },
+ workflows: {
+ canRead: true,
+ canCreate: true,
+ canUpdate: true,
+ canDelete: true,
+ },
+ };
+
+ const membership = await prisma.membership.findFirst({
+ where: {
+ userId,
+ teamId,
+ },
+ select: {
+ role: true,
+ },
+ });
+
+ if (!membership) throw new Error("Membership not found");
+
+ const [eventTypePermissions, workflowPermissions] = await Promise.all([
+ getResourcePermissions({
+ userId,
+ teamId,
+ resource: Resource.EventType,
+ userRole: membership.role,
+ fallbackRoles: {
+ read: {
+ roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ update: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ delete: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ create: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ },
+ }),
+ getResourcePermissions({
+ userId,
+ teamId,
+ resource: Resource.Workflow,
+ userRole: membership.role,
+ fallbackRoles: {
+ read: {
+ roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ update: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ delete: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ create: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ },
+ }),
+ ]);
+
+ return {
+ eventTypes: {
+ canRead: eventTypePermissions.canRead,
+ canCreate: eventTypePermissions.canCreate,
+ canUpdate: eventTypePermissions.canEdit,
+ canDelete: eventTypePermissions.canDelete,
+ },
+ workflows: {
+ canRead: workflowPermissions.canRead,
+ canCreate: workflowPermissions.canCreate,
+ canUpdate: workflowPermissions.canEdit,
+ canDelete: workflowPermissions.canDelete,
+ },
+ };
+};
+
const ServerPage = async ({ params }: PageProps) => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user?.id) {
@@ -59,7 +152,10 @@ const ServerPage = async ({ params }: PageProps) => {
throw new Error("This event type does not exist");
}
- return ;
+ // Fetch permissions for the event type's team
+ const permissions = await getEventPermissions(session.user.id, data.eventType.teamId);
+
+ return ;
};
export default ServerPage;
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/RoleSheet.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/RoleSheet.tsx
index df10530954..68360fdb30 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/RoleSheet.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/RoleSheet.tsx
@@ -107,9 +107,9 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
const { isAdvancedMode, permissions, color } = form.watch();
- const filteredResources = useMemo(() => {
+ const { filteredResources, scopedRegistry } = useMemo(() => {
const scopedRegistry = getPermissionsForScope(scope);
- return Object.keys(scopedRegistry).filter((resource) =>
+ const filteredResources = Object.keys(scopedRegistry).filter((resource) =>
t(
scopedRegistry[resource as Resource][CrudAction.All as keyof (typeof scopedRegistry)[Resource]]
?.i18nKey || ""
@@ -117,6 +117,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
.toLowerCase()
.includes(searchQuery.toLowerCase())
);
+ return { filteredResources, scopedRegistry };
}, [searchQuery, t, scope]);
const createMutation = trpc.viewer.pbac.createRole.useMutation({
@@ -212,7 +213,6 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
disabled={isSystemRole}
/>
-
{filteredResources.map((resource) => (
- ))}
+ ))}{" "}
) : (
@@ -237,7 +237,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
- {Object.keys(getPermissionsForScope(scope)).map((resource) => (
+ {Object.keys(scopedRegistry).map((resource) => (
))}
-
+ {" "}
)}
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/SimplePermissionItem.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/SimplePermissionItem.tsx
index 5ba1ec0d86..e658890c30 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/SimplePermissionItem.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/SimplePermissionItem.tsx
@@ -1,7 +1,11 @@
"use client";
-import type { Resource, Scope } from "@calcom/features/pbac/domain/types/permission-registry";
-import { PERMISSION_REGISTRY } from "@calcom/features/pbac/domain/types/permission-registry";
+import type { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
+import {
+ Scope,
+ PERMISSION_REGISTRY,
+ getPermissionsForScope,
+} from "@calcom/features/pbac/domain/types/permission-registry";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ToggleGroup } from "@calcom/ui/components/form";
@@ -21,11 +25,13 @@ export function SimplePermissionItem({
permissions,
onChange,
disabled,
- scope,
+ scope = Scope.Organization,
}: SimplePermissionItemProps) {
const { t } = useLocale();
const { getResourcePermissionLevel, toggleResourcePermissionLevel } = usePermissions(scope);
+ const scopedRegistry = getPermissionsForScope(scope);
+ const registry = scopedRegistry || PERMISSION_REGISTRY;
const isAllResources = resource === "*";
const options = isAllResources
? [
@@ -41,7 +47,7 @@ export function SimplePermissionItem({
return (
- {t(PERMISSION_REGISTRY[resource as Resource]._resource?.i18nKey || resource)}
+ {t(registry[resource as Resource]._resource?.i18nKey || resource)}
{
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts
index d7d5c2ea36..176186a29f 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts
@@ -1,8 +1,5 @@
import { CrudAction, Scope } from "@calcom/features/pbac/domain/types/permission-registry";
-import {
- PERMISSION_REGISTRY,
- getPermissionsForScope,
-} from "@calcom/features/pbac/domain/types/permission-registry";
+import { getPermissionsForScope } from "@calcom/features/pbac/domain/types/permission-registry";
import {
getTransitiveDependencies,
getTransitiveDependents,
@@ -52,7 +49,8 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
return permissions.includes("*.*") ? "all" : "none";
}
- const resourceConfig = PERMISSION_REGISTRY[resource as keyof typeof PERMISSION_REGISTRY];
+ const scopedRegistry = getPermissionsForScope(scope);
+ const resourceConfig = scopedRegistry[resource as keyof typeof scopedRegistry];
if (!resourceConfig) return "none";
// Check if global all permissions (*.*) is present
@@ -111,7 +109,19 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
allResourcePerms = Object.keys(resourceConfig)
.filter((action) => !action.startsWith("_"))
.map((action) => `${resource}.${action}`);
+
+ // Add the resource permissions
newPermissions.push(...allResourcePerms);
+
+ // Add all transitive dependencies for each permission
+ allResourcePerms.forEach((perm) => {
+ const dependencies = getTransitiveDependencies(perm, scope);
+ dependencies.forEach((dep) => {
+ if (!newPermissions.includes(dep)) {
+ newPermissions.push(dep);
+ }
+ });
+ });
break;
}
@@ -132,15 +142,12 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
// First, remove *.* since we're modifying individual permissions
let newPermissions = currentPermissions.filter((p) => p !== "*.*");
- // Parse the permission to get resource and action
- const [resource, action] = permission.split(".");
-
if (enabled) {
// Add the requested permission
newPermissions.push(permission);
// Add all transitive dependencies
- const dependencies = getTransitiveDependencies(permission);
+ const dependencies = getTransitiveDependencies(permission, scope);
dependencies.forEach((dependency) => {
if (!newPermissions.includes(dependency)) {
newPermissions.push(dependency);
@@ -151,7 +158,7 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
newPermissions = newPermissions.filter((p) => p !== permission);
// Remove all transitive dependents
- const dependents = getTransitiveDependents(permission);
+ const dependents = getTransitiveDependents(permission, scope);
dependents.forEach((dependent) => {
newPermissions = newPermissions.filter((p) => p !== dependent);
});
diff --git a/apps/web/modules/event-types/views/event-types-listing-view.tsx b/apps/web/modules/event-types/views/event-types-listing-view.tsx
index 6ca2619060..6fe670651a 100644
--- a/apps/web/modules/event-types/views/event-types-listing-view.tsx
+++ b/apps/web/modules/event-types/views/event-types-listing-view.tsx
@@ -27,7 +27,7 @@ import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { parseEventTypeColor } from "@calcom/lib/isEventTypeColor";
import { localStorage } from "@calcom/lib/webstorage";
-import type { MembershipRole } from "@calcom/prisma/enums";
+import { MembershipRole } from "@calcom/prisma/enums";
import { SchedulingType } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
@@ -954,6 +954,24 @@ export const EventTypesCTA = ({ userEventGroupsData }: Omit) => {
userEventGroupsData?.profiles
?.filter((profile) => !profile.readOnly)
?.filter((profile) => !profile.eventTypesLockedByOrg)
+ ?.filter((profile) => {
+ // For personal profiles (teamId is null), always allow creation
+ if (!profile.teamId) {
+ return true;
+ }
+
+ // For team profiles, check if user has eventType.create permission
+ // This will be populated by the server-side PBAC check
+ // Fallback to role-based check (admin/owner) if canCreateEventTypes is not set
+ if (profile.canCreateEventTypes !== undefined) {
+ return profile.canCreateEventTypes;
+ }
+
+ // Fallback: allow admin and owner roles
+ return (
+ profile.membershipRole === MembershipRole.ADMIN || profile.membershipRole === MembershipRole.OWNER
+ );
+ })
?.map((profile) => {
return {
teamId: profile.teamId,
diff --git a/packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx b/packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx
index 87e27a2200..0cae9b1b9a 100644
--- a/packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx
+++ b/packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx
@@ -25,7 +25,10 @@ import { showToast } from "@calcom/ui/components/toast";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { revalidateEventTypeEditPage } from "@calcom/web/app/(use-page-wrapper)/event-types/[type]/actions";
-type PartialWorkflowType = Pick;
+type PartialWorkflowType = Pick<
+ WorkflowType,
+ "name" | "activeOn" | "isOrg" | "steps" | "id" | "readOnly" | "permissions"
+>;
type ItemProps = {
workflow: PartialWorkflowType;
@@ -42,14 +45,6 @@ const WorkflowListItem = (props: ItemProps) => {
const { workflow, eventType, isActive } = props;
const { t } = useLocale();
- const [activeEventTypeIds, setActiveEventTypeIds] = useState(
- workflow.activeOn?.map((active) => {
- if (active.eventType) {
- return active.eventType.id;
- }
- }) ?? []
- );
-
const utils = trpc.useUtils();
const activateEventTypeMutation = trpc.viewer.workflows.activateEventType.useMutation({
@@ -137,7 +132,7 @@ const WorkflowListItem = (props: ItemProps) => {
>
- {!workflow.readOnly && (
+ {workflow.permissions?.canUpdate && (