diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx
index 55cbeac844c307..cfa0900043cfb7 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx
@@ -1,7 +1,15 @@
import { _generateMetadata, getTranslate } from "app/_utils";
+import { headers, cookies } from "next/headers";
+import { redirect } from "next/navigation";
import OrgSettingsAttributesPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view";
+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 SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
+import { MembershipRole } from "@calcom/prisma/enums";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -14,10 +22,40 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
+ return redirect("/settings/profile");
+ }
+
+ const { canRead, canEdit, canDelete, canCreate } = await getResourcePermissions({
+ userId: session.user.id,
+ teamId: session.user.profile.organizationId,
+ resource: Resource.Attributes,
+ userRole: session.user.org.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],
+ },
+ },
+ });
+
+ if (!canRead) {
+ return redirect("/settings/profile");
+ }
return (
-
+
);
};
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/dsync/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/dsync/page.tsx
index a53e48fd1ec599..161ce65275a78c 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/dsync/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/dsync/page.tsx
@@ -1,7 +1,15 @@
import { _generateMetadata, getTranslate } from "app/_utils";
+import { headers, cookies } from "next/headers";
+import { redirect } from "next/navigation";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import DirectorySyncTeamView from "@calcom/features/ee/dsync/page/team-dsync-view";
+import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
+import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
+import { MembershipRole } from "@calcom/prisma/enums";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -14,10 +22,27 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
+ return redirect("/settings/organizations/general");
+ }
+
+ const { canEdit } = await getResourcePermissions({
+ userId: session.user.id,
+ teamId: session.user.profile.organizationId,
+ resource: Resource.Organization,
+ userRole: session.user.org.role,
+ fallbackRoles: {
+ update: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ },
+ });
return (
-
+
);
};
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx
index 8900c77c70038b..7a4e75a2f43e0a 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx
@@ -1,7 +1,15 @@
import { _generateMetadata, getTranslate } from "app/_utils";
+import { headers, cookies } from "next/headers";
+import { redirect } from "next/navigation";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";
+import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
+import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
+import { MembershipRole } from "@calcom/prisma/enums";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -15,9 +23,34 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
+ return redirect("/settings/profile");
+ }
+
+ const { canRead, canEdit } = await getResourcePermissions({
+ userId: session.user.id,
+ teamId: session.user.profile.organizationId,
+ resource: Resource.Organization,
+ userRole: session.user.org.role,
+ fallbackRoles: {
+ read: {
+ roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ update: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ },
+ });
+
+ if (!canRead) {
+ return redirect("/settings/profile");
+ }
+
return (
-
+
);
};
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx
index 93750ac2fdadfe..a3fcf936b1cb6a 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx
@@ -1,7 +1,15 @@
import { _generateMetadata, getTranslate } from "app/_utils";
+import { headers, cookies } from "next/headers";
+import { redirect } from "next/navigation";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import OrgSSOView from "@calcom/features/ee/sso/page/orgs-sso-view";
+import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
+import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
+import { MembershipRole } from "@calcom/prisma/enums";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -14,10 +22,27 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
+ return redirect("/settings/organizations/general");
+ }
+
+ const { canEdit } = await getResourcePermissions({
+ userId: session.user.id,
+ teamId: session.user.profile.organizationId,
+ resource: Resource.Organization,
+ userRole: session.user.org.role,
+ fallbackRoles: {
+ update: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ },
+ });
return (
-
+
);
};
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx
index 75aeaf2f6e8b36..5ce47fad0ba9b1 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx
@@ -1,7 +1,15 @@
import { _generateMetadata, getTranslate } from "app/_utils";
+import { headers, cookies } from "next/headers";
+import { redirect } from "next/navigation";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/general";
+import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
+import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
+import { MembershipRole } from "@calcom/prisma/enums";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -15,9 +23,30 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
+ return redirect("/settings/profile");
+ }
+
+ const { canRead, canEdit } = await getResourcePermissions({
+ userId: session.user.id,
+ teamId: session.user.profile.organizationId,
+ resource: Resource.Organization,
+ userRole: session.user.org.role,
+ fallbackRoles: {
+ read: {
+ roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ update: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ },
+ });
+
return (
-
+
);
};
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx
index 3849a833050756..4b58650b45d306 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx
@@ -1,7 +1,16 @@
import { _generateMetadata, getTranslate } from "app/_utils";
+import { cookies, headers } from "next/headers";
+import { redirect } from "next/navigation";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile";
+import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
+import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
+import type { Membership } from "@calcom/prisma/client";
+import { MembershipRole } from "@calcom/prisma/enums";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -13,14 +22,47 @@ export const generateMetadata = async () =>
);
const Page = async () => {
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const t = await getTranslate();
+ const orgRole = session?.user.profile?.organization.members?.find(
+ (member: Membership) => member.userId === session?.user.id
+ )?.role;
+
+ if (!session?.user.id || !session?.user.profile?.organizationId || !orgRole) {
+ return redirect("/settings/profile");
+ }
+
+ const { canRead, canEdit, canDelete } = await getResourcePermissions({
+ userId: session.user.id,
+ teamId: session?.user.profile?.organizationId,
+ resource: Resource.Organization,
+ userRole: orgRole,
+ fallbackRoles: {
+ read: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ update: {
+ roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+ },
+ delete: {
+ roles: [MembershipRole.OWNER],
+ },
+ },
+ });
+
return (
-
+
);
};
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 11a55d364c781e..fe8f9ffdd56610 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -871,6 +871,10 @@
"create_team": "Create Team",
"name": "Name",
"nameless_team": "Nameless Team",
+ "oauth_clients": "OAuth Clients",
+ "oauth_clients_description": "Manage OAuth clients for your organization",
+ "create_oauth_client": "Create OAuth Client",
+ "create_oauth_client_description": "Create a new OAuth client for third-party integrations",
"oauth_client_deletion_message": "OAuth client deleted successfully",
"create_new_team_description": "Create a new team to collaborate with users.",
"create_new_team": "Create a new team",
@@ -1836,6 +1840,8 @@
"edit_event_type": "Edit event type",
"only_admin_can_see_members_of_org": "This Organization is private, and only the organization's admin or owner can view its members.",
"only_admin_can_manage_sso_org": "Only the organization's admin or owner can manage SSO settings",
+ "only_admin_can_manage_directory_sync": "Only the organization's admin or owner can manage directory sync settings",
+ "only_admin_can_manage_oauth_clients": "Only the organization's admin or owner can manage OAuth clients",
"collective_scheduling": "Collective Scheduling",
"make_it_easy_to_book": "Make it easy to book your team when everyone is available.",
"find_the_best_person": "Find the best person available and cycle through your team.",
@@ -3302,6 +3308,11 @@
"error_creating_role": "Error creating role",
"error_updating_role": "Error updating role",
"pbac_desc_create_roles": "Create roles",
+ "pbac_resource_attributes": "Attributes",
+ "pbac_desc_view_organization_attributes": "View organization attributes",
+ "pbac_desc_update_organization_attributes": "Update organization attributes",
+ "pbac_desc_delete_organization_attributes": "Delete organization attributes",
+ "pbac_desc_create_organization_attributes": "Create organization attributes",
"pbac_desc_view_roles": "View roles",
"pbac_desc_update_roles": "Update roles",
"pbac_desc_delete_roles": "Delete roles",
diff --git a/packages/features/ee/dsync/page/team-dsync-view.tsx b/packages/features/ee/dsync/page/team-dsync-view.tsx
index c8a453ba2e1a8c..bf8202f3af2e25 100644
--- a/packages/features/ee/dsync/page/team-dsync-view.tsx
+++ b/packages/features/ee/dsync/page/team-dsync-view.tsx
@@ -12,7 +12,7 @@ import { showToast } from "@calcom/ui/components/toast";
import ConfigureDirectorySync from "../components/ConfigureDirectorySync";
// For Hosted Cal - Team view
-const DirectorySync = () => {
+const DirectorySync = ({ permissions }: { permissions?: { canEdit: boolean } }) => {
const { t } = useLocale();
const router = useRouter();
@@ -36,6 +36,10 @@ const DirectorySync = () => {
showToast(error.message, "error");
}
+ if (!permissions?.canEdit) {
+ router.push("/404");
+ }
+
return (
{HOSTED_CAL_FEATURES && }
diff --git a/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx b/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx
index 9e499828e8debd..b1852f86857098 100644
--- a/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx
+++ b/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx
@@ -10,10 +10,9 @@ import { showToast } from "@calcom/ui/components/toast";
interface GeneralViewProps {
currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"];
- isAdminOrOwner: boolean;
}
-export const DisablePhoneOnlySMSNotificationsSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewProps) => {
+export const DisablePhoneOnlySMSNotificationsSwitch = ({ currentOrg }: GeneralViewProps) => {
const { t } = useLocale();
const utils = trpc.useUtils();
const [disablePhoneOnlySMSNotificationsActive, setDisablePhoneOnlySMSNotificationsActive] = useState(
@@ -32,8 +31,6 @@ export const DisablePhoneOnlySMSNotificationsSwitch = ({ currentOrg, isAdminOrOw
},
});
- if (!isAdminOrOwner) return null;
-
return (
<>
{
+export const LockEventTypeSwitch = ({ currentOrg }: GeneralViewProps) => {
const [lockEventTypeCreationForUsers, setLockEventTypeCreationForUsers] = useState(
!!currentOrg.organizationSettings.lockEventTypeCreationForUsers
);
@@ -50,8 +49,6 @@ export const LockEventTypeSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewP
},
});
- if (!isAdminOrOwner) return null;
-
const currentLockedOption = formMethods.watch("currentEventTypeOptions");
const { reset, getValues } = formMethods;
@@ -69,7 +66,7 @@ export const LockEventTypeSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewP
{
@@ -124,9 +121,7 @@ export const LockEventTypeSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewP
-
+
diff --git a/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx b/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx
index 4494251490179d..4d714409df8416 100644
--- a/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx
+++ b/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx
@@ -8,10 +8,9 @@ import { showToast } from "@calcom/ui/components/toast";
interface GeneralViewProps {
currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"];
- isAdminOrOwner: boolean;
}
-export const NoSlotsNotificationSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewProps) => {
+export const NoSlotsNotificationSwitch = ({ currentOrg }: GeneralViewProps) => {
const { t } = useLocale();
const utils = trpc.useUtils();
const [notificationActive, setNotificationActive] = useState(
@@ -30,8 +29,6 @@ export const NoSlotsNotificationSwitch = ({ currentOrg, isAdminOrOwner }: Genera
},
});
- if (!isAdminOrOwner) return null;
-
return (
<>
>;
+ permissions: { canEdit: boolean; canDelete: boolean };
}) {
const { t } = useLocale();
const [isEnabled, setIsEnabled] = useState(attribute.enabled);
@@ -87,44 +89,54 @@ function AttributeItem({
-
-
-
-
-
-
-
- }
+ {(permissions.canEdit || permissions.canDelete) && (
+
+
+
-
- setAttributeToDelete(attribute)}>
- {t("delete")}
-
-
-
-
+ variant="icon"
+ color="secondary"
+ StartIcon="ellipsis"
+ className="ltr:radix-state-open:rounded-r-md rtl:radix-state-open:rounded-l-md"
+ />
+
+
+ {permissions.canEdit && (
+
+
+ {t("edit")}
+
+
+ )}
+ {permissions.canDelete && (
+
+ setAttributeToDelete(attribute)}>
+ {t("delete")}
+
+
+ )}
+
+
+ )}
);
}
-function OrganizationAttributesPage() {
+function OrganizationAttributesPage({
+ permissions,
+}: {
+ permissions: { canEdit: boolean; canDelete: boolean; canCreate: boolean };
+}) {
const { t } = useLocale();
const { data, isLoading } = trpc.viewer.attributes.list.useQuery();
const [attributeToDelete, setAttributeToDelete] = useState();
@@ -149,17 +161,20 @@ function OrganizationAttributesPage() {
))}
-
+ {permissions.canCreate && (
+
+ )}
>
) : (
@@ -173,13 +188,15 @@ function OrganizationAttributesPage() {
{t("add_attributes_description")}
-
+ {permissions.canCreate && (
+
+ )}
)}
diff --git a/packages/features/ee/organizations/pages/settings/general.tsx b/packages/features/ee/organizations/pages/settings/general.tsx
index b4862548c67395..98141bab962bdf 100644
--- a/packages/features/ee/organizations/pages/settings/general.tsx
+++ b/packages/features/ee/organizations/pages/settings/general.tsx
@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
-import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { TimezoneSelect } from "@calcom/features/components/timezone-select";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
@@ -42,22 +41,31 @@ const SkeletonLoader = () => {
interface GeneralViewProps {
currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"];
- isAdminOrOwner: boolean;
localeProp: string;
+
+ permissions: {
+ canRead: boolean;
+ canEdit: boolean;
+ };
}
-const OrgGeneralView = () => {
+const OrgGeneralView = ({
+ permissions,
+}: {
+ permissions: {
+ canRead: boolean;
+ canEdit: boolean;
+ };
+}) => {
const { t } = useLocale();
const router = useRouter();
const session = useSession();
- const isAdminOrOwner = checkAdminOrOwner(session.data?.user?.org?.role);
const {
data: currentOrg,
isPending,
error,
} = trpc.viewer.organizations.listCurrent.useQuery(undefined, {});
- const { data: user } = trpc.viewer.me.get.useQuery();
useEffect(
function refactorMeWithoutEffect() {
@@ -77,18 +85,22 @@ const OrgGeneralView = () => {
-
-
-
+ {permissions.canEdit && (
+ <>
+
+
+
+ >
+ )}
);
};
-const GeneralView = ({ currentOrg, isAdminOrOwner, localeProp }: GeneralViewProps) => {
+const GeneralView = ({ currentOrg, permissions, localeProp }: GeneralViewProps) => {
const { t } = useLocale();
const mutation = trpc.viewer.organizations.update.useMutation({
@@ -136,7 +148,7 @@ const GeneralView = ({ currentOrg, isAdminOrOwner, localeProp }: GeneralViewProp
reset,
getValues,
} = formMethods;
- const isDisabled = isSubmitting || !isDirty || !isAdminOrOwner;
+ const isDisabled = isSubmitting || !isDirty || !permissions.canEdit;
return (