diff --git a/.changeset/little-steaks-itch.md b/.changeset/little-steaks-itch.md new file mode 100644 index 0000000000000..ee04b7a333402 --- /dev/null +++ b/.changeset/little-steaks-itch.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes incorrect permission checks on workspace registration status, aligning the API and UI hooks with manage-cloud access. diff --git a/apps/meteor/app/api/server/v1/cloud.ts b/apps/meteor/app/api/server/v1/cloud.ts index 369169987c100..acf62e7f1bb62 100644 --- a/apps/meteor/app/api/server/v1/cloud.ts +++ b/apps/meteor/app/api/server/v1/cloud.ts @@ -2,7 +2,7 @@ import { isCloudConfirmationPollProps, isCloudCreateRegistrationIntentProps, isC import { CloudWorkspaceRegistrationError } from '../../../../lib/errors/CloudWorkspaceRegistrationError'; import { SystemLogger } from '../../../../server/lib/logger/system'; -import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { getCheckoutUrl } from '../../../cloud/server/functions/getCheckoutUrl'; import { getConfirmationPoll } from '../../../cloud/server/functions/getConfirmationPoll'; import { @@ -88,7 +88,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - if (!(await hasRoleAsync(this.userId, 'admin'))) { + if (!(await hasPermissionAsync(this.userId, 'manage-cloud'))) { return API.v1.forbidden(); } diff --git a/apps/meteor/client/hooks/useRegistrationStatus.spec.ts b/apps/meteor/client/hooks/useRegistrationStatus.spec.ts new file mode 100644 index 0000000000000..e602f541e96eb --- /dev/null +++ b/apps/meteor/client/hooks/useRegistrationStatus.spec.ts @@ -0,0 +1,68 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useRegistrationStatus } from './useRegistrationStatus'; + +describe('useRegistrationStatus', () => { + it('should not call API and return error state when user does not have manage-cloud permission', async () => { + const mockGetRegistrationStatus = jest.fn(); + + const { result } = renderHook(() => useRegistrationStatus(), { + wrapper: mockAppRoot().withEndpoint('GET', '/v1/cloud.registrationStatus', mockGetRegistrationStatus).withJohnDoe().build(), + }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.canViewRegistrationStatus).toBe(false); + expect(result.current.isRegistered).toBeFalsy(); + expect(mockGetRegistrationStatus).not.toHaveBeenCalled(); + }); + + it('should call API and return isRegistered as true and canViewRegistrationStatus as true when workspace is registered and user has manage-cloud permission', async () => { + const mockGetRegistrationStatus = jest.fn().mockResolvedValue({ + registrationStatus: { + workspaceRegistered: true, + }, + }); + + const { result } = renderHook(() => useRegistrationStatus(), { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/cloud.registrationStatus', mockGetRegistrationStatus) + .withPermission('manage-cloud') + .withJohnDoe() + .build(), + }); + + await waitFor(() => { + expect(mockGetRegistrationStatus).toHaveBeenCalled(); + }); + + expect(result.current.isRegistered).toBe(true); + expect(result.current.canViewRegistrationStatus).toBe(true); + }); + + it('should call API, return isRegistered as false and canViewRegistrationStatus as true when workspace is not registered and user has manage-cloud permission', async () => { + const mockGetRegistrationStatus = jest.fn().mockResolvedValue({ + registrationStatus: { + workspaceRegistered: false, + }, + }); + + const { result } = renderHook(() => useRegistrationStatus(), { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/cloud.registrationStatus', mockGetRegistrationStatus) + .withPermission('manage-cloud') + .withJohnDoe() + .build(), + }); + + await waitFor(() => { + expect(mockGetRegistrationStatus).toHaveBeenCalled(); + }); + + expect(result.current.isRegistered).toBe(false); + expect(result.current.canViewRegistrationStatus).toBe(true); + }); +}); diff --git a/apps/meteor/client/hooks/useRegistrationStatus.ts b/apps/meteor/client/hooks/useRegistrationStatus.ts index 34bcf31ae38cb..522d493caeff6 100644 --- a/apps/meteor/client/hooks/useRegistrationStatus.ts +++ b/apps/meteor/client/hooks/useRegistrationStatus.ts @@ -4,17 +4,18 @@ import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; type useRegistrationStatusReturnType = { + canViewRegistrationStatus: boolean; isRegistered?: boolean; } & UseQueryResult>; export const useRegistrationStatus = (): useRegistrationStatusReturnType => { const getRegistrationStatus = useEndpoint('GET', '/v1/cloud.registrationStatus'); - const canViewregistrationStatus = usePermission('manage-cloud'); + const canViewRegistrationStatus = usePermission('manage-cloud'); const queryResult = useQuery({ queryKey: ['getRegistrationStatus'], queryFn: () => { - if (!canViewregistrationStatus) { + if (!canViewRegistrationStatus) { throw new Error('unauthorized api call'); } return getRegistrationStatus(); @@ -22,5 +23,9 @@ export const useRegistrationStatus = (): useRegistrationStatusReturnType => { staleTime: Infinity, }); - return { isRegistered: !queryResult.isPending && queryResult.data?.registrationStatus?.workspaceRegistered, ...queryResult }; + return { + canViewRegistrationStatus, + isRegistered: !queryResult.isPending && queryResult.data?.registrationStatus?.workspaceRegistered, + ...queryResult, + }; }; diff --git a/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx b/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx index 2efa553b291eb..f3f34299556b4 100644 --- a/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx +++ b/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx @@ -54,7 +54,7 @@ const SubscriptionPage = () => { const showLicense = useShowLicense(); const router = useRouter(); const { data: enterpriseData } = useIsEnterprise(); - const { isRegistered } = useRegistrationStatus(); + const { canViewRegistrationStatus } = useRegistrationStatus(); const { data: licensesData, isLoading: isLicenseLoading } = useLicenseWithCloudAnnouncement({ loadValues: true }); const syncLicenseUpdate = useWorkspaceSync(); const invalidateLicenseQuery = useInvalidateLicense(); @@ -108,7 +108,7 @@ const SubscriptionPage = () => { - {isRegistered && ( + {canViewRegistrationStatus && ( diff --git a/apps/meteor/client/views/admin/workspace/VersionCard/VersionCard.tsx b/apps/meteor/client/views/admin/workspace/VersionCard/VersionCard.tsx index a0cfdef382c20..398bab1b684ad 100644 --- a/apps/meteor/client/views/admin/workspace/VersionCard/VersionCard.tsx +++ b/apps/meteor/client/views/admin/workspace/VersionCard/VersionCard.tsx @@ -47,7 +47,7 @@ const VersionCard = ({ serverInfo }: VersionCardProps): ReactElement => { const formatDate = useFormatDate(); const { data: licenseData, isPending, refetch: refetchLicense } = useLicense({ loadValues: true }); - const { isRegistered } = useRegistrationStatus(); + const { isRegistered, canViewRegistrationStatus } = useRegistrationStatus(); const { license, limits } = licenseData || {}; const isAirgapped = license?.information?.offline; @@ -82,7 +82,7 @@ const VersionCard = ({ serverInfo }: VersionCardProps): ReactElement => { action: () => void; label: ReactNode; } = useMemo(() => { - if (!isRegistered) { + if (canViewRegistrationStatus && !isRegistered) { return { action: () => { const handleModalClose = (): void => { @@ -107,7 +107,7 @@ const VersionCard = ({ serverInfo }: VersionCardProps): ReactElement => { if (isOverLimits) { return { path: '/admin/subscription', label: t('Manage_subscription') }; } - }, [isRegistered, versionStatus, isOverLimits, t, setModal, refetchLicense]); + }, [canViewRegistrationStatus, isRegistered, versionStatus, isOverLimits, t, setModal, refetchLicense]); const actionItems = useMemo(() => { return ( @@ -154,19 +154,30 @@ const VersionCard = ({ serverInfo }: VersionCardProps): ReactElement => { ), }, - isRegistered - ? { - icon: 'check', - label: t('Workspace_registered'), - } - : { - danger: true, - icon: 'warning', - label: t('Workspace_not_registered'), - }, + canViewRegistrationStatus && + (isRegistered + ? { + icon: 'check', + label: t('Workspace_registered'), + } + : { + danger: true, + icon: 'warning', + label: t('Workspace_not_registered'), + }), ].filter(Boolean) as VersionActionItem[] ).sort((a) => (a.danger ? -1 : 1)); - }, [isOverLimits, t, isAirgapped, versions, versionStatus?.label, versionStatus?.expiration, formatDate, isRegistered]); + }, [ + isOverLimits, + t, + isAirgapped, + versions, + versionStatus?.label, + versionStatus?.expiration, + formatDate, + canViewRegistrationStatus, + isRegistered, + ]); if (isPending && !licenseData) { return ( diff --git a/apps/meteor/tests/end-to-end/api/cloud.ts b/apps/meteor/tests/end-to-end/api/cloud.ts index dc6d08e990925..ab6ce13be0bd0 100644 --- a/apps/meteor/tests/end-to-end/api/cloud.ts +++ b/apps/meteor/tests/end-to-end/api/cloud.ts @@ -178,4 +178,50 @@ describe('[Cloud]', function () { }); }); }); + + describe('[/cloud.registrationStatus]', () => { + before(async () => { + return updatePermission('manage-cloud', ['admin']); + }); + + after(async () => { + return updatePermission('manage-cloud', ['admin']); + }); + + it('should fail if user is not authenticated', async () => { + return request + .get(api('cloud.registrationStatus')) + .expect('Content-Type', 'application/json') + .expect(401) + .expect((res: Response) => { + expect(res.body).to.have.property('status', 'error'); + expect(res.body).to.have.property('message', 'You must be logged in to do this.'); + }); + }); + + it('should fail when user does not have the manage-cloud permission', async () => { + await updatePermission('manage-cloud', []); + return request + .get(api('cloud.registrationStatus')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + }); + }); + + it('should return registration status when user has the manage-cloud permission', async () => { + await updatePermission('manage-cloud', ['admin']); + return request + .get(api('cloud.registrationStatus')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('registrationStatus'); + }); + }); + }); });