diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index 5fb223a40..1ccd16aa9 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -473,7 +473,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo }, }; - const resourcePools = { + const namespaces = { name: 'kubernetes.resourcePools', url: '/namespaces', views: { @@ -499,7 +499,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo }, }; - const resourcePool = { + const namespace = { name: 'kubernetes.resourcePools.resourcePool', url: '/:id?tab', views: { @@ -681,9 +681,9 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo $stateRegistryProvider.register(node); $stateRegistryProvider.register(nodeStats); $stateRegistryProvider.register(kubectlShell); - $stateRegistryProvider.register(resourcePools); + $stateRegistryProvider.register(namespaces); $stateRegistryProvider.register(namespaceCreation); - $stateRegistryProvider.register(resourcePool); + $stateRegistryProvider.register(namespace); $stateRegistryProvider.register(namespaceAccess); $stateRegistryProvider.register(volumes); $stateRegistryProvider.register(volume); diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsView.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsView.tsx index 5f343c29d..7cdb9eb69 100644 --- a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsView.tsx +++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsView.tsx @@ -2,6 +2,7 @@ import { AlertTriangle, Code, History, Minimize2 } from 'lucide-react'; import { useCurrentStateAndParams } from '@uirouter/react'; import LaptopCode from '@/assets/ico/laptop-code.svg?c'; +import { useNamespaceAccessRedirect } from '@/react/kubernetes/namespaces/hooks/useNamespaceAccessRedirect'; import { PageHeader } from '@@/PageHeader'; import { Tab, WidgetTabs, findSelectedTabIndex } from '@@/Widget/WidgetTabs'; @@ -30,6 +31,7 @@ export function ApplicationDetailsView() { const { params: { namespace, name }, } = stateAndParams; + useNamespaceAccessRedirect(namespace, { to: 'kubernetes.applications' }); // placements table data const { placementsData, isPlacementsTableLoading, hasPlacementWarning } = diff --git a/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.test.tsx b/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.test.tsx index 2e0f3b4da..aad448436 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.test.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.test.tsx @@ -156,6 +156,20 @@ function createCommonHandlers() { http.get('/api/endpoints/3/kubernetes/helm/test-release/history', () => HttpResponse.json(helmReleaseHistory) ), + http.get('/api/kubernetes/3/namespaces', () => + HttpResponse.json([ + { + Id: 'default', + Name: 'default', + Status: { phase: 'Active' }, + Annotations: null, + CreationDate: '2021-01-01T00:00:00Z', + NamespaceOwner: '', + IsSystem: false, + IsDefault: true, + }, + ]) + ), http.get('/api/kubernetes/3/namespaces/default/events', () => HttpResponse.json([]) ), diff --git a/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.tsx b/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.tsx index f063c06d4..d3c671761 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/HelmApplicationView.tsx @@ -5,6 +5,7 @@ import helm from '@/assets/ico/vendor/helm.svg?c'; import { PageHeader } from '@/react/components/PageHeader'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { Authorized } from '@/react/hooks/useUser'; +import { useNamespaceAccessRedirect } from '@/react/kubernetes/namespaces/hooks/useNamespaceAccessRedirect'; import { WidgetTitle, WidgetBody, Widget, Loading } from '@@/Widget'; import { Card } from '@@/Card'; @@ -26,6 +27,7 @@ export function HelmApplicationView() { const queryClient = useQueryClient(); const { params } = useCurrentStateAndParams(); const { name, namespace, revision } = params; + useNamespaceAccessRedirect(namespace, { to: 'kubernetes.applications' }); const helmHistoryQuery = useHelmHistory(environmentId, name, namespace); const latestRevision = helmHistoryQuery.data?.[0]?.version; const earlistRevision = diff --git a/app/react/kubernetes/ingresses/CreateIngressView/CreateIngressView.tsx b/app/react/kubernetes/ingresses/CreateIngressView/CreateIngressView.tsx index b5de9afe1..554155d04 100644 --- a/app/react/kubernetes/ingresses/CreateIngressView/CreateIngressView.tsx +++ b/app/react/kubernetes/ingresses/CreateIngressView/CreateIngressView.tsx @@ -9,6 +9,7 @@ import { notifyError, notifySuccess } from '@/portainer/services/notifications'; import { useAuthorizations } from '@/react/hooks/useUser'; import { Annotation } from '@/react/kubernetes/annotations/types'; import { prepareAnnotations } from '@/react/kubernetes/utils'; +import { useNamespaceAccessRedirect } from '@/react/kubernetes/namespaces/hooks/useNamespaceAccessRedirect'; import { Link } from '@@/Link'; import { PageHeader } from '@@/PageHeader'; @@ -43,6 +44,7 @@ import { export function CreateIngressView() { const environmentId = useEnvironmentId(); const { params } = useCurrentStateAndParams(); + useNamespaceAccessRedirect(params.namespace, { to: 'kubernetes.ingresses' }); const { authorized: isAuthorizedToAddEdit } = useAuthorizations([ 'K8sIngressesW', ]); diff --git a/app/react/kubernetes/namespaces/ItemView/NamespaceView.tsx b/app/react/kubernetes/namespaces/ItemView/NamespaceView.tsx index fae86a79a..f49ff2cf0 100644 --- a/app/react/kubernetes/namespaces/ItemView/NamespaceView.tsx +++ b/app/react/kubernetes/namespaces/ItemView/NamespaceView.tsx @@ -2,6 +2,7 @@ import { useCurrentStateAndParams } from '@uirouter/react'; import { AlertTriangle, Code, Layers, History } from 'lucide-react'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; +import { useNamespaceAccessRedirect } from '@/react/kubernetes/namespaces/hooks/useNamespaceAccessRedirect'; import { PageHeader } from '@@/PageHeader'; import { findSelectedTabIndex, Tab, WidgetTabs } from '@@/Widget/WidgetTabs'; @@ -20,6 +21,9 @@ export function NamespaceView() { const { params: { id: namespace }, } = stateAndParams; + useNamespaceAccessRedirect(namespace, { + to: 'kubernetes.resourcePools', + }); const environmentId = useEnvironmentId(); const eventWarningCount = useEventWarningsCount(environmentId, namespace); diff --git a/app/react/kubernetes/namespaces/hooks/useNamespaceAccessRedirect.ts b/app/react/kubernetes/namespaces/hooks/useNamespaceAccessRedirect.ts new file mode 100644 index 000000000..2701abdc4 --- /dev/null +++ b/app/react/kubernetes/namespaces/hooks/useNamespaceAccessRedirect.ts @@ -0,0 +1,51 @@ +import { useEffect } from 'react'; +import { useCurrentStateAndParams, useRouter } from '@uirouter/react'; + +import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; + +import { useNamespacesQuery } from '../queries/useNamespacesQuery'; + +type RedirectOptions = { + to: string; + params?: Record; +}; + +/** + * Redirects away when the provided namespace is not in the allowed namespaces list for the current environment. + */ +export function useNamespaceAccessRedirect( + namespace?: string, + { to, params } = { to: 'kubernetes.dashboard', params: {} } as RedirectOptions +) { + const router = useRouter(); + const namespaceInParams = useCurrentStateAndParams().params.namespace; + const currentNamespace = namespace || namespaceInParams; + const environmentId = useEnvironmentId(); + + const namespacesQuery = useNamespacesQuery(environmentId); + + useEffect(() => { + if (!currentNamespace) { + return; + } + + if (namespacesQuery.isLoading || namespacesQuery.isFetching) { + return; + } + + const namespaces = namespacesQuery.data ?? []; + const isAllowed = namespaces.some((ns) => ns.Name === currentNamespace); + + if (!isAllowed) { + router.stateService.go(to, params); + } + }, [ + currentNamespace, + to, + params, + router.stateService, + namespacesQuery.isLoading, + namespacesQuery.isFetching, + namespacesQuery.data, + ]); +}