diff --git a/config/setupTests.js b/config/setupTests.js
index 03ff480d3..00367f4a8 100644
--- a/config/setupTests.js
+++ b/config/setupTests.js
@@ -57,4 +57,24 @@ 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 }),
+ fetchDefaultWorkspace: jest.fn(() => Promise.resolve({ id: 'mock-workspace-id' })),
+}));
+
global.React = React;
diff --git a/package-lock.json b/package-lock.json
index 530278ea9..d230b87c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,6 +23,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",
@@ -6746,6 +6747,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/src/App.js b/src/App.js
index 9e3685c6e..a3234d066 100644
--- a/src/App.js
+++ b/src/App.js
@@ -4,8 +4,10 @@ import { NotificationsProvider } from '@redhat-cloud-services/frontend-component
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';
@@ -40,13 +42,13 @@ 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/Utilities/constants.js b/src/Utilities/constants.js
index 5f87dbd46..300e0f0f9 100644
--- a/src/Utilities/constants.js
+++ b/src/Utilities/constants.js
@@ -296,8 +296,11 @@ export const exportNotifications = (format) => ({
export const multiValueFilters = ['installed_evra', 'os', 'creator', 'status', 'group_name'];
+export const KESSEL_API_BASE_URL = '/api/kessel/v1beta2';
+
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/useKesselWorkspaces.js b/src/Utilities/hooks/useKesselWorkspaces.js
new file mode 100644
index 000000000..4a1e52b01
--- /dev/null
+++ b/src/Utilities/hooks/useKesselWorkspaces.js
@@ -0,0 +1,36 @@
+import { useState, useEffect } from 'react';
+import { fetchDefaultWorkspace } from '@project-kessel/react-kessel-access-check';
+
+let defaultWorkspacePromise = null;
+
+export const useFetchDefaultWorkspaceId = (enabled = true) => {
+ const [defaultWorkspace, setDefaultWorkspace] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const baseUrl = window.location.origin;
+
+ useEffect(() => {
+ if (!enabled) {
+ setIsLoading(false);
+ return;
+ }
+
+ if (!defaultWorkspacePromise) {
+ defaultWorkspacePromise = fetchDefaultWorkspace(baseUrl);
+ }
+
+ defaultWorkspacePromise
+ .then(setDefaultWorkspace)
+ .catch((err) => {
+ defaultWorkspacePromise = null;
+ setError(err);
+ })
+ .finally(() => setIsLoading(false));
+ }, [baseUrl, enabled]);
+
+ return {
+ workspaceId: defaultWorkspace?.id,
+ isLoading,
+ error,
+ };
+};
diff --git a/src/Utilities/hooks/usePermissionCheck.js b/src/Utilities/hooks/usePermissionCheck.js
new file mode 100644
index 000000000..f01c02ef1
--- /dev/null
+++ b/src/Utilities/hooks/usePermissionCheck.js
@@ -0,0 +1,66 @@
+import { useMemo } from 'react';
+import { usePermissionsWithContext } from '@redhat-cloud-services/frontend-components-utilities/RBACHook';
+import { getKesselAccessCheckParams } from '@redhat-cloud-services/frontend-components-utilities/kesselPermissions';
+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',
+};
+
+export const useRbacV1Permissions = (requiredPermissions) => {
+ const { hasAccess, isLoading } = usePermissionsWithContext(requiredPermissions);
+ return { hasAccess, isLoading };
+};
+
+export const useKesselPermissions = (requiredPermissions, enabled = true) => {
+ const {
+ workspaceId,
+ isLoading: workspaceLoading,
+ error: workspaceError,
+ } = useFetchDefaultWorkspaceId(enabled);
+
+ const checkParams = useMemo(
+ () =>
+ getKesselAccessCheckParams({
+ permissionMap: PERMISSION_MAP,
+ requiredPermissions,
+ resourceIdOrIds: workspaceId,
+ }),
+ [workspaceId, requiredPermissions],
+ );
+
+ const { data, loading, error } = useSelfAccessCheck(checkParams);
+
+ if (workspaceLoading) {
+ return { hasAccess: false, isLoading: true };
+ }
+
+ if (checkParams?.resources?.length === 0) {
+ return { hasAccess: true, isLoading: false };
+ }
+
+ if (!workspaceId || workspaceError || 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);
+
+ return isKesselEnabled ? kessel : rbac;
+};
+
+export default usePermissionCheck;
diff --git a/src/index.js b/src/index.js
index e5b2f4a61..272e569b5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,10 +4,15 @@ import { SystemAdvisoryListStore } from './store/Reducers/SystemAdvisoryListStor
import { SystemPackageListStore } from './store/Reducers/SystemPackageListStore';
import { Bullseye, Spinner } from '@patternfly/react-core';
import { Provider } from 'react-redux';
+import { AccessCheck } from '@project-kessel/react-kessel-access-check';
import PropTypes from 'prop-types';
+import { useKesselFeatureFlag } from './Utilities/hooks/useFeatureFlag';
+import { KESSEL_API_BASE_URL } from './Utilities/constants';
const WrappedSystemDetail = ({ getRegistry, ...props }) => {
const [Wrapper, setWrapper] = useState();
+ const isKesselEnabled = useKesselFeatureFlag();
+
useEffect(() => {
if (getRegistry) {
getRegistry()?.register?.({ SystemAdvisoryListStore, SystemPackageListStore });
@@ -15,7 +20,8 @@ const WrappedSystemDetail = ({ getRegistry, ...props }) => {
setWrapper(() => (getRegistry ? Provider : Fragment));
}, []);
- return Wrapper ? (
+
+ const content = Wrapper ? (
@@ -24,6 +30,19 @@ const WrappedSystemDetail = ({ getRegistry, ...props }) => {
);
+
+ if (!isKesselEnabled) {
+ return content;
+ }
+
+ return (
+
+ {content}
+
+ );
};
WrappedSystemDetail.propTypes = {