From 2a7c6590a3d2b20f28faa4be8f8c02c58d412995 Mon Sep 17 00:00:00 2001
From: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Date: Wed, 26 Nov 2025 13:58:29 +0000
Subject: [PATCH] feat: add permission for editUsers + implement UI (#25402)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## What does this PR do?
This PR implements attributes PBAC - router checks + UI
## Visual Demo (For contributors especially)
A visual demonstration is strongly recommended, for both the original and new change **(video / image - any one)**.
#### Video Demo (if applicable):
- Show screen recordings of the issue or feature.
- Demonstrate how to reproduce the issue, the behavior before and after the change.
#### Image Demo (if applicable):
- Add side-by-side screenshots of the original and updated change.
- Highlight any significant change(s).
## Mandatory Tasks (DO NOT REMOVE)
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [ ] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox.
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.
## How should this be tested?
Enable PBAC on an org
Create a custom role -> advanced -> organizations -> "editUser"
Assign it to a user
impersonate user
test they have access to all things attributes
remove permissions
check they dont have permissions.
## Checklist
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my changes generate no new warnings
---
## Summary by cubic
Adds a new PBAC “editUsers” permission and read gating for Attributes, updating the UI and backend so users can view and edit attributes only when allowed.
- **New Features**
- Added CustomAction.EditUsers to the permission registry for organization-scoped attribute editing.
- Settings computes canViewAttributes and shows the Attributes tab only when allowed.
- Members page fetches Attributes permissions and exposes canViewAttributes and canEditAttributesForUser to the UI.
- User Edit Sheet hides attributes without read permission and shows attribute editing and the bulk “Mass Assign Attributes” action only with editUsers; other user edits depend on changeMemberRole.
- Attributes TRPC router gates create/edit/delete/toggle via PBAC and requires organization.attributes.editUsers for assign/bulk-assign; added a helper to create PBAC-aware procedures.
- **Migration**
- Run the new Prisma migration to seed the admin role with editUsers.
- If using custom roles, grant Edit Users under Attributes as needed.
Written for commit 856aa2e8e1521fc22cd71cb0fa6d720036efe8a2. Summary will update automatically on new commits.
---
.../SettingsLayoutAppDirClient.tsx | 25 ++---
.../settings/(settings-layout)/layout.tsx | 9 +-
.../(org-user-only)/members/page.tsx | 97 +++++++++++--------
apps/web/public/static/locales/en/common.json | 2 +
.../pbac/domain/types/permission-registry.ts | 18 ++++
.../UserTable/EditSheet/EditUserForm.tsx | 10 +-
.../UserTable/EditSheet/EditUserSheet.tsx | 22 +++--
.../EditSheet/SheetFooterControls.tsx | 54 ++++++++---
.../components/UserTable/UserListTable.tsx | 23 +++--
.../users/components/UserTable/types.ts | 2 +
.../migration.sql | 10 ++
.../routers/viewer/attributes/_router.ts | 47 +++++----
.../server/routers/viewer/attributes/util.ts | 46 +++++++++
13 files changed, 266 insertions(+), 99 deletions(-)
create mode 100644 packages/prisma/migrations/20251126085109_pbac_add_attributes_edit_users_permission/migration.sql
create mode 100644 packages/trpc/server/routers/viewer/attributes/util.ts
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx
index 674fa89970..0ca9440c30 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx
@@ -181,6 +181,7 @@ export interface SettingsPermissions {
canViewRoles?: boolean;
canViewOrganizationBilling?: boolean;
canUpdateOrganization?: boolean;
+ canViewAttributes?: boolean;
}
const useTabs = ({
@@ -211,7 +212,7 @@ const useTabs = ({
(child) => permissions?.canUpdateOrganization || !organizationAdminKeys.includes(child.name)
);
- if (permissions?.canUpdateOrganization) {
+ if (permissions?.canViewAttributes) {
newArray.splice(4, 0, {
name: "attributes",
href: "/settings/organizations/attributes",
@@ -452,7 +453,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record
)}
@@ -460,7 +461,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record
{/* Show roles only for sub-teams with PBAC-enabled parent */}
@@ -481,7 +482,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record
{/* Hide if there is a parent ID */}
@@ -491,7 +492,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record
>
@@ -500,7 +501,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record
>
@@ -578,7 +579,7 @@ const SettingsSidebarContainer = ({
@@ -772,14 +773,14 @@ const SettingsSidebarContainer = ({
name={t("profile")}
href={`/settings/organizations/teams/other/${otherTeam.id}/profile`}
textClassNames="px-3 text-emphasis font-medium text-sm"
- className="me-5 h-7 w-auto px-2!"
+ className="px-2! me-5 h-7 w-auto"
disableChevron
/>
<>
@@ -881,7 +882,7 @@ export default function SettingsLayoutAppDirClient({
TopNavContainer={
setSideContainerOpen(!sideContainerOpen)} />
}>
-