diff --git a/.env.example b/.env.example index 3c1e86a566..428a8a016b 100644 --- a/.env.example +++ b/.env.example @@ -211,5 +211,5 @@ UNKEY_ROOT_KEY= # It's used automatically by Sentry during the build for authentication when uploading source maps. # SENTRY_AUTH_TOKEN= -# Disable the user management from UI -# DISABLE_USER_MANAGEMENT=1 \ No newline at end of file +# Configure the minimum role for user management from UI(owner, manager, disabled) +# USER_MANAGEMENT_MINIMUM_ROLE="manager" \ No newline at end of file diff --git a/apps/web/lib/constants.ts b/apps/web/lib/constants.ts index 7c1877f808..b72f0d8901 100644 --- a/apps/web/lib/constants.ts +++ b/apps/web/lib/constants.ts @@ -282,4 +282,4 @@ export const SENTRY_DSN = env.SENTRY_DSN; export const PROMETHEUS_ENABLED = env.PROMETHEUS_ENABLED === "1"; -export const DISABLE_USER_MANAGEMENT = env.DISABLE_USER_MANAGEMENT === "1"; +export const USER_MANAGEMENT_MINIMUM_ROLE = env.USER_MANAGEMENT_MINIMUM_ROLE ?? "manager"; diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts index c7dc1fd3af..56d1ce8b7b 100644 --- a/apps/web/lib/env.ts +++ b/apps/web/lib/env.ts @@ -104,7 +104,7 @@ export const env = createEnv({ NODE_ENV: z.enum(["development", "production", "test"]).optional(), PROMETHEUS_EXPORTER_PORT: z.string().optional(), PROMETHEUS_ENABLED: z.enum(["1", "0"]).optional(), - DISABLE_USER_MANAGEMENT: z.enum(["1", "0"]).optional(), + USER_MANAGEMENT_MINIMUM_ROLE: z.enum(["owner", "manager", "disabled"]).optional(), }, /* @@ -199,6 +199,6 @@ export const env = createEnv({ NODE_ENV: process.env.NODE_ENV, PROMETHEUS_ENABLED: process.env.PROMETHEUS_ENABLED, PROMETHEUS_EXPORTER_PORT: process.env.PROMETHEUS_EXPORTER_PORT, - DISABLE_USER_MANAGEMENT: process.env.DISABLE_USER_MANAGEMENT, + USER_MANAGEMENT_MINIMUM_ROLE: process.env.USER_MANAGEMENT_MINIMUM_ROLE, }, }); diff --git a/apps/web/lib/membership/utils.ts b/apps/web/lib/membership/utils.ts index 404cf3829f..7b9596ee04 100644 --- a/apps/web/lib/membership/utils.ts +++ b/apps/web/lib/membership/utils.ts @@ -13,3 +13,21 @@ export const getAccessFlags = (role?: TOrganizationRole) => { isMember, }; }; + +export const getUserManagementAccess = ( + role: TOrganizationRole, + minimumRole: "owner" | "manager" | "disabled" +): boolean => { + // If minimum role is "disabled", no one has access + if (minimumRole === "disabled") { + return false; + } + if (minimumRole === "owner") { + return role === "owner"; + } + + if (minimumRole === "manager") { + return role === "owner" || role === "manager"; + } + return false; +}; diff --git a/apps/web/modules/ee/role-management/actions.test.ts b/apps/web/modules/ee/role-management/actions.test.ts index 3288dbdc75..e9c40d58f8 100644 --- a/apps/web/modules/ee/role-management/actions.test.ts +++ b/apps/web/modules/ee/role-management/actions.test.ts @@ -15,14 +15,14 @@ import { AuthenticationError, OperationNotAllowedError, ValidationError } from " // Mock constants with getter functions to allow overriding in tests let mockIsFormbricksCloud = false; -let mockDisableUserManagement = false; +let mockUserManagementMinimumRole = "owner"; vi.mock("@/lib/constants", () => ({ get IS_FORMBRICKS_CLOUD() { return mockIsFormbricksCloud; }, - get DISABLE_USER_MANAGEMENT() { - return mockDisableUserManagement; + get USER_MANAGEMENT_MINIMUM_ROLE() { + return mockUserManagementMinimumRole; }, })); @@ -62,7 +62,7 @@ describe("Role Management Actions", () => { afterEach(() => { vi.resetAllMocks(); mockIsFormbricksCloud = false; - mockDisableUserManagement = false; + mockUserManagementMinimumRole = "owner"; }); describe("checkRoleManagementPermission", () => { @@ -220,7 +220,7 @@ describe("Role Management Actions", () => { test("throws error if user management is disabled", async () => { vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any); - mockDisableUserManagement = true; + mockUserManagementMinimumRole = "disabled"; await expect( updateMembershipAction({ @@ -231,12 +231,12 @@ describe("Role Management Actions", () => { data: { role: "member" }, }, } as any) - ).rejects.toThrow(new OperationNotAllowedError("User management is disabled")); + ).rejects.toThrow(new OperationNotAllowedError("User management is not allowed for your role")); }); test("throws error if billing role is not allowed in self-hosted", async () => { vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any); - mockDisableUserManagement = false; + mockUserManagementMinimumRole = "owner"; vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); await expect( @@ -253,7 +253,7 @@ describe("Role Management Actions", () => { test("allows billing role in cloud environment", async () => { vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any); - mockDisableUserManagement = false; + mockUserManagementMinimumRole = "owner"; mockIsFormbricksCloud = true; vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any); @@ -274,7 +274,7 @@ describe("Role Management Actions", () => { test("throws error if manager tries to assign a role other than member", async () => { vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "manager" } as any); - mockDisableUserManagement = false; + mockUserManagementMinimumRole = "manager"; vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); await expect( @@ -291,7 +291,7 @@ describe("Role Management Actions", () => { test("allows manager to assign member role", async () => { vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "manager" } as any); - mockDisableUserManagement = false; + mockUserManagementMinimumRole = "manager"; vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any); vi.mocked(getRoleManagementPermission).mockResolvedValue(true); @@ -312,7 +312,7 @@ describe("Role Management Actions", () => { test("successful membership update as owner", async () => { vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any); - mockDisableUserManagement = false; + mockUserManagementMinimumRole = "owner"; vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any); vi.mocked(getRoleManagementPermission).mockResolvedValue(true); diff --git a/apps/web/modules/ee/role-management/actions.ts b/apps/web/modules/ee/role-management/actions.ts index cc82f81f8d..254b994959 100644 --- a/apps/web/modules/ee/role-management/actions.ts +++ b/apps/web/modules/ee/role-management/actions.ts @@ -1,7 +1,8 @@ "use server"; -import { DISABLE_USER_MANAGEMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { IS_FORMBRICKS_CLOUD, USER_MANAGEMENT_MINIMUM_ROLE } from "@/lib/constants"; import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; +import { getUserManagementAccess } from "@/lib/membership/utils"; import { getOrganization } from "@/lib/organization/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; @@ -87,8 +88,13 @@ export const updateMembershipAction = authenticatedActionClient if (!currentUserMembership) { throw new AuthenticationError("User not a member of this organization"); } - if (DISABLE_USER_MANAGEMENT) { - throw new OperationNotAllowedError("User management is disabled"); + const hasUserManagementAccess = getUserManagementAccess( + currentUserMembership.role, + USER_MANAGEMENT_MINIMUM_ROLE + ); + + if (!hasUserManagementAccess) { + throw new OperationNotAllowedError("User management is not allowed for your role"); } await checkAuthorizationUpdated({ diff --git a/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts b/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts deleted file mode 100644 index e645309ddb..0000000000 --- a/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { TInviteUpdateInput } from "@/modules/ee/role-management/types/invites"; -import { Session } from "next-auth"; -import { TMembership, TMembershipUpdateInput } from "@formbricks/types/memberships"; -import { TOrganization, TOrganizationBillingPlan } from "@formbricks/types/organizations"; -import { TUser } from "@formbricks/types/user"; - -// Common mock IDs -export const mockOrganizationId = "cblt7dwr7d0hvdifl4iw6d5x"; -export const mockUserId = "wl43gybf3pxmqqx3fcmsk8eb"; -export const mockInviteId = "dc0b6ea6-bb65-4a22-88e1-847df2e85af4"; -export const mockTargetUserId = "vevt9qm7sqmh44e3za6a2vzd"; - -// Mock user -export const mockUser: TUser = { - id: mockUserId, - name: "Test User", - email: "test@example.com", - emailVerified: new Date(), - createdAt: new Date(), - updatedAt: new Date(), - identityProvider: "email", - twoFactorEnabled: false, - objective: null, - notificationSettings: { - alert: {}, - weeklySummary: {}, - }, - locale: "en-US", - imageUrl: null, - role: null, - lastLoginAt: new Date(), - isActive: true, -}; - -// Mock session -export const mockSession: Session = { - user: { - id: mockUserId, - }, - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), -}; - -// Mock organizations -export const createMockOrganization = (plan: TOrganizationBillingPlan): TOrganization => ({ - id: mockOrganizationId, - name: "Test Organization", - createdAt: new Date(), - updatedAt: new Date(), - isAIEnabled: false, - billing: { - stripeCustomerId: null, - plan, - period: "monthly", - periodStart: new Date(), - limits: { - projects: plan === "free" ? 3 : null, - monthly: { - responses: plan === "free" ? 1500 : null, - miu: plan === "free" ? 2000 : null, - }, - }, - }, -}); - -export const mockOrganizationFree = createMockOrganization("free"); -export const mockOrganizationStartup = createMockOrganization("startup"); -export const mockOrganizationScale = createMockOrganization("scale"); - -// Mock membership data -export const createMockMembership = (role: TMembership["role"]): TMembership => ({ - userId: mockUserId, - organizationId: mockOrganizationId, - role, - accepted: true, -}); - -export const mockMembershipMember = createMockMembership("member"); -export const mockMembershipManager = createMockMembership("manager"); -export const mockMembershipOwner = createMockMembership("owner"); - -// Mock data payloads -export const mockInviteDataMember: TInviteUpdateInput = { role: "member" }; -export const mockInviteDataOwner: TInviteUpdateInput = { role: "owner" }; -export const mockInviteDataBilling: TInviteUpdateInput = { role: "billing" }; - -export const mockMembershipUpdateMember: TMembershipUpdateInput = { role: "member" }; -export const mockMembershipUpdateOwner: TMembershipUpdateInput = { role: "owner" }; -export const mockMembershipUpdateBilling: TMembershipUpdateInput = { role: "billing" }; - -// Mock input objects for actions -export const mockUpdateInviteInput = { - inviteId: mockInviteId, - organizationId: mockOrganizationId, - data: mockInviteDataMember, -}; - -export const mockUpdateMembershipInput = { - userId: mockTargetUserId, - organizationId: mockOrganizationId, - data: mockMembershipUpdateMember, -}; - -// Mock responses -export const mockUpdatedMembership: TMembership = { - userId: mockTargetUserId, - organizationId: mockOrganizationId, - role: "member", - accepted: true, -}; diff --git a/apps/web/modules/ee/role-management/tests/actions.test.ts b/apps/web/modules/ee/role-management/tests/actions.test.ts deleted file mode 100644 index c5ef6a2b4d..0000000000 --- a/apps/web/modules/ee/role-management/tests/actions.test.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { - mockInviteDataBilling, - mockInviteDataOwner, - mockMembershipManager, - mockMembershipMember, - mockMembershipUpdateBilling, - mockMembershipUpdateOwner, - mockOrganizationFree, - mockOrganizationId, - mockOrganizationScale, - mockOrganizationStartup, - mockSession, - mockUpdateInviteInput, - mockUpdateMembershipInput, - mockUpdatedMembership, - mockUser, -} from "./__mocks__/actions.mock"; -import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; -import { getOrganization } from "@/lib/organization/service"; -import { getUser } from "@/lib/user/service"; -import "@/lib/utils/action-client-middleware"; -import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; -import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; -import { updateInvite } from "@/modules/ee/role-management/lib/invite"; -import { updateMembership } from "@/modules/ee/role-management/lib/membership"; -import { getServerSession } from "next-auth"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { OperationNotAllowedError, ValidationError } from "@formbricks/types/errors"; -import { checkRoleManagementPermission } from "../actions"; -import { updateInviteAction, updateMembershipAction } from "../actions"; - -// Mock all external dependencies -vi.mock("@/modules/ee/license-check/lib/utils", () => ({ - getRoleManagementPermission: vi.fn(), -})); - -vi.mock("@/modules/ee/role-management/lib/invite", () => ({ - updateInvite: vi.fn(), -})); - -vi.mock("@/lib/user/service", () => ({ - getUser: vi.fn(), -})); - -vi.mock("@/modules/ee/role-management/lib/membership", () => ({ - updateMembership: vi.fn(), -})); - -vi.mock("@/lib/membership/service", () => ({ - getMembershipByUserIdOrganizationId: vi.fn(), -})); - -vi.mock("@/lib/organization/service", () => ({ - getOrganization: vi.fn(), -})); - -vi.mock("@/lib/utils/action-client-middleware", () => ({ - checkAuthorizationUpdated: vi.fn(), -})); - -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - -// Mock constants without importing the actual module -vi.mock("@/lib/constants", () => ({ - IS_FORMBRICKS_CLOUD: false, - IS_MULTI_ORG_ENABLED: true, - ENCRYPTION_KEY: "test-encryption-key", - ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key", - GITHUB_ID: "test-github-id", - GITHUB_SECRET: "test-github-secret", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azure-client-id", - AZUREAD_CLIENT_SECRET: "test-azure-client-secret", - AZUREAD_TENANT_ID: "test-azure-tenant-id", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_SIGNING_ALGORITHM: "test-oidc-algorithm", - SAML_DATABASE_URL: "test-saml-db-url", - NEXTAUTH_SECRET: "test-nextauth-secret", - WEBAPP_URL: "http://localhost:3000", - DISABLE_USER_MANAGEMENT: false, -})); - -vi.mock("@/lib/utils/action-client-middleware", () => ({ - checkAuthorizationUpdated: vi.fn(), -})); -vi.mock("@/lib/errors", () => ({ - OperationNotAllowedError: vi.fn(), - ValidationError: vi.fn(), -})); - -describe("role-management/actions.ts", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - describe("checkRoleManagementPermission", () => { - test("throws error when organization not found", async () => { - vi.mocked(getOrganization).mockResolvedValue(null); - - await expect(checkRoleManagementPermission(mockOrganizationId)).rejects.toThrow( - "Organization not found" - ); - - expect(getOrganization).toHaveBeenCalledWith(mockOrganizationId); - }); - - test("throws error when role management is not allowed", async () => { - vi.mocked(getOrganization).mockResolvedValue(mockOrganizationFree); - vi.mocked(getRoleManagementPermission).mockResolvedValue(false); - - await expect(checkRoleManagementPermission(mockOrganizationId)).rejects.toThrow( - new OperationNotAllowedError("Role management is not allowed for this organization") - ); - - expect(getRoleManagementPermission).toHaveBeenCalledWith("free"); - - expect(getOrganization).toHaveBeenCalledWith(mockOrganizationId); - }); - - test("succeeds when role management is allowed", async () => { - vi.mocked(getOrganization).mockResolvedValue(mockOrganizationStartup); - vi.mocked(getRoleManagementPermission).mockResolvedValue(true); - - await expect(checkRoleManagementPermission(mockOrganizationId)).resolves.toBeUndefined(); - await expect(getRoleManagementPermission).toHaveBeenCalledWith("startup"); - expect(getOrganization).toHaveBeenCalledWith(mockOrganizationId); - }); - }); - - describe("updateInviteAction", () => { - test("throws error when user is not a member of the organization", async () => { - vi.mocked(getServerSession).mockResolvedValue(mockSession); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null); - - expect(await updateInviteAction(mockUpdateInviteInput)).toStrictEqual({ - serverError: "User not a member of this organization", - }); - }); - - test("throws error when billing role is not allowed in self-hosted", async () => { - const inputWithBillingRole = { - ...mockUpdateInviteInput, - data: mockInviteDataBilling, - }; - - vi.mocked(getServerSession).mockResolvedValue(mockSession); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipMember); - vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); - - expect(await updateInviteAction(inputWithBillingRole)).toStrictEqual({ - serverError: "Something went wrong while executing the operation.", - }); - }); - - test("throws error when manager tries to assign non-member role", async () => { - const inputWithOwnerRole = { - ...mockUpdateInviteInput, - data: mockInviteDataOwner, - }; - - vi.mocked(getServerSession).mockResolvedValue(mockSession); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipManager); - vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); - - expect(await updateInviteAction(inputWithOwnerRole)).toStrictEqual({ - serverError: "Managers can only invite members", - }); - }); - - test("successfully updates invite", async () => { - vi.mocked(getServerSession).mockResolvedValue(mockSession); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipManager); - vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); - vi.mocked(getOrganization).mockResolvedValue(mockOrganizationScale); - vi.mocked(getRoleManagementPermission).mockResolvedValue(true); - vi.mocked(updateInvite).mockResolvedValue(true); - - const result = await updateInviteAction(mockUpdateInviteInput); - - expect(result).toEqual({ data: true }); - }); - }); - - describe("updateMembershipAction", () => { - test("throws error when user is not a member of the organization", async () => { - vi.mocked(getServerSession).mockResolvedValue(mockSession); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null); - - expect(await updateMembershipAction(mockUpdateMembershipInput)).toStrictEqual({ - serverError: "User not a member of this organization", - }); - }); - - test("throws error when billing role is not allowed in self-hosted", async () => { - const inputWithBillingRole = { - ...mockUpdateMembershipInput, - data: mockMembershipUpdateBilling, - }; - - vi.mocked(getServerSession).mockResolvedValue(mockSession); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipMember); - vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); - - expect(await updateMembershipAction(inputWithBillingRole)).toStrictEqual({ - serverError: "Something went wrong while executing the operation.", - }); - }); - - test("throws error when manager tries to assign non-member role", async () => { - const inputWithOwnerRole = { - ...mockUpdateMembershipInput, - data: mockMembershipUpdateOwner, - }; - - vi.mocked(getServerSession).mockResolvedValue(mockSession); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipManager); - vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); - - expect(await updateMembershipAction(inputWithOwnerRole)).toStrictEqual({ - serverError: "Managers can only assign users to the member role", - }); - }); - - test("successfully updates membership", async () => { - vi.mocked(getServerSession).mockResolvedValue(mockSession); - vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipManager); - vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true); - vi.mocked(getOrganization).mockResolvedValue(mockOrganizationScale); - vi.mocked(getRoleManagementPermission).mockResolvedValue(true); - vi.mocked(updateMembership).mockResolvedValue(mockUpdatedMembership); - - const result = await updateMembershipAction(mockUpdateMembershipInput); - - expect(result).toEqual({ - data: mockUpdatedMembership, - }); - }); - }); -}); diff --git a/apps/web/modules/organization/settings/teams/page.test.tsx b/apps/web/modules/organization/settings/teams/page.test.tsx index 0c47f2e7c6..2fc8099030 100644 --- a/apps/web/modules/organization/settings/teams/page.test.tsx +++ b/apps/web/modules/organization/settings/teams/page.test.tsx @@ -13,7 +13,7 @@ vi.mock( ); vi.mock("@/lib/constants", () => ({ - DISABLE_USER_MANAGEMENT: 0, + USER_MANAGEMENT_MINIMUM_ROLE: "owner", IS_FORMBRICKS_CLOUD: 1, ENCRYPTION_KEY: "test-key", ENTERPRISE_LICENSE_KEY: "test-enterprise-key", diff --git a/apps/web/modules/organization/settings/teams/page.tsx b/apps/web/modules/organization/settings/teams/page.tsx index a1377685ee..9be90f8440 100644 --- a/apps/web/modules/organization/settings/teams/page.tsx +++ b/apps/web/modules/organization/settings/teams/page.tsx @@ -1,5 +1,6 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; -import { DISABLE_USER_MANAGEMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { IS_FORMBRICKS_CLOUD, USER_MANAGEMENT_MINIMUM_ROLE } from "@/lib/constants"; +import { getUserManagementAccess } from "@/lib/membership/utils"; import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; import { TeamsView } from "@/modules/ee/teams/team-list/components/teams-view"; import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; @@ -15,6 +16,10 @@ export const TeamsPage = async (props) => { const { session, currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId); const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan); + const hasUserManagementAccess = getUserManagementAccess( + currentUserMembership?.role, + USER_MANAGEMENT_MINIMUM_ROLE + ); return ( @@ -32,7 +37,7 @@ export const TeamsPage = async (props) => { currentUserId={session.user.id} environmentId={params.environmentId} canDoRoleManagement={canDoRoleManagement} - isUserManagementDisabledFromUi={DISABLE_USER_MANAGEMENT} + isUserManagementDisabledFromUi={!hasUserManagementAccess} />