diff --git a/config/setupTests.js b/config/setupTests.js
index 03ff480d3..57f301303 100644
--- a/config/setupTests.js
+++ b/config/setupTests.js
@@ -57,4 +57,23 @@ jest.mock('../src/Utilities/hooks/useRemediationDataProvider', () => ({
jest.mock('../src/Utilities/hooks/useFeatureFlag', () => jest.fn());
+jest.mock('../src/Utilities/hooks/usePermissionCheck', () => ({
+ __esModule: true,
+ default: () => ({ hasAccess: true, isLoading: false }),
+ useRbacV1Permissions: () => ({ hasAccess: true, isLoading: false }),
+ useKesselPermissions: () => ({ hasAccess: true, isLoading: false }),
+ PERMISSION_MAP: {
+ 'patch:*:read': 'patch_system_view',
+ 'patch:*:*': 'patch_system_edit',
+ 'patch:template:write': 'patch_template_edit',
+ },
+}));
+
+jest.mock('@project-kessel/react-kessel-access-check', () => ({
+ AccessCheck: {
+ Provider: ({ children }) => <>{children}>,
+ },
+ useSelfAccessCheck: () => ({ data: null, loading: false, error: null }),
+}));
+
global.React = React;
diff --git a/package-lock.json b/package-lock.json
index f1b0799a4..d4fd58ebc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,7 @@
"@patternfly/react-core": "^6.3.1",
"@patternfly/react-icons": "^6.3.1",
"@patternfly/react-table": "^6.4.1",
- "@project-kessel/react-kessel-access-check": "^0.0.2",
+ "@project-kessel/react-kessel-access-check": "^0.3.1",
"@redhat-cloud-services/frontend-components": "^7.0.4",
"@redhat-cloud-services/frontend-components-notifications": "^6.1.13",
"@redhat-cloud-services/frontend-components-remediations": "^4.0.17",
@@ -25,6 +25,7 @@
"@redhat-cloud-services/javascript-clients-shared": "^2.0.0",
"@scalprum/react-core": "^0.7.1",
"@sentry/webpack-plugin": "^3.1.0",
+ "@tanstack/react-query": "^5.90.5",
"@types/dockerode": "^3.3.47",
"@unleash/proxy-client-react": "^3.5.0",
"axios": "^1.13.5",
@@ -5424,9 +5425,9 @@
}
},
"node_modules/@project-kessel/react-kessel-access-check": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/@project-kessel/react-kessel-access-check/-/react-kessel-access-check-0.0.2.tgz",
- "integrity": "sha512-jTPHrgByktaSfOO4BmH2oLKuKxbgvL4SiaszFGlo7aJyXButj1chcOQS7UHEzzz8psmc0ZSPm/UQGovXCKp1ig==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@project-kessel/react-kessel-access-check/-/react-kessel-access-check-0.3.2.tgz",
+ "integrity": "sha512-yYghByDgWo0IpbBb1S5Pa+0uTTzrUbDwGKhEq7BycG3mKX32hhXB0PjbUXMHOWBGptruw68Fc1lPbsRQ7L4lTw==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -6834,6 +6835,32 @@
"@swc/counter": "^0.1.3"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
+ "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.90.21",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
+ "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.90.20"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
diff --git a/package.json b/package.json
index 9f03e16b4..09e081907 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"@patternfly/react-core": "^6.3.1",
"@patternfly/react-icons": "^6.3.1",
"@patternfly/react-table": "^6.4.1",
- "@project-kessel/react-kessel-access-check": "^0.0.2",
+ "@project-kessel/react-kessel-access-check": "^0.3.1",
"@redhat-cloud-services/frontend-components": "^7.0.4",
"@redhat-cloud-services/frontend-components-notifications": "^6.1.13",
"@redhat-cloud-services/frontend-components-remediations": "^4.0.17",
@@ -19,6 +19,7 @@
"@redhat-cloud-services/host-inventory-client": "^4.1.7",
"@redhat-cloud-services/javascript-clients-shared": "^2.0.0",
"@scalprum/react-core": "^0.7.1",
+ "@tanstack/react-query": "^5.90.5",
"@sentry/webpack-plugin": "^3.1.0",
"@types/dockerode": "^3.3.47",
"@unleash/proxy-client-react": "^3.5.0",
diff --git a/src/App.js b/src/App.js
index 9e3685c6e..0ba926511 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,14 +1,19 @@
import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { NotificationsProvider } from '@redhat-cloud-services/frontend-components-notifications';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import '@redhat-cloud-services/frontend-components-notifications/index.css';
import { RBACProvider } from '@redhat-cloud-services/frontend-components/RBACProvider';
+import { AccessCheck } from '@project-kessel/react-kessel-access-check';
import { changeGlobalTags, changeProfile, globalFilter } from './store/Actions/Actions';
import { mapGlobalFilters } from './Utilities/Helpers';
+import { KESSEL_API_BASE_URL } from './Utilities/constants';
import './App.scss';
import Routes from './Routes';
+const queryClient = new QueryClient();
+
const App = () => {
const dispatch = useDispatch();
const chrome = useChrome();
@@ -40,13 +45,15 @@ const App = () => {
}, []);
return (
-
+
-
-
-
+
+
+
+
+
-
+
);
};
diff --git a/src/PresentationalComponents/WithPermission/WithPermission.js b/src/PresentationalComponents/WithPermission/WithPermission.js
index e645a366a..7c6093963 100644
--- a/src/PresentationalComponents/WithPermission/WithPermission.js
+++ b/src/PresentationalComponents/WithPermission/WithPermission.js
@@ -1,20 +1,21 @@
import React from 'react';
import propTypes from 'prop-types';
import { NotAuthorized } from '@redhat-cloud-services/frontend-components/NotAuthorized';
-import { usePermissionsWithContext } from '@redhat-cloud-services/frontend-components-utilities/RBACHook';
+import usePermissionCheck from '../../Utilities/hooks/usePermissionCheck';
-const WithPermission = ({ children, requiredPermissions = [] }) => {
- const { hasAccess, isLoading } = usePermissionsWithContext(requiredPermissions);
- if (!isLoading) {
- return hasAccess ? children : ;
- } else {
- return '';
+const WithPermission = ({ children, requiredPermissions = [], hide = false }) => {
+ const { hasAccess, isLoading } = usePermissionCheck(requiredPermissions);
+
+ if (isLoading) {
+ return null;
}
+ return hasAccess ? children : !hide && ;
};
WithPermission.propTypes = {
children: propTypes.node,
requiredPermissions: propTypes.array,
+ hide: propTypes.bool,
};
export default WithPermission;
diff --git a/src/Routes.js b/src/Routes.js
index 85d4b4da9..1e6fcf536 100644
--- a/src/Routes.js
+++ b/src/Routes.js
@@ -1,15 +1,15 @@
import { Bullseye, Spinner } from '@patternfly/react-core';
import { NotAuthorized } from '@redhat-cloud-services/frontend-components/NotAuthorized';
-import { usePermissionsWithContext } from '@redhat-cloud-services/frontend-components-utilities/RBACHook';
import AsyncComponent from '@redhat-cloud-services/frontend-components/AsyncComponent';
import axios from 'axios';
import PropTypes from 'prop-types';
import React, { lazy, Suspense, useEffect, useState } from 'react';
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
import { NavigateToSystem } from './Utilities/NavigateToSystem';
+import usePermissionCheck from './Utilities/hooks/usePermissionCheck';
const PermissionRoute = ({ requiredPermissions = [] }) => {
- const { hasAccess, isLoading } = usePermissionsWithContext(requiredPermissions);
+ const { hasAccess, isLoading } = usePermissionCheck(requiredPermissions);
if (!isLoading) {
return hasAccess ? : ;
} else {
diff --git a/src/SmartComponents/PatchSet/PatchSet.js b/src/SmartComponents/PatchSet/PatchSet.js
index f719402bb..702974d0a 100644
--- a/src/SmartComponents/PatchSet/PatchSet.js
+++ b/src/SmartComponents/PatchSet/PatchSet.js
@@ -35,7 +35,7 @@ import {
} from './PatchSetAssets';
import PatchSetWizard from '../PatchSetWizard/PatchSetWizard';
import { patchSetDeleteNotifications } from '../../Utilities/constants';
-import { usePermissionsWithContext } from '@redhat-cloud-services/frontend-components-utilities/RBACHook';
+import usePermissionCheck from '../../Utilities/hooks/usePermissionCheck';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import { Icon, Popover } from '@patternfly/react-core';
import DeleteSetModal from '../Modals/DeleteSetModal';
@@ -133,7 +133,7 @@ const PatchSet = () => {
});
};
- const { hasAccess } = usePermissionsWithContext(['patch:*:*', 'patch:template:write']);
+ const { hasAccess } = usePermissionCheck(['patch:*:*', 'patch:template:write']);
const CreatePatchSetButton = createPatchSetButton(setPatchSetState, hasAccess);
const actionsConfig = patchSetRowActions(openPatchSetEditModal, openPatchDeleteModal);
diff --git a/src/SmartComponents/PatchSetDetail/PatchSetDetail.js b/src/SmartComponents/PatchSetDetail/PatchSetDetail.js
index 7085f4272..828f7c5df 100644
--- a/src/SmartComponents/PatchSetDetail/PatchSetDetail.js
+++ b/src/SmartComponents/PatchSetDetail/PatchSetDetail.js
@@ -49,7 +49,7 @@ import {
} from '../../Utilities/Helpers';
import PatchSetWizard from '../PatchSetWizard/PatchSetWizard';
import { patchSetDetailRowActions } from '../PatchSet/PatchSetAssets';
-import { usePermissionsWithContext } from '@redhat-cloud-services/frontend-components-utilities/RBACHook';
+import usePermissionCheck from '../../Utilities/hooks/usePermissionCheck';
import UnassignSystemsModal from '../Modals/UnassignSystemsModal';
import { TableVariant } from '@patternfly/react-table';
import { InventoryTable } from '@redhat-cloud-services/frontend-components/Inventory';
@@ -119,7 +119,7 @@ const PatchSetDetail = () => {
({ PatchSetDetailSystemsStore }) => PatchSetDetailSystemsStore?.templateHasSystems,
);
- const { hasAccess } = usePermissionsWithContext(['patch:*:*', 'patch:template:write']);
+ const { hasAccess } = usePermissionCheck(['patch:*:*', 'patch:template:write']);
const patchSetName = templateDetails.data.attributes.name;
diff --git a/src/SmartComponents/Systems/SystemsTable.js b/src/SmartComponents/Systems/SystemsTable.js
index e86ee328e..f32a7bf17 100644
--- a/src/SmartComponents/Systems/SystemsTable.js
+++ b/src/SmartComponents/Systems/SystemsTable.js
@@ -29,7 +29,7 @@ import {
mergeInventoryColumns,
} from '../../Utilities/SystemsHelpers';
import { combineReducers } from 'redux';
-import { usePermissionsWithContext } from '@redhat-cloud-services/frontend-components-utilities/RBACHook';
+import usePermissionCheck from '../../Utilities/hooks/usePermissionCheck';
import propTypes from 'prop-types';
const SystemsTable = ({
@@ -52,7 +52,7 @@ const SystemsTable = ({
const areAllSelected = useSelector(({ SystemsStore }) => SystemsStore?.areAllSelected);
const queryParams = useSelector(({ SystemsStore }) => SystemsStore?.queryParams || {});
- const { hasAccess: hasTemplateAccess } = usePermissionsWithContext([
+ const { hasAccess: hasTemplateAccess } = usePermissionCheck([
'patch:*:*',
'patch:template:write',
]);
diff --git a/src/Utilities/constants.js b/src/Utilities/constants.js
index f990d86a8..200dfb8df 100644
--- a/src/Utilities/constants.js
+++ b/src/Utilities/constants.js
@@ -307,8 +307,12 @@ export const patchSetDeleteNotifications = (templateName) => ({
export const multiValueFilters = ['installed_evra', 'os', 'creator', 'status', 'group_name'];
+export const KESSEL_API_BASE_URL = '/api/kessel/v1beta2';
+export const RBAC_API_BASE_V2 = '/api/rbac/v2';
+
export const featureFlags = {
patch_set: 'patch.patch_set',
+ kessel_enabled: 'patch-frontend.kessel-enabled',
};
export const NO_ADVISORIES_TEXT =
diff --git a/src/Utilities/hooks/useFeatureFlag.js b/src/Utilities/hooks/useFeatureFlag.js
index a125bbaf9..f9be4525e 100644
--- a/src/Utilities/hooks/useFeatureFlag.js
+++ b/src/Utilities/hooks/useFeatureFlag.js
@@ -3,10 +3,9 @@ import { useFlag, useFlagsStatus } from '@unleash/proxy-client-react';
const useFeatureFlag = (flag) => {
const { flagsReady } = useFlagsStatus();
const isFlagEnabled = useFlag(flag);
- return flagsReady ? isFlagEnabled : false;
+ return flagsReady ? isFlagEnabled : undefined;
};
export default useFeatureFlag;
-export const useKesselFeatureFlag = () =>
- useFeatureFlag('patch-frontend.kessel-enabled');
+export const useKesselFeatureFlag = () => useFeatureFlag('patch-frontend.kessel-enabled');
diff --git a/src/Utilities/hooks/useKesselWorkspaces.js b/src/Utilities/hooks/useKesselWorkspaces.js
new file mode 100644
index 000000000..7837e1e76
--- /dev/null
+++ b/src/Utilities/hooks/useKesselWorkspaces.js
@@ -0,0 +1,36 @@
+import { useQuery } from '@tanstack/react-query';
+import { RBAC_API_BASE_V2 } from '../constants';
+
+const STALE_TIME = 5 * 60 * 1000;
+
+export const useKesselWorkspaces = (options = {}) =>
+ useQuery({
+ queryKey: ['workspaces', options.type, options.limit],
+ queryFn: async () => {
+ const response = await fetch(
+ `${RBAC_API_BASE_V2}/workspaces/?limit=${options.limit ?? 1000}&type=${options.type ?? 'all'}`,
+ );
+ if (!response.ok) {
+ throw new Error('Failed to fetch workspaces');
+ }
+ const data = await response.json();
+ return data.data || [];
+ },
+ enabled: options.enabled ?? true,
+ staleTime: options.staleTime,
+ });
+
+export const useFetchDefaultWorkspaceId = (enabled = true) => {
+ const {
+ data: workspaces,
+ isLoading,
+ error,
+ } = useKesselWorkspaces({ type: 'default', limit: 1, staleTime: STALE_TIME, enabled });
+ const defaultWorkspace = workspaces?.[0];
+
+ return {
+ workspaceId: defaultWorkspace?.id,
+ isLoading: enabled ? isLoading : false,
+ error,
+ };
+};
diff --git a/src/Utilities/hooks/usePermissionCheck.js b/src/Utilities/hooks/usePermissionCheck.js
new file mode 100644
index 000000000..29d79af96
--- /dev/null
+++ b/src/Utilities/hooks/usePermissionCheck.js
@@ -0,0 +1,81 @@
+import { useMemo } from 'react';
+import { usePermissionsWithContext } from '@redhat-cloud-services/frontend-components-utilities/RBACHook';
+import { useSelfAccessCheck } from '@project-kessel/react-kessel-access-check';
+import { useFetchDefaultWorkspaceId } from './useKesselWorkspaces';
+import useFeatureFlag from './useFeatureFlag';
+import { featureFlags } from '../constants';
+
+export const PERMISSION_MAP = {
+ 'patch:*:read': 'patch_system_view',
+ 'patch:*:*': 'patch_system_edit',
+ 'patch:template:write': 'patch_template_edit',
+};
+
+const getKesselAccessCheckParams = (permissionMap, requiredPermissions, workspaceId) => {
+ const resources = requiredPermissions
+ .map((perm) => {
+ const relation = permissionMap[perm];
+ if (!relation) {
+ return null;
+ }
+ return { id: workspaceId, type: 'workspace', relation };
+ })
+ .filter(Boolean);
+
+ if (resources.length === 0) {
+ return { resources: [] };
+ }
+
+ if (resources.length === 1) {
+ return {
+ resource: { id: resources[0].id, type: resources[0].type },
+ relation: resources[0].relation,
+ };
+ }
+
+ return { resources };
+};
+
+export const useRbacV1Permissions = (requiredPermissions) => {
+ const { hasAccess, isLoading } = usePermissionsWithContext(requiredPermissions);
+ return { hasAccess, isLoading };
+};
+
+export const useKesselPermissions = (requiredPermissions, enabled = true) => {
+ const { workspaceId, isLoading: workspaceLoading } = useFetchDefaultWorkspaceId(enabled);
+
+ const checkParams = useMemo(
+ () => getKesselAccessCheckParams(PERMISSION_MAP, requiredPermissions, workspaceId),
+ [workspaceId, requiredPermissions],
+ );
+
+ const { data, loading, error } = useSelfAccessCheck(checkParams);
+
+ if (workspaceLoading) {
+ return { hasAccess: false, isLoading: true };
+ }
+
+ if (!workspaceId || error) {
+ return { hasAccess: false, isLoading: false };
+ }
+
+ const hasAccess = Array.isArray(data)
+ ? data.some((check) => check.allowed)
+ : (data?.allowed ?? false);
+
+ return { hasAccess, isLoading: loading };
+};
+
+const usePermissionCheck = (requiredPermissions) => {
+ const isKesselEnabled = useFeatureFlag(featureFlags.kessel_enabled);
+ const rbac = useRbacV1Permissions(requiredPermissions);
+ const kessel = useKesselPermissions(requiredPermissions, !!isKesselEnabled);
+
+ if (isKesselEnabled === undefined) {
+ return { hasAccess: false, isLoading: true };
+ }
+
+ return isKesselEnabled ? kessel : rbac;
+};
+
+export default usePermissionCheck;