Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions config/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
35 changes: 31 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just used the compliance frontend version, package lock is set to latest: 5.90.21

"@sentry/webpack-plugin": "^3.1.0",
"@types/dockerode": "^3.3.47",
"@unleash/proxy-client-react": "^3.5.0",
Expand Down
17 changes: 12 additions & 5 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -40,13 +45,15 @@ const App = () => {
}, []);

return (
<React.Fragment>
<QueryClientProvider client={queryClient}>
<RBACProvider appName='patch'>
<NotificationsProvider>
<Routes />
</NotificationsProvider>
<AccessCheck.Provider baseUrl={window.location.origin} apiPath={KESSEL_API_BASE_URL}>
<NotificationsProvider>
<Routes />
</NotificationsProvider>
</AccessCheck.Provider>
</RBACProvider>
</React.Fragment>
</QueryClientProvider>
);
};

Expand Down
15 changes: 8 additions & 7 deletions src/PresentationalComponents/WithPermission/WithPermission.js
Original file line number Diff line number Diff line change
@@ -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 : <NotAuthorized serviceName='patch' />;
} else {
return '';
const WithPermission = ({ children, requiredPermissions = [], hide = false }) => {
const { hasAccess, isLoading } = usePermissionCheck(requiredPermissions);

if (isLoading) {
return null;
}
return hasAccess ? children : !hide && <NotAuthorized serviceName='patch' />;
};

WithPermission.propTypes = {
children: propTypes.node,
requiredPermissions: propTypes.array,
hide: propTypes.bool,
};

export default WithPermission;
4 changes: 2 additions & 2 deletions src/Routes.js
Original file line number Diff line number Diff line change
@@ -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 ? <Outlet /> : <NotAuthorized serviceName='patch' />;
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/SmartComponents/PatchSet/PatchSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions src/SmartComponents/PatchSetDetail/PatchSetDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions src/SmartComponents/Systems/SystemsTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ({
Expand All @@ -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',
]);
Expand Down
4 changes: 4 additions & 0 deletions src/Utilities/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
5 changes: 2 additions & 3 deletions src/Utilities/hooks/useFeatureFlag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
36 changes: 36 additions & 0 deletions src/Utilities/hooks/useKesselWorkspaces.js
Original file line number Diff line number Diff line change
@@ -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,
};
};
81 changes: 81 additions & 0 deletions src/Utilities/hooks/usePermissionCheck.js
Original file line number Diff line number Diff line change
@@ -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;
Loading