diff --git a/frontend/src/components/LoadData.tsx b/frontend/src/components/LoadData.tsx index e446fb2476d..69fa7bad595 100644 --- a/frontend/src/components/LoadData.tsx +++ b/frontend/src/components/LoadData.tsx @@ -2,7 +2,7 @@ import { Fragment, ReactNode, useContext, useEffect, useMemo, useState } from 'react' import { PluginDataContext } from '../lib/PluginDataContext' // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { SetterOrUpdater, useSetRecoilState } from 'recoil' +import { SetterOrUpdater, useRecoilValue, useSetRecoilState } from 'recoil' import { tokenExpired } from '../logout' import { AgentClusterInstallApiVersion, @@ -175,7 +175,6 @@ import { WatchEvent, } from '../atoms' import { useQuery } from '../lib/useQuery' -import { useRecoilValue } from '../shared-recoil' import get from 'lodash/get' export function LoadData(props: { children?: ReactNode }) { diff --git a/frontend/src/lib/rbac-util.test.ts b/frontend/src/lib/rbac-util.test.ts index 1cedd3128dd..7ed34eb9140 100644 --- a/frontend/src/lib/rbac-util.test.ts +++ b/frontend/src/lib/rbac-util.test.ts @@ -2,7 +2,7 @@ import { Namespace, NamespaceDefinition } from '../resources' import { nockIgnoreApiPaths, nockRBAC } from './nock-util' -import { getAuthorizedNamespaces, isAnyNamespaceAuthorized } from './rbac-util' +import { areAllNamespacesUnauthorized, getAuthorizedNamespaces, isAnyNamespaceAuthorized } from './rbac-util' import { waitForNocks } from './test-util' const adminAccess = { name: '*', namespace: '*', resource: '*', verb: '*' } @@ -49,43 +49,81 @@ describe('isAnyNamespaceAuthorized', () => { nockRBAC({ ...createDeployment, namespace: 'test-namespace-2' }, true), ] expect( - await isAnyNamespaceAuthorized( - [createDeployment], - [ - { - ...(NamespaceDefinition as Pick), - metadata: { name: 'test-namespace-1' }, - }, - { - ...(NamespaceDefinition as Pick), - metadata: { name: 'test-namespace-2' }, - }, - ] - ) + await isAnyNamespaceAuthorized(Promise.resolve(createDeployment), [ + { + ...(NamespaceDefinition as Pick), + metadata: { name: 'test-namespace-1' }, + }, + { + ...(NamespaceDefinition as Pick), + metadata: { name: 'test-namespace-2' }, + }, + ]).promise ).toEqual(true) await waitForNocks(nocks) }) it('returns false for an empty namespace list', async () => { - expect(await isAnyNamespaceAuthorized([createDeployment], [])).toEqual(false) + expect(await isAnyNamespaceAuthorized(Promise.resolve(createDeployment), []).promise).toEqual(false) }) it('returns true without checking namespaces for an admin user', async () => { nockIgnoreApiPaths() const nocks = [nockRBAC(adminAccess, true)] expect( - await isAnyNamespaceAuthorized( - [createDeployment], - [ - { - ...(NamespaceDefinition as Pick), - metadata: { name: 'test-namespace-1' }, - }, - { - ...(NamespaceDefinition as Pick), - metadata: { name: 'test-namespace-2' }, - }, - ] - ) + await isAnyNamespaceAuthorized(Promise.resolve(createDeployment), [ + { + ...(NamespaceDefinition as Pick), + metadata: { name: 'test-namespace-1' }, + }, + { + ...(NamespaceDefinition as Pick), + metadata: { name: 'test-namespace-2' }, + }, + ]).promise ).toEqual(true) await waitForNocks(nocks) }) }) + +describe('areAllNamespacesUnauthorized', () => { + it('checks each namespace individually for non-admin users', async () => { + nockIgnoreApiPaths() + const nocks = [ + nockRBAC(adminAccess, false), + nockRBAC({ ...createDeployment, namespace: 'test-namespace-1' }, false), + nockRBAC({ ...createDeployment, namespace: 'test-namespace-2' }, true), + ] + expect( + await areAllNamespacesUnauthorized(Promise.resolve(createDeployment), [ + { + ...(NamespaceDefinition as Pick), + metadata: { name: 'test-namespace-1' }, + }, + { + ...(NamespaceDefinition as Pick), + metadata: { name: 'test-namespace-2' }, + }, + ]).promise + ).toEqual(false) + await waitForNocks(nocks) + }) + it('returns true for an empty namespace list', async () => { + expect(await areAllNamespacesUnauthorized(Promise.resolve(createDeployment), []).promise).toEqual(true) + }) + it('returns false without checking namespaces for an admin user', async () => { + nockIgnoreApiPaths() + const nocks = [nockRBAC(adminAccess, true)] + expect( + await areAllNamespacesUnauthorized(Promise.resolve(createDeployment), [ + { + ...(NamespaceDefinition as Pick), + metadata: { name: 'test-namespace-1' }, + }, + { + ...(NamespaceDefinition as Pick), + metadata: { name: 'test-namespace-2' }, + }, + ]).promise + ).toEqual(false) + await waitForNocks(nocks) + }) +}) diff --git a/frontend/src/lib/rbac-util.ts b/frontend/src/lib/rbac-util.ts index 6ae3e9fa857..a34d22c8737 100644 --- a/frontend/src/lib/rbac-util.ts +++ b/frontend/src/lib/rbac-util.ts @@ -1,7 +1,7 @@ /* Copyright Contributors to the Open Cluster Management project */ +import { useEffect, useState } from 'react' import { - createShortCircuitSubjectAccessReviews, createSubjectAccessReview, createSubjectAccessReviews, fallbackPlural, @@ -11,29 +11,131 @@ import { Namespace, ResourceAttributes, } from '../resources' +import { useRecoilValue, useSharedAtoms } from '../shared-recoil' -export async function isAnyNamespaceAuthorized(resourceAttributes: ResourceAttributes[], namespaces: Namespace[]) { +const SELF_ACCESS_CHECK_BATCH_SIZE = 40 + +export function isAnyNamespaceAuthorized(resourceAttributes: Promise, namespaces: Namespace[]) { const namespaceList: string[] = namespaces.map((namespace) => namespace.metadata.name!) if (namespaceList.length === 0) { - return false + return { promise: Promise.resolve(false) } } - const adminAccessRequest = await checkAdminAccess() - const isAdmin = adminAccessRequest?.status?.allowed ?? false - if (isAdmin) { - return true + let abortFn: () => void | undefined + const abort = () => { + abortFn?.() } - const resourceList: Array = [] - namespaceList.forEach((namespace) => { - resourceList.push(...resourceAttributes.map((attribute) => ({ ...attribute, namespace }))) - }) + return { + promise: resourceAttributes.then((resourceAttributes) => + checkAdminAccess().then((adminAccessRequest) => { + if (adminAccessRequest?.status?.allowed) { + return true + } else { + const resourceList: Array = [] + + namespaceList.forEach((namespace) => { + resourceList.push({ ...resourceAttributes, namespace }) + }) + + // eslint-disable-next-line no-inner-declarations + async function processBatch(): Promise { + const nextBatch = resourceList.splice(0, SELF_ACCESS_CHECK_BATCH_SIZE) + const results = nextBatch.map((resource) => { + return createSubjectAccessReview(resource) + }) + abortFn = () => results.forEach((result) => result.abort()) + try { + // short-circuit as soon as any namespace says the access is allowed + return await Promise.any( + results.map((result) => + result.promise.then((result) => { + if (result.status?.allowed) { + abort() + return true + } else { + throw new Error('access not allowed') + } + }) + ) + ) + } catch { + if (resourceList.length) { + return processBatch() + } else { + return false + } + } + } + return processBatch() + } + }) + ), + abort, + } +} + +export function areAllNamespacesUnauthorized(resourceAttributes: Promise, namespaces: Namespace[]) { + const namespaceList: string[] = namespaces.map((namespace) => namespace.metadata.name!) + + if (namespaceList.length === 0) { + return { promise: Promise.resolve(true) } + } + + let abortFn: () => void | undefined + const abort = () => { + abortFn?.() + } + + return { + promise: resourceAttributes.then((resourceAttributes) => + checkAdminAccess().then((adminAccessRequest) => { + if (adminAccessRequest?.status?.allowed) { + return false + } else { + const resourceList: Array = [] - try { - return await createShortCircuitSubjectAccessReviews(resourceList).promise - } catch { - return false + namespaceList.forEach((namespace) => { + resourceList.push({ ...resourceAttributes, namespace }) + }) + + // eslint-disable-next-line no-inner-declarations + async function processBatch(): Promise { + const nextBatch = resourceList.splice(0, SELF_ACCESS_CHECK_BATCH_SIZE) + const results = nextBatch.map((resource) => { + return createSubjectAccessReview(resource) + }) + abortFn = () => results.forEach((result) => result.abort()) + try { + // short-circuit as soon as any namespace says the access is allowed + return await Promise.all( + results.map((result) => + result.promise.then((result) => { + if (result.status && !result.status.allowed) { + return true + } else { + abort() + throw new Error('access is allowed') + } + }) + ) + ).then(() => { + if (resourceList.length) { + return processBatch() + } else { + return true + } + }) + } catch { + return false + } + } + return processBatch() + } + }) + ), + abort, } } @@ -141,12 +243,26 @@ export function canUser( return selfSubjectAccessReview } -export async function checkPermission( - resourceAttributes: Promise, - setStateFn: (state: boolean) => void, - namespaces: Namespace[] -) { - setStateFn(namespaces.length ? await isAnyNamespaceAuthorized([await resourceAttributes], namespaces) : false) +export function useIsAnyNamespaceAuthorized(resourceAttributes: Promise) { + const { namespacesState } = useSharedAtoms() + const namespaces = useRecoilValue(namespacesState) + const [someNamespaceIsAuthorized, setSomeNamespaceIsAuthorized] = useState(false) + + useEffect(() => { + const result = (someNamespaceIsAuthorized ? areAllNamespacesUnauthorized : isAnyNamespaceAuthorized)( + resourceAttributes, + namespaces + ) + result.promise.then((flipAuthorization) => { + if (flipAuthorization) setSomeNamespaceIsAuthorized(!someNamespaceIsAuthorized) + }) + + return () => result.abort?.() + // exclude someNamespaceIsAuthorized from dependency list to avoid update loop + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resourceAttributes, namespaces]) + + return someNamespaceIsAuthorized } export function rbacResourceTestHelper( diff --git a/frontend/src/resources/self-subject-access-review.ts b/frontend/src/resources/self-subject-access-review.ts index 66c9afcbad8..3b6979a6799 100644 --- a/frontend/src/resources/self-subject-access-review.ts +++ b/frontend/src/resources/self-subject-access-review.ts @@ -61,23 +61,3 @@ export function createSubjectAccessReviews(resourceAttributes: Array results.forEach((result) => result.abort()), } } - -export function createShortCircuitSubjectAccessReviews(resourceAttributes: Array) { - const results = resourceAttributes.map((resource) => createSubjectAccessReview(resource)) - const abort = () => results.forEach((result) => result.abort()) - return { - promise: Promise.any( - results.map((result) => - result.promise.then((result) => { - if (result.status?.allowed) { - abort() - return true - } else { - throw new Error('access not allowed') - } - }) - ) - ), - abort, - } -} diff --git a/frontend/src/routes/Applications/AdvancedConfiguration.tsx b/frontend/src/routes/Applications/AdvancedConfiguration.tsx index 0a703421ba2..3f93cd5d2a7 100644 --- a/frontend/src/routes/Applications/AdvancedConfiguration.tsx +++ b/frontend/src/routes/Applications/AdvancedConfiguration.tsx @@ -51,7 +51,6 @@ export default function AdvancedConfiguration(props: AdvancedConfigurationPagePr const { applicationsState, channelsState, - namespacesState, placementDecisionsState, placementsState, placementRulesState, @@ -64,7 +63,6 @@ export default function AdvancedConfiguration(props: AdvancedConfigurationPagePr const placements = useRecoilValue(placementsState) const placementDecisions = useRecoilValue(placementDecisionsState) const subscriptions = useRecoilValue(subscriptionsState) - const namespaces = useRecoilValue(namespacesState) const subscriptionsWithoutLocal = subscriptions.filter((subscription) => { return !_.endsWith(subscription.metadata.name, '-local') @@ -816,7 +814,6 @@ export default function AdvancedConfiguration(props: AdvancedConfigurationPagePr table={table} keyFn={keyFn} t={t} - namespaces={namespaces} defaultToggleOption={props.defaultToggleOption} /> } diff --git a/frontend/src/routes/Applications/Overview.tsx b/frontend/src/routes/Applications/Overview.tsx index 7dabfbae79a..a77f7763135 100644 --- a/frontend/src/routes/Applications/Overview.tsx +++ b/frontend/src/routes/Applications/Overview.tsx @@ -13,7 +13,7 @@ import { import { ExternalLinkAltIcon } from '@patternfly/react-icons' import { cellWidth } from '@patternfly/react-table' import { get } from 'lodash' -import { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { useCallback, useContext, useMemo, useState } from 'react' import { TFunction } from 'react-i18next' import { generatePath, Link, useNavigate } from 'react-router-dom-v5-compat' import { HighlightSearchText } from '../../components/HighlightSearchText' @@ -21,7 +21,7 @@ import { Pages, usePageVisitMetricHandler } from '../../hooks/console-metrics' import { Trans, useTranslation } from '../../lib/acm-i18next' import { DOC_LINKS, ViewDocumentationLink } from '../../lib/doc-util' import { PluginContext } from '../../lib/PluginContext' -import { checkPermission, rbacCreate, rbacDelete } from '../../lib/rbac-util' +import { rbacCreate, rbacDelete, useIsAnyNamespaceAuthorized } from '../../lib/rbac-util' import { fetchAggregate, IRequestListView, SupportedAggregate, useAggregate } from '../../lib/useAggregates' import { NavigationPath } from '../../NavigationPath' import { @@ -314,7 +314,6 @@ export default function ApplicationsOverview() { applicationsState, argoApplicationsState, channelsState, - namespacesState, placementRulesState, placementsState, placementDecisionsState, @@ -329,7 +328,6 @@ export default function ApplicationsOverview() { const placementRules = useRecoilValue(placementRulesState) const placements = useRecoilValue(placementsState) const placementDecisions = useRecoilValue(placementDecisionsState) - const namespaces = useRecoilValue(namespacesState) const { acmExtensions } = useContext(PluginContext) const { dataContext } = useContext(PluginContext) const { backendUrl } = useContext(dataContext) @@ -781,9 +779,9 @@ export default function ApplicationsOverview() { ) const navigate = useNavigate() - const [canCreateApplication, setCanCreateApplication] = useState(false) - const [canDeleteApplication, setCanDeleteApplication] = useState(false) - const [canDeleteApplicationSet, setCanDeleteApplicationSet] = useState(false) + const canCreateApplication = useIsAnyNamespaceAuthorized(rbacCreate(ApplicationDefinition)) + const canDeleteApplication = useIsAnyNamespaceAuthorized(rbacDelete(ApplicationDefinition)) + const canDeleteApplicationSet = useIsAnyNamespaceAuthorized(rbacDelete(ApplicationSetDefinition)) const rowActionResolver = useCallback( (resource: IResource) => { @@ -990,16 +988,6 @@ export default function ApplicationsOverview() { ] ) - useEffect(() => { - checkPermission(rbacCreate(ApplicationDefinition), setCanCreateApplication, namespaces) - }, [namespaces]) - useEffect(() => { - checkPermission(rbacDelete(ApplicationDefinition), setCanDeleteApplication, namespaces) - }, [namespaces]) - useEffect(() => { - checkPermission(rbacDelete(ApplicationSetDefinition), setCanDeleteApplicationSet, namespaces) - }, [namespaces]) - const appCreationButton = useMemo( () => ( { modalProps: IDeleteResourceModalProps | { open: false } table: any t: TFunction - namespaces: Namespace[] defaultToggleOption?: ApplicationToggleOptions } export type ApplicationToggleOptions = 'subscriptions' | 'channels' | 'placements' | 'placementrules' @@ -33,14 +32,10 @@ export function ToggleSelector(props: IToggleSelectorProps) { { id: 'placements', title: t('Placements'), emptyMessage: t("You don't have any placements") }, { id: 'placementrules', title: t('Placement rules'), emptyMessage: t("You don't have any placement rules") }, ] as const - const [canCreateApplication, setCanCreateApplication] = useState(false) + const canCreateApplication = useIsAnyNamespaceAuthorized(rbacCreate(ApplicationDefinition)) const selectedId = getSelectedId({ location, options, defaultOption, queryParam: 'resources' }) const selectedResources = _.get(props.table, `${selectedId}`) - useEffect(() => { - checkPermission(rbacCreate(ApplicationDefinition), setCanCreateApplication, props.namespaces) - }, [props.namespaces]) - return ( diff --git a/frontend/src/routes/Credentials/CredentialsPage.tsx b/frontend/src/routes/Credentials/CredentialsPage.tsx index 4ad3dba7e57..1c6ee0893bc 100644 --- a/frontend/src/routes/Credentials/CredentialsPage.tsx +++ b/frontend/src/routes/Credentials/CredentialsPage.tsx @@ -14,14 +14,14 @@ import { ProviderLongTextMap, } from '../../ui-components' import moment from 'moment' -import { Fragment, useEffect, useMemo, useState } from 'react' +import { Fragment, useMemo, useState } from 'react' import { Link, generatePath, useNavigate } from 'react-router-dom-v5-compat' import { useRecoilValue, useSharedAtoms } from '../../shared-recoil' import { BulkActionModal, BulkActionModalProps } from '../../components/BulkActionModal' import { RbacDropdown } from '../../components/Rbac' import { Trans, useTranslation } from '../../lib/acm-i18next' import { DOC_LINKS, ViewDocumentationLink } from '../../lib/doc-util' -import { checkPermission, rbacCreate, rbacDelete, rbacPatch } from '../../lib/rbac-util' +import { rbacCreate, rbacDelete, rbacPatch, useIsAnyNamespaceAuthorized } from '../../lib/rbac-util' import { getBackCancelLocationLinkProps, navigateToBackCancelLocation, NavigationPath } from '../../NavigationPath' import { deleteResource, @@ -81,13 +81,8 @@ export function CredentialsTable(props: { const [modalProps, setModalProps] = useState | { open: false }>({ open: false, }) - const { namespacesState } = useSharedAtoms() const unauthorizedMessage = t('rbac.unauthorized') - const namespaces = useRecoilValue(namespacesState) - const [canAddCredential, setCanAddCredential] = useState(false) - useEffect(() => { - checkPermission(rbacCreate(SecretDefinition), setCanAddCredential, namespaces) - }, [namespaces]) + const canAddCredential = useIsAnyNamespaceAuthorized(rbacCreate(SecretDefinition)) sessionStorage.removeItem('DiscoveryCredential') diff --git a/frontend/src/routes/Governance/overview/Overview.tsx b/frontend/src/routes/Governance/overview/Overview.tsx index 1c2b6ef3940..4aadd49b373 100644 --- a/frontend/src/routes/Governance/overview/Overview.tsx +++ b/frontend/src/routes/Governance/overview/Overview.tsx @@ -1,11 +1,11 @@ /* Copyright Contributors to the Open Cluster Management project */ import { Button, ButtonVariant, Card, CardBody, CardTitle, PageSection, Stack, Tooltip } from '@patternfly/react-core' import { CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon } from '@patternfly/react-icons' -import { Fragment, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { Fragment, useCallback, useContext, useMemo, useState } from 'react' import { AcmMasonry } from '../../../components/AcmMasonry' import { Pages, usePageVisitMetricHandler } from '../../../hooks/console-metrics' import { useTranslation } from '../../../lib/acm-i18next' -import { checkPermission, rbacCreate } from '../../../lib/rbac-util' +import { rbacCreate, useIsAnyNamespaceAuthorized } from '../../../lib/rbac-util' import { ManagedCluster, Policy, PolicyDefinition } from '../../../resources' import { useRecoilValue, useSharedAtoms } from '../../../shared-recoil' import { AcmDrawerContext, compareStrings } from '../../../ui-components' @@ -21,15 +21,11 @@ import { SecurityGroupPolicySummarySidebar } from './SecurityGroupPolicySummaryS export default function GovernanceOverview() { usePageVisitMetricHandler(Pages.governance) - const { usePolicies, namespacesState } = useSharedAtoms() + const { usePolicies } = useSharedAtoms() const policies = usePolicies() - const namespaces = useRecoilValue(namespacesState) const policyViolationSummary = usePolicyViolationSummary(policies) - const [canCreatePolicy, setCanCreatePolicy] = useState(false) + const canCreatePolicy = useIsAnyNamespaceAuthorized(rbacCreate(PolicyDefinition)) const { t } = useTranslation() - useEffect(() => { - checkPermission(rbacCreate(PolicyDefinition), setCanCreatePolicy, namespaces) - }, [namespaces]) if (policies.length === 0) { return ( diff --git a/frontend/src/routes/Governance/policies/Policies.tsx b/frontend/src/routes/Governance/policies/Policies.tsx index fb80e92d125..fcd7b68c2db 100644 --- a/frontend/src/routes/Governance/policies/Policies.tsx +++ b/frontend/src/routes/Governance/policies/Policies.tsx @@ -23,7 +23,7 @@ import { generatePath, useNavigate } from 'react-router-dom-v5-compat' import { BulkActionModal, BulkActionModalProps } from '../../../components/BulkActionModal' import { useTranslation } from '../../../lib/acm-i18next' import { deletePolicy } from '../../../lib/delete-policy' -import { checkPermission, rbacCreate, rbacPatch, rbacUpdate } from '../../../lib/rbac-util' +import { rbacCreate, rbacUpdate, rbacPatch, useIsAnyNamespaceAuthorized } from '../../../lib/rbac-util' import { NavigationPath } from '../../../NavigationPath' import { patchResource, @@ -118,17 +118,10 @@ export default function PoliciesPage() { ) const policyClusterViolationsColumn = usePolicyViolationsColumn(policyClusterViolationSummaryMap) const [modal, setModal] = useState() - const [canCreatePolicy, setCanCreatePolicy] = useState(false) - const [canPatchPolicy, setCanPatchPolicy] = useState(false) - const [canCreatePolicyAutomation, setCanCreatePolicyAutomation] = useState(false) - const [canUpdatePolicyAutomation, setCanUpdatePolicyAutomation] = useState(false) - - useEffect(() => { - checkPermission(rbacCreate(PolicyDefinition), setCanCreatePolicy, namespaces) - checkPermission(rbacPatch(PolicyDefinition), setCanPatchPolicy, namespaces) - checkPermission(rbacCreate(PolicyAutomationDefinition), setCanCreatePolicyAutomation, namespaces) - checkPermission(rbacUpdate(PolicyAutomationDefinition), setCanUpdatePolicyAutomation, namespaces) - }, [namespaces]) + const canCreatePolicy = useIsAnyNamespaceAuthorized(rbacCreate(PolicyDefinition)) + const canPatchPolicy = useIsAnyNamespaceAuthorized(rbacPatch(PolicyDefinition)) + const canCreatePolicyAutomation = useIsAnyNamespaceAuthorized(rbacCreate(PolicyAutomationDefinition)) + const canUpdatePolicyAutomation = useIsAnyNamespaceAuthorized(rbacUpdate(PolicyAutomationDefinition)) const policyColumns = useMemo[]>( () => [ diff --git a/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsOverview.tsx b/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsOverview.tsx index 4bd1f4fbd73..f1c3d55f5e2 100644 --- a/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsOverview.tsx +++ b/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsOverview.tsx @@ -3,11 +3,11 @@ import { Alert, ButtonVariant, LabelGroup, PageSection, Stack, Text, TextVariant import { CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon } from '@patternfly/react-icons' import { AcmButton, AcmDescriptionList, AcmDrawerContext, AcmTable } from '../../../../ui-components' import moment from 'moment' -import { ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { ReactNode, useCallback, useContext, useMemo, useState } from 'react' import { Link, generatePath } from 'react-router-dom-v5-compat' import { useRecoilValue, useSharedAtoms } from '../../../../shared-recoil' import { useTranslation } from '../../../../lib/acm-i18next' -import { checkPermission, rbacCreate, rbacUpdate } from '../../../../lib/rbac-util' +import { rbacCreate, rbacUpdate, useIsAnyNamespaceAuthorized } from '../../../../lib/rbac-util' import { NavigationPath } from '../../../../NavigationPath' import { Placement, @@ -46,7 +46,6 @@ export default function PolicyDetailsOverview() { const { t } = useTranslation() const { setDrawerContext } = useContext(AcmDrawerContext) const { - namespacesState, placementBindingsState, placementDecisionsState, placementRulesState, @@ -60,7 +59,6 @@ export default function PolicyDetailsOverview() { const placementRules = useRecoilValue(placementRulesState) const placementDecisions = useRecoilValue(placementDecisionsState) const policyAutomations = useRecoilValue(policyAutomationState) - const namespaces = useRecoilValue(namespacesState) const policies = usePropagatedPolicies(policy) const govData = useGovernanceData([policy]) const clusterRiskScore = @@ -74,13 +72,8 @@ export default function PolicyDetailsOverview() { (pa: PolicyAutomation) => pa.spec.policyRef === policy.metadata.name ) const [modal, setModal] = useState() - const [canCreatePolicyAutomation, setCanCreatePolicyAutomation] = useState(false) - const [canUpdatePolicyAutomation, setCanUpdatePolicyAutomation] = useState(false) - - useEffect(() => { - checkPermission(rbacCreate(PolicyAutomationDefinition), setCanCreatePolicyAutomation, namespaces) - checkPermission(rbacUpdate(PolicyAutomationDefinition), setCanUpdatePolicyAutomation, namespaces) - }, [namespaces]) + const canCreatePolicyAutomation = useIsAnyNamespaceAuthorized(rbacCreate(PolicyAutomationDefinition)) + const canUpdatePolicyAutomation = useIsAnyNamespaceAuthorized(rbacUpdate(PolicyAutomationDefinition)) const { leftItems, rightItems } = useMemo(() => { const unauthorizedMessage = !canCreatePolicyAutomation || !canUpdatePolicyAutomation ? t('rbac.unauthorized') : '' diff --git a/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsResults.tsx b/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsResults.tsx index 8d8f0c87fa6..33adb8af3ab 100644 --- a/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsResults.tsx +++ b/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsResults.tsx @@ -3,11 +3,11 @@ import { PageSection, Title, Tooltip } from '@patternfly/react-core' import { CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon } from '@patternfly/react-icons' import { AcmEmptyState, AcmTable, AcmTablePaginationContextProvider, compareStrings } from '../../../../ui-components' import moment from 'moment' -import { ReactNode, useEffect, useMemo, useState } from 'react' +import { ReactNode, useMemo } from 'react' import { Link, generatePath } from 'react-router-dom-v5-compat' import { useRecoilValue, useSharedAtoms } from '../../../../shared-recoil' import { useTranslation } from '../../../../lib/acm-i18next' -import { checkPermission, rbacCreate } from '../../../../lib/rbac-util' +import { rbacCreate, useIsAnyNamespaceAuthorized } from '../../../../lib/rbac-util' import { transformBrowserUrlToFilterPresets } from '../../../../lib/urlQuery' import { NavigationPath, UNKNOWN_NAMESPACE } from '../../../../NavigationPath' import { getGroupFromApiVersion, Policy, PolicyDefinition, PolicyStatusDetails } from '../../../../resources' @@ -86,14 +86,9 @@ export default function PolicyDetailsResults() { const { t } = useTranslation() const filterPresets = transformBrowserUrlToFilterPresets(window.location.search) const { policy } = usePolicyDetailsContext() - const { namespacesState, policiesState } = useSharedAtoms() + const { policiesState } = useSharedAtoms() const policies = useRecoilValue(policiesState) - const namespaces = useRecoilValue(namespacesState) - const [canCreatePolicy, setCanCreatePolicy] = useState(false) - - useEffect(() => { - checkPermission(rbacCreate(PolicyDefinition), setCanCreatePolicy, namespaces) - }, [namespaces]) + const canCreatePolicy = useIsAnyNamespaceAuthorized(rbacCreate(PolicyDefinition)) const policiesDeployedOnCluster: ResultsTableData[] = useMemo(() => { const policyName = policy.metadata.name ?? '' diff --git a/frontend/src/routes/Governance/policy-sets/PolicySets.tsx b/frontend/src/routes/Governance/policy-sets/PolicySets.tsx index 9659e394aa0..494706b947f 100644 --- a/frontend/src/routes/Governance/policy-sets/PolicySets.tsx +++ b/frontend/src/routes/Governance/policy-sets/PolicySets.tsx @@ -13,7 +13,7 @@ import { Link } from 'react-router-dom-v5-compat' import { AcmMasonry } from '../../../components/AcmMasonry' import { useTranslation } from '../../../lib/acm-i18next' import { usePaginationTitles } from '../../../lib/paginationStrings' -import { checkPermission, rbacCreate, rbacDelete, rbacUpdate } from '../../../lib/rbac-util' +import { rbacCreate, rbacDelete, rbacUpdate, useIsAnyNamespaceAuthorized } from '../../../lib/rbac-util' import { transformBrowserUrlToFilterPresets } from '../../../lib/urlQuery' import { NavigationPath } from '../../../NavigationPath' import { PolicySet, PolicySetDefinition } from '../../../resources/policy-set' @@ -65,9 +65,8 @@ export default function PolicySetsPage() { const { t } = useTranslation() const presets = transformBrowserUrlToFilterPresets(window.location.search) const { presetNames, presetNs } = getPresetURIFilters(presets.initialSearch) - const { namespacesState, policySetsState } = useSharedAtoms() + const { policySetsState } = useSharedAtoms() const policySets = useRecoilValue(policySetsState) - const namespaces = useRecoilValue(namespacesState) const [searchFilter, setSearchFilter] = useState>({ Name: presetNames, Namespace: presetNs, @@ -77,18 +76,12 @@ export default function PolicySetsPage() { const [perPage, setPerPage] = useState(10) const [filteredPolicySets, setFilteredPolicySets] = useState(policySets) const [selectedCardID, setSelectedCardID] = useState('') - const [canCreatePolicySet, setCanCreatePolicySet] = useState(false) - const [canEditPolicySet, setCanEditPolicySet] = useState(false) - const [canDeletePolicySet, setCanDeletePolicySet] = useState(false) + const canCreatePolicySet = useIsAnyNamespaceAuthorized(rbacCreate(PolicySetDefinition)) + const canEditPolicySet = useIsAnyNamespaceAuthorized(rbacUpdate(PolicySetDefinition)) + const canDeletePolicySet = useIsAnyNamespaceAuthorized(rbacDelete(PolicySetDefinition)) const translatedPaginationTitles = usePaginationTitles() - useEffect(() => { - checkPermission(rbacCreate(PolicySetDefinition), setCanCreatePolicySet, namespaces) - checkPermission(rbacUpdate(PolicySetDefinition), setCanEditPolicySet, namespaces) - checkPermission(rbacDelete(PolicySetDefinition), setCanDeletePolicySet, namespaces) - }, [namespaces]) - const updatePerPage = useCallback( (newPerPage: number) => { // keep the first item in view on pagination size change diff --git a/frontend/src/routes/Infrastructure/Automations/AnsibleAutomations.tsx b/frontend/src/routes/Infrastructure/Automations/AnsibleAutomations.tsx index 3266bc48c90..bbf403560aa 100644 --- a/frontend/src/routes/Infrastructure/Automations/AnsibleAutomations.tsx +++ b/frontend/src/routes/Infrastructure/Automations/AnsibleAutomations.tsx @@ -14,13 +14,13 @@ import { } from '../../../ui-components' import { Fragment, useContext, useEffect, useState } from 'react' import { Link, generatePath, useNavigate } from 'react-router-dom-v5-compat' -import { useRecoilValue, useSharedAtoms, useSharedSelectors } from '../../../shared-recoil' +import { useRecoilValue, useSharedSelectors } from '../../../shared-recoil' import { BulkActionModal, BulkActionModalProps } from '../../../components/BulkActionModal' import { DropdownActionModal, IDropdownActionModalProps } from '../../../components/DropdownActionModal' import { RbacDropdown } from '../../../components/Rbac' import { Trans, useTranslation } from '../../../lib/acm-i18next' import { DOC_LINKS, ViewDocumentationLink } from '../../../lib/doc-util' -import { checkPermission, rbacCreate, rbacDelete, rbacPatch } from '../../../lib/rbac-util' +import { rbacCreate, rbacDelete, rbacPatch, useIsAnyNamespaceAuthorized } from '../../../lib/rbac-util' import { getBackCancelLocationLinkProps, NavigationPath } from '../../../NavigationPath' import { ClusterCurator, @@ -68,12 +68,7 @@ function AnsibleJobTemplateTable() { }) const { t } = useTranslation() const unauthorizedMessage = t('rbac.unauthorized') - const { namespacesState } = useSharedAtoms() - const namespaces = useRecoilValue(namespacesState) - const [canCreateAutomationTemplate, setCanCreateAutomationTemplate] = useState(false) - useEffect(() => { - checkPermission(rbacCreate(ClusterCuratorDefinition), setCanCreateAutomationTemplate, namespaces) - }, [namespaces]) + const canCreateAutomationTemplate = useIsAnyNamespaceAuthorized(rbacCreate(ClusterCuratorDefinition)) const navigate = useNavigate() // Set table diff --git a/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/NodePoolsProgress.tsx b/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/NodePoolsProgress.tsx index 5de2223db4a..aa61e26af77 100644 --- a/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/NodePoolsProgress.tsx +++ b/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/NodePoolsProgress.tsx @@ -1,5 +1,5 @@ /* Copyright Contributors to the Open Cluster Management project */ -import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' +import { ReactNode, useCallback, useMemo, useState } from 'react' import { ButtonVariant, ExpandableSectionToggle, @@ -22,8 +22,7 @@ import { AcmButton } from '../../../../../ui-components' import { AddNodePoolModal } from './AddNodePoolModal' import { useClusterDetailsContext } from '../ClusterDetails/ClusterDetails' import { HypershiftCloudPlatformType } from '../../../../../resources/utils/constants' -import { checkPermission, rbacCreate } from '../../../../../lib/rbac-util' -import { useRecoilValue, useSharedAtoms } from '../../../../../shared-recoil' +import { rbacCreate, useIsAnyNamespaceAuthorized } from '../../../../../lib/rbac-util' import { onToggle } from '../utils/utils' export type NodePoolStatus = { @@ -88,15 +87,9 @@ const NodePoolsProgress = ({ nodePools, ...rest }: NodePoolsProgressProps) => { () => toggleOpenAddNodepoolModal(!openAddNodepoolModal), [openAddNodepoolModal] ) - const [canCreateNodepool, setCanCreateNodepool] = useState(false) - const { namespacesState } = useSharedAtoms() - const namespaces = useRecoilValue(namespacesState) + const canCreateNodepool = useIsAnyNamespaceAuthorized(rbacCreate(NodePoolDefinition)) const nodepoolList = nodePools.map((nodePool) => nodePool.metadata?.name) as string[] - useEffect(() => { - checkPermission(rbacCreate(NodePoolDefinition), setCanCreateNodepool, namespaces) - }, [namespaces]) - const addNodePoolStatusMessage = useMemo(() => { if (hostedCluster?.spec?.platform?.type !== HypershiftCloudPlatformType.AWS) { return t('Add node pool is only supported for AWS. Use the hcp CLI to add additional node pools.') diff --git a/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/NodePoolsTable.tsx b/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/NodePoolsTable.tsx index 87407aabf9b..249de963ea6 100644 --- a/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/NodePoolsTable.tsx +++ b/frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/NodePoolsTable.tsx @@ -1,7 +1,7 @@ /* Copyright Contributors to the Open Cluster Management project */ import { ButtonVariant, Stack, StackItem, Text } from '@patternfly/react-core' import { CheckCircleIcon, InProgressIcon } from '@patternfly/react-icons' -import { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { useCallback, useContext, useMemo, useState } from 'react' import { ClusterImageSetK8sResource, NodePoolK8sResource } from '@openshift-assisted/ui-lib/cim' import { useTranslation, Trans } from '../../../../../lib/acm-i18next' import { AcmButton, AcmEmptyState, AcmTable, IAcmRowAction, IAcmTableColumn } from '../../../../../ui-components' @@ -13,8 +13,7 @@ import { DistributionField } from './DistributionField' import { AddNodePoolModal } from './AddNodePoolModal' import { IManageNodePoolNodesModalProps, ManageNodePoolNodesModal } from './ManageNodePoolNodesModal' import { IRemoveNodePoolModalProps, RemoveNodePoolModal } from './RemoveNodePoolModal' -import { useSharedAtoms, useRecoilValue } from '../../../../../shared-recoil' -import { checkPermission, rbacCreate, rbacDelete, rbacPatch } from '../../../../../lib/rbac-util' +import { rbacCreate, rbacDelete, rbacPatch, useIsAnyNamespaceAuthorized } from '../../../../../lib/rbac-util' import { NodePoolTableWidthContext } from './HypershiftClusterInstallProgress' @@ -52,11 +51,9 @@ const NodePoolsTable = ({ nodePools, clusterImages }: NodePoolsTableProps): JSX. open: false, } ) - const [canCreateNodepool, setCanCreateNodepool] = useState(false) - const [canDeleteNodepool, setCanDeleteNodepool] = useState(false) - const [canPatchNodepool, setCanPatchNodepool] = useState(false) - const { namespacesState } = useSharedAtoms() - const namespaces = useRecoilValue(namespacesState) + const canCreateNodepool = useIsAnyNamespaceAuthorized(rbacCreate(NodePoolDefinition)) + const canDeleteNodepool = useIsAnyNamespaceAuthorized(rbacDelete(NodePoolDefinition)) + const canPatchNodepool = useIsAnyNamespaceAuthorized(rbacPatch(NodePoolDefinition)) const renderNodepoolStatus = useCallback( (nodepool: NodePool) => { @@ -307,16 +304,6 @@ const NodePoolsTable = ({ nodePools, clusterImages }: NodePoolsTableProps): JSX. [hostedCluster, nodePools.length, t, canDeleteNodepool, canPatchNodepool, cluster?.hypershift?.isUpgrading] ) - useEffect(() => { - checkPermission(rbacCreate(NodePoolDefinition), setCanCreateNodepool, namespaces) - }, [namespaces]) - useEffect(() => { - checkPermission(rbacDelete(NodePoolDefinition), setCanDeleteNodepool, namespaces) - }, [namespaces]) - useEffect(() => { - checkPermission(rbacPatch(NodePoolDefinition), setCanPatchNodepool, namespaces) - }, [namespaces]) - const addNodepoolButton = useMemo( () => (