From b4c9c92a25f9fba93c0bc2b75d7e01f36a951563 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Tue, 10 Mar 2026 17:33:31 +0200 Subject: [PATCH 1/3] Add permision check for tables --- .../src/app/features/navigation/lib/menu-links-hook.ts | 1 + .../react-ui/src/app/routes/openops-tables/index.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/app/features/navigation/lib/menu-links-hook.ts b/packages/react-ui/src/app/features/navigation/lib/menu-links-hook.ts index 6ff8a85565..2b0afd5908 100644 --- a/packages/react-ui/src/app/features/navigation/lib/menu-links-hook.ts +++ b/packages/react-ui/src/app/features/navigation/lib/menu-links-hook.ts @@ -55,6 +55,7 @@ export const useMenuLinks = () => { to: '/tables', label: t('Tables'), icon: TableProperties, + locked: !checkAccess(Permission.WRITE_TABLE), }, ...(hasAnalyticsAccess ? [ diff --git a/packages/react-ui/src/app/routes/openops-tables/index.tsx b/packages/react-ui/src/app/routes/openops-tables/index.tsx index 5e13b39e96..bb7b53504d 100644 --- a/packages/react-ui/src/app/routes/openops-tables/index.tsx +++ b/packages/react-ui/src/app/routes/openops-tables/index.tsx @@ -1,13 +1,15 @@ import { t } from 'i18next'; -import { useLocation } from 'react-router-dom'; +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuthorization } from '@/app/common/hooks/authorization-hooks'; import { flagsHooks } from '@/app/common/hooks/flags-hooks'; import { useDefaultSidebarState } from '@/app/common/hooks/use-default-sidebar-state'; import { useCandu } from '@/app/features/extensions/candu/use-candu'; -import { FlagId } from '@openops/shared'; +import { FlagId, Permission } from '@openops/shared'; const OpenOpsTablesPage = () => { useDefaultSidebarState('minimized'); + const { checkAccess } = useAuthorization(); const { isCanduEnabled, canduClientToken, canduUserId } = useCandu(); const parentData = encodeURIComponent( JSON.stringify({ isCanduEnabled, userId: canduUserId, canduClientToken }), @@ -22,6 +24,10 @@ const OpenOpsTablesPage = () => { FlagId.OPENOPS_TABLES_PUBLIC_URL, ); + if (!checkAccess(Permission.WRITE_TABLE)) { + return ; + } + if (!openopsTablesUrl) { console.error('OpenOps Tables URL is not defined'); return null; From b640a07eb099623713c07ec41c0192a70965b130 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Tue, 10 Mar 2026 17:37:08 +0200 Subject: [PATCH 2/3] check access for analytics --- .../app/features/navigation/lib/menu-links-hook.ts | 1 + .../src/app/routes/openops-analytics/index.tsx | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/app/features/navigation/lib/menu-links-hook.ts b/packages/react-ui/src/app/features/navigation/lib/menu-links-hook.ts index 2b0afd5908..f07d249a7d 100644 --- a/packages/react-ui/src/app/features/navigation/lib/menu-links-hook.ts +++ b/packages/react-ui/src/app/features/navigation/lib/menu-links-hook.ts @@ -63,6 +63,7 @@ export const useMenuLinks = () => { to: '/analytics', label: t('Analytics'), icon: LucideBarChart2, + locked: !checkAccess(Permission.WRITE_ANALYTICS), }, ] : []), diff --git a/packages/react-ui/src/app/routes/openops-analytics/index.tsx b/packages/react-ui/src/app/routes/openops-analytics/index.tsx index c4152e9519..ba69c312eb 100644 --- a/packages/react-ui/src/app/routes/openops-analytics/index.tsx +++ b/packages/react-ui/src/app/routes/openops-analytics/index.tsx @@ -1,7 +1,10 @@ +import { Navigate } from 'react-router-dom'; + +import { useAuthorization } from '@/app/common/hooks/authorization-hooks'; import { flagsHooks } from '@/app/common/hooks/flags-hooks'; import { useDefaultSidebarState } from '@/app/common/hooks/use-default-sidebar-state'; import { useCandu } from '@/app/features/extensions/candu/use-candu'; -import { FlagId } from '@openops/shared'; +import { FlagId, Permission } from '@openops/shared'; import { AnalyticsDashboardEmptyState, @@ -14,7 +17,7 @@ import { useEmbedDashboard } from './use-embed-dashboard'; const OpenOpsAnalyticsPage = () => { useDefaultSidebarState('minimized'); - + const { checkAccess } = useAuthorization(); const { isCanduEnabled, canduClientToken, canduUserId } = useCandu(); const { data: analyticsPublicUrl } = flagsHooks.useFlag( FlagId.ANALYTICS_PUBLIC_URL, @@ -36,6 +39,10 @@ const OpenOpsAnalyticsPage = () => { canduUserId, }); + if (!checkAccess(Permission.WRITE_ANALYTICS)) { + return ; + } + if (!analyticsPublicUrl) { console.error('OpenOps Analytics URL is not defined'); return null; From 1115a4b5c54aa8c07285dca7860ae6933bf5f5d3 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Wed, 11 Mar 2026 11:54:35 +0200 Subject: [PATCH 3/3] Refactor permission check redirect --- .../src/app/common/hooks/authorization-hooks.ts | 17 ++++++++++++++++- .../select-flow-template-dialog-content.tsx | 7 ++++--- .../react-ui/src/app/routes/flows/id/index.tsx | 8 +++++++- .../react-ui/src/app/routes/flows/index.tsx | 8 +++++++- .../src/app/routes/openops-analytics/index.tsx | 10 ++++------ .../src/app/routes/openops-tables/index.tsx | 10 +++++----- 6 files changed, 43 insertions(+), 17 deletions(-) diff --git a/packages/react-ui/src/app/common/hooks/authorization-hooks.ts b/packages/react-ui/src/app/common/hooks/authorization-hooks.ts index ace62e7f88..682562a15f 100644 --- a/packages/react-ui/src/app/common/hooks/authorization-hooks.ts +++ b/packages/react-ui/src/app/common/hooks/authorization-hooks.ts @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { authenticationSession } from '@/app/lib/authentication-session'; import { Permission } from '@openops/shared'; @@ -12,3 +13,17 @@ export const useAuthorization = () => { return { checkAccess, role }; }; + +export const useCheckAccessAndRedirect = (permission: Permission) => { + const { checkAccess } = useAuthorization(); + const navigate = useNavigate(); + const hasAccess = checkAccess(permission); + + useEffect(() => { + if (!hasAccess) { + navigate('/', { replace: true }); + } + }, [hasAccess, navigate]); + + return hasAccess; +}; diff --git a/packages/react-ui/src/app/features/templates/components/select-flow-template-dialog-content.tsx b/packages/react-ui/src/app/features/templates/components/select-flow-template-dialog-content.tsx index cd5f92c1dc..2bc42ec16d 100644 --- a/packages/react-ui/src/app/features/templates/components/select-flow-template-dialog-content.tsx +++ b/packages/react-ui/src/app/features/templates/components/select-flow-template-dialog-content.tsx @@ -1,4 +1,4 @@ -import { useAuthorization } from '@/app/common/hooks/authorization-hooks'; +import { useCheckAccessAndRedirect } from '@/app/common/hooks/authorization-hooks'; import { useTheme } from '@/app/common/providers/theme-provider'; import { OPENOPS_CONNECT_TEMPLATES_URL } from '@/app/constants/cloud'; import { ExpandedTemplate } from '@/app/features/templates/components/expanded-template'; @@ -97,8 +97,9 @@ const SelectFlowTemplateDialogContent = ({ const ownerLogoUrl = useOwnerLogoUrl(); const { createPollingInterval } = useUserInfoPolling(); const { isCloudUser } = useShowTemplatesBanner(); - const { checkAccess } = useAuthorization(); - const hasWriteFlowPermission = checkAccess(Permission.WRITE_FLOW); + const hasWriteFlowPermission = useCheckAccessAndRedirect( + Permission.WRITE_FLOW, + ); const isFullCatalog = !isTemplatePreselected && isCloudUser; const onExploreMoreClick = () => { diff --git a/packages/react-ui/src/app/routes/flows/id/index.tsx b/packages/react-ui/src/app/routes/flows/id/index.tsx index 81614a7887..f37c10fa73 100644 --- a/packages/react-ui/src/app/routes/flows/id/index.tsx +++ b/packages/react-ui/src/app/routes/flows/id/index.tsx @@ -1,9 +1,10 @@ -import { PopulatedFlow } from '@openops/shared'; +import { Permission, PopulatedFlow } from '@openops/shared'; import { useQuery } from '@tanstack/react-query'; import { useEffect } from 'react'; import { Navigate, useParams, useSearchParams } from 'react-router-dom'; import { FullPageSpinner } from '@/app/common/components/full-page-spinner'; +import { useCheckAccessAndRedirect } from '@/app/common/hooks/authorization-hooks'; import { QueryKeys } from '@/app/constants/query-keys'; import { SEARCH_PARAMS } from '@/app/constants/search-params'; import { BuilderPage } from '@/app/features/builder'; @@ -15,6 +16,7 @@ import { flowsApi } from '@/app/features/flows/lib/flows-api'; import { AxiosError } from 'axios'; const FlowBuilderPage = () => { + const hasAccess = useCheckAccessAndRedirect(Permission.READ_FLOW); const { flowId } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); @@ -51,6 +53,10 @@ const FlowBuilderPage = () => { refetchOnWindowFocus: 'always', }); + if (!hasAccess) { + return null; + } + if (isError && (error as AxiosError).status === 404) { return ; } diff --git a/packages/react-ui/src/app/routes/flows/index.tsx b/packages/react-ui/src/app/routes/flows/index.tsx index 39acf54192..49476f449b 100644 --- a/packages/react-ui/src/app/routes/flows/index.tsx +++ b/packages/react-ui/src/app/routes/flows/index.tsx @@ -7,6 +7,7 @@ import qs from 'qs'; import { useCallback, useMemo, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useCheckAccessAndRedirect } from '@/app/common/hooks/authorization-hooks'; import { useDefaultSidebarState } from '@/app/common/hooks/use-default-sidebar-state'; import { isModifierOrMiddleClick } from '@/app/common/navigation/table-navigation-helper'; import { @@ -16,10 +17,11 @@ import { import { flowsApi } from '@/app/features/flows/lib/flows-api'; import { FlowsFolderBreadcrumbsManager } from '@/app/features/folders/component/folder-breadcrumbs-manager'; import { FLOWS_TABLE_FILTERS } from '@/app/features/folders/lib/flows-table-filters'; -import { FlowStatus, FlowVersionState } from '@openops/shared'; +import { FlowStatus, FlowVersionState, Permission } from '@openops/shared'; const FlowsPage = () => { useDefaultSidebarState('expanded'); + const hasAccess = useCheckAccessAndRedirect(Permission.READ_FLOW); const navigate = useNavigate(); const [tableRefresh, setTableRefresh] = useState(false); const onTableRefresh = useCallback( @@ -58,6 +60,10 @@ const FlowsPage = () => { [], ); + if (!hasAccess) { + return null; + } + return (
diff --git a/packages/react-ui/src/app/routes/openops-analytics/index.tsx b/packages/react-ui/src/app/routes/openops-analytics/index.tsx index ba69c312eb..66dc993ee5 100644 --- a/packages/react-ui/src/app/routes/openops-analytics/index.tsx +++ b/packages/react-ui/src/app/routes/openops-analytics/index.tsx @@ -1,6 +1,4 @@ -import { Navigate } from 'react-router-dom'; - -import { useAuthorization } from '@/app/common/hooks/authorization-hooks'; +import { useCheckAccessAndRedirect } from '@/app/common/hooks/authorization-hooks'; import { flagsHooks } from '@/app/common/hooks/flags-hooks'; import { useDefaultSidebarState } from '@/app/common/hooks/use-default-sidebar-state'; import { useCandu } from '@/app/features/extensions/candu/use-candu'; @@ -17,7 +15,7 @@ import { useEmbedDashboard } from './use-embed-dashboard'; const OpenOpsAnalyticsPage = () => { useDefaultSidebarState('minimized'); - const { checkAccess } = useAuthorization(); + const hasAccess = useCheckAccessAndRedirect(Permission.WRITE_ANALYTICS); const { isCanduEnabled, canduClientToken, canduUserId } = useCandu(); const { data: analyticsPublicUrl } = flagsHooks.useFlag( FlagId.ANALYTICS_PUBLIC_URL, @@ -39,8 +37,8 @@ const OpenOpsAnalyticsPage = () => { canduUserId, }); - if (!checkAccess(Permission.WRITE_ANALYTICS)) { - return ; + if (!hasAccess) { + return null; } if (!analyticsPublicUrl) { diff --git a/packages/react-ui/src/app/routes/openops-tables/index.tsx b/packages/react-ui/src/app/routes/openops-tables/index.tsx index bb7b53504d..fb8e9272ad 100644 --- a/packages/react-ui/src/app/routes/openops-tables/index.tsx +++ b/packages/react-ui/src/app/routes/openops-tables/index.tsx @@ -1,7 +1,7 @@ import { t } from 'i18next'; -import { Navigate, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; -import { useAuthorization } from '@/app/common/hooks/authorization-hooks'; +import { useCheckAccessAndRedirect } from '@/app/common/hooks/authorization-hooks'; import { flagsHooks } from '@/app/common/hooks/flags-hooks'; import { useDefaultSidebarState } from '@/app/common/hooks/use-default-sidebar-state'; import { useCandu } from '@/app/features/extensions/candu/use-candu'; @@ -9,7 +9,7 @@ import { FlagId, Permission } from '@openops/shared'; const OpenOpsTablesPage = () => { useDefaultSidebarState('minimized'); - const { checkAccess } = useAuthorization(); + const hasAccess = useCheckAccessAndRedirect(Permission.WRITE_TABLE); const { isCanduEnabled, canduClientToken, canduUserId } = useCandu(); const parentData = encodeURIComponent( JSON.stringify({ isCanduEnabled, userId: canduUserId, canduClientToken }), @@ -24,8 +24,8 @@ const OpenOpsTablesPage = () => { FlagId.OPENOPS_TABLES_PUBLIC_URL, ); - if (!checkAccess(Permission.WRITE_TABLE)) { - return ; + if (!hasAccess) { + return null; } if (!openopsTablesUrl) {