diff --git a/pkg/query/configuration/objectkind.go b/pkg/query/configuration/objectkind.go index 3d0c1773d..493779090 100644 --- a/pkg/query/configuration/objectkind.go +++ b/pkg/query/configuration/objectkind.go @@ -183,7 +183,7 @@ var ( AddToSchemeFunc: rbacv1.AddToScheme, } - PolicyAgentEventObjectKind = ObjectKind{ + PolicyAgentAuditEventObjectKind = ObjectKind{ Gvk: corev1.SchemeGroupVersion.WithKind("Event"), NewClientObjectFunc: func() client.Object { return &corev1.Event{} @@ -195,7 +195,7 @@ var ( return false } - return e.Source.Component == "policy-agent" + return e.Labels["pac.weave.works/type"] == "Audit" && e.Source.Component == "policy-agent" }, RetentionPolicy: RetentionPolicy(24 * time.Hour), StatusFunc: func(obj client.Object) ObjectStatus { @@ -231,7 +231,7 @@ var SupportedObjectKinds = []ObjectKind{ GitRepositoryObjectKind, OCIRepositoryObjectKind, BucketObjectKind, - PolicyAgentEventObjectKind, + PolicyAgentAuditEventObjectKind, } // SupportedRbacKinds list with the default supported RBAC resources. diff --git a/ui-cra/src/components/Clusters/index.tsx b/ui-cra/src/components/Clusters/index.tsx index 42207ae0e..4182a51a7 100644 --- a/ui-cra/src/components/Clusters/index.tsx +++ b/ui-cra/src/components/Clusters/index.tsx @@ -9,19 +9,16 @@ import { Kind, KubeStatusIndicator, Link, - PolicyViolationsList, - RouterTab, - SubRouterTabs, filterByStatusCallback, filterConfig, statusSortHelper, - useListSources, + useListSources } from '@weaveworks/weave-gitops'; import { Source } from '@weaveworks/weave-gitops/ui/lib/objects'; import { PageRoute } from '@weaveworks/weave-gitops/ui/lib/types'; import _ from 'lodash'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { useHistory, useRouteMatch } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; import { GitProvider } from '../../api/gitauth/gitauth.pb'; import EKS from '../../assets/img/EKS.svg'; @@ -163,7 +160,6 @@ export const getGitRepos = (sources: Source[] | undefined) => const MCCP: FC<{ location: { state: { notification: NotificationData[] } }; }> = ({ location }) => { - const { path } = useRouteMatch(); const { clusters, isLoading } = useClusters(); const { setNotifications } = useNotifications(); const [selectedCluster, setSelectedCluster] = @@ -389,95 +385,85 @@ const MCCP: FC<{ )} - - - - ( - - ), - maxWidth: 25, - }, - { - label: 'Name', - value: (c: GitopsClusterEnriched) => - c.controlPlane === true ? ( - {c.name} - ) : ( - - {c.name} - - ), - sortValue: ({ name }) => name, - textSearchable: true, - maxWidth: 275, - }, - { - label: 'Dashboards', - value: (c: GitopsClusterEnriched) => ( - - ), - }, - { - label: 'Type', - value: (c: GitopsClusterEnriched) => ( - - ), - }, - { - label: 'Namespace', - value: 'namespace', - }, - { - label: 'Status', - value: (c: GitopsClusterEnriched) => - c.conditions && c.conditions.length > 0 ? ( - - ) : null, - sortValue: statusSortHelper, - }, - { - label: 'Message', - value: (c: GitopsClusterEnriched) => - (c.conditions && c.conditions[0]?.message) || null, - sortValue: ({ conditions }) => computeMessage(conditions), - maxWidth: 600, - }, - { - label: '', - value: (c: GitopsClusterEnriched) => ( - - ), - }, - ]} - /> - - - - - - + + ( + + ), + maxWidth: 25, + }, + { + label: 'Name', + value: (c: GitopsClusterEnriched) => + c.controlPlane === true ? ( + {c.name} + ) : ( + + {c.name} + + ), + sortValue: ({ name }) => name, + textSearchable: true, + maxWidth: 275, + }, + { + label: 'Dashboards', + value: (c: GitopsClusterEnriched) => ( + + ), + }, + { + label: 'Type', + value: (c: GitopsClusterEnriched) => ( + + ), + }, + { + label: 'Namespace', + value: 'namespace', + }, + { + label: 'Status', + value: (c: GitopsClusterEnriched) => + c.conditions && c.conditions.length > 0 ? ( + + ) : null, + sortValue: statusSortHelper, + }, + { + label: 'Message', + value: (c: GitopsClusterEnriched) => + (c.conditions && c.conditions[0]?.message) || null, + sortValue: ({ conditions }) => computeMessage(conditions), + maxWidth: 600, + }, + { + label: '', + value: (c: GitopsClusterEnriched) => ( + + ), + }, + ]} + /> + diff --git a/ui-cra/src/components/Policies/Audit/AuditTable.tsx b/ui-cra/src/components/Policies/Audit/AuditTable.tsx new file mode 100644 index 000000000..23b7699c5 --- /dev/null +++ b/ui-cra/src/components/Policies/Audit/AuditTable.tsx @@ -0,0 +1,192 @@ +import { + DataTable, + Flex, + Icon, + IconType, + Link, + RequestStateHandler, + Severity, + Text, + Timestamp, + V2Routes, + formatURL, +} from '@weaveworks/weave-gitops'; +import { useListFacets } from '../../../hooks/query'; +import { QueryState, columnHeaderHandler } from '../../Explorer/hooks'; + +import { IconButton } from '@material-ui/core'; +import _ from 'lodash'; +import qs from 'query-string'; +import { useEffect, useState } from 'react'; +import { useHistory } from 'react-router'; +import { QueryResponse } from '../../../api/query/query.pb'; +import { RequestError } from '../../../types/custom'; +import FilterDrawer from '../../Explorer/FilterDrawer'; +import Filters from '../../Explorer/Filters'; +import PaginationControls from '../../Explorer/PaginationControls'; +import QueryInput from '../../Explorer/QueryInput'; +import QueryStateChips from '../../Explorer/QueryStateChips'; +import { TableWrapper } from '../../Shared'; + +type AuditProps = { + data: QueryResponse | undefined; + queryState: QueryState; + setQueryState: (queryState: QueryState) => void; +}; + +export const AuditTable = ({ data, queryState, setQueryState }: AuditProps) => { + const history = useHistory(); + const [filterDrawerOpen, setFilterDrawerOpen] = useState(false); + const [showTable, setshowTable] = useState(false); + + const { data: facetsRes, error, isLoading } = useListFacets(); + const filteredFacets = facetsRes?.facets?.filter(f => f.field !== 'kind'); + const rows = data?.objects?.map(obj => { + const { unstructured, cluster } = obj; + const details = JSON.parse(unstructured || ''); + const { + metadata: { + annotations: { category, policy_name, severity, policy_id }, + creationTimestamp, + }, + involvedObject: { namespace, name, kind }, + message, + } = details.Object; + return { + message, + cluster, + category, + policy_name, + severity, + creationTimestamp, + policy_id, + namespace, + name, + kind, + }; + }); + + useEffect(() => { + const url = qs.parse(history.location.search); + const clearFilters = _.omit(url, ['filters', 'search']); + history.replace({ + ...history.location, + search: qs.stringify(clearFilters), + }); + setshowTable(true); + }, [history]); + return ( + + + + + setFilterDrawerOpen(!filterDrawerOpen)}> + + + + + + {showTable && ( + ( + {message} + ), + textSearchable: true, + maxWidth: 300, + sortValue: ({ message }) => message, + }, + { + label: 'Cluster', + value: 'cluster', + sortValue: ({ cluster }) => cluster, + }, + { + label: 'Application', + value: ({ namespace, name, kind }) => + kind === 'Kustomization' || kind === 'HelmRelease' + ? `${namespace}/${name}` + : '-', + sortValue: ({ namespace, name }) => `${namespace}/${name}`, + maxWidth: 150, + }, + { + label: 'Severity', + value: ({ severity }) => ( + + ), + sortValue: ({ severity }) => severity, + }, + { + label: 'Category', + value: ({ category }) => ( + {category} + ), + sortValue: ({ category }) => category, + maxWidth: 100, + }, + + { + label: 'Violated Policy', + value: ({ policy_name, cluster, policy_id }) => ( + + + {policy_name} + + + ), + sortValue: ({ policy_name }) => policy_name, + maxWidth: 200, + }, + + { + label: 'Violation Time', + value: ({ creationTimestamp }) => ( + + ), + defaultSort: true, + sortValue: ({ creationTimestamp }) => { + const t = + creationTimestamp && + new Date(creationTimestamp).getTime(); + return t * -1; + }, + }, + ]} + hideSearchAndFilters + onColumnHeaderClick={columnHeaderHandler( + queryState, + setQueryState, + )} + /> + )} + + setFilterDrawerOpen(false)} + open={filterDrawerOpen} + > + + + + + + + + + ); +}; diff --git a/ui-cra/src/components/Policies/Audit/PolicyAuditList.tsx b/ui-cra/src/components/Policies/Audit/PolicyAuditList.tsx new file mode 100644 index 000000000..945918995 --- /dev/null +++ b/ui-cra/src/components/Policies/Audit/PolicyAuditList.tsx @@ -0,0 +1,46 @@ +import { RequestStateHandler, useFeatureFlags } from '@weaveworks/weave-gitops'; +import { useHistory } from 'react-router-dom'; +import { useQueryService } from '../../../hooks/query'; +import { RequestError } from '../../../types/custom'; +import { URLQueryStateManager } from '../../Explorer/QueryStateManager'; +import { QueryStateProvider } from '../../Explorer/hooks'; +import { AuditTable } from './AuditTable'; +import WarningMsg from './WarningMsg'; + +const PolicyAuditList = () => { + const history = useHistory(); + const { isFlagEnabled } = useFeatureFlags(); + const useQueryServiceBackend = isFlagEnabled( + 'WEAVE_GITOPS_FEATURE_QUERY_SERVICE_BACKEND', + ); + const manager = new URLQueryStateManager(history); + const queryState = manager.read(); + const setQueryState = manager.write; + const { data, error, isLoading } = useQueryService({ + terms: queryState.terms, + filters: ['kind:Event', ...queryState.filters], + limit: queryState.limit, + offset: queryState.offset, + orderBy: queryState.orderBy, + ascending: queryState.orderAscending, + }); + + return ( + + + {useQueryServiceBackend ? ( + data?.objects?.length && ( + + ) + ) : ( + + )} + + + ); +}; +export default PolicyAuditList; diff --git a/ui-cra/src/components/Policies/Audit/WarningMsg.tsx b/ui-cra/src/components/Policies/Audit/WarningMsg.tsx new file mode 100644 index 000000000..f3973f8a7 --- /dev/null +++ b/ui-cra/src/components/Policies/Audit/WarningMsg.tsx @@ -0,0 +1,40 @@ +import { + Button, + Flex, + MessageBox, + Text +} from '@weaveworks/weave-gitops'; +import { NotificationsWrapper } from '../../Layout/NotificationsWrapper'; +import { LinkTag } from '../../Shared'; + +const WarningMsg = () => { + return ( + + + + + + Explorer Disabled + + + the explorer service is disabled and it's required to view the + audit logs. + + + + + + + + + + + ); +}; + +export default WarningMsg; diff --git a/ui-cra/src/components/Policies/PoliciesListPage.tsx b/ui-cra/src/components/Policies/PoliciesListPage.tsx index e2aa2249e..0439bf205 100644 --- a/ui-cra/src/components/Policies/PoliciesListPage.tsx +++ b/ui-cra/src/components/Policies/PoliciesListPage.tsx @@ -1,20 +1,29 @@ -import { NotificationsWrapper } from '../Layout/NotificationsWrapper'; +import { + PolicyViolationsList, + RouterTab, + SubRouterTabs, +} from '@weaveworks/weave-gitops'; +import { useRouteMatch } from 'react-router-dom'; import { Page } from '../Layout/App'; -import { useListPolicies } from '../../contexts/PolicyViolations'; -import { PolicyTable } from '@weaveworks/weave-gitops'; +import { PoliciesTab } from './PoliciesListTab'; +import PolicyAuditList from './Audit/PolicyAuditList'; const Policies = () => { - const { data, isLoading } = useListPolicies({}); + const { path } = useRouteMatch(); return ( - - - {data?.policies && ( -
- -
- )} -
+ + + + + + + + + + + + ); }; diff --git a/ui-cra/src/components/Policies/PoliciesListTab.tsx b/ui-cra/src/components/Policies/PoliciesListTab.tsx new file mode 100644 index 000000000..5b6ae7ae9 --- /dev/null +++ b/ui-cra/src/components/Policies/PoliciesListTab.tsx @@ -0,0 +1,18 @@ +import { PolicyTable } from '@weaveworks/weave-gitops'; +import { useListPolicies } from '../../contexts/PolicyViolations'; +import { TableWrapper } from '../Shared'; +import LoadingWrapper from '../Workspaces/WorkspaceDetails/Tabs/WorkspaceTabsWrapper'; + +export const PoliciesTab = () => { + const { data, isLoading } = useListPolicies({}); + + return ( + + {data?.policies && ( + + + + )} + + ); +}; diff --git a/ui-cra/src/components/Policies/PolicyViolationPage.tsx b/ui-cra/src/components/Policies/PolicyViolationPage.tsx index 47dd65eb6..dd8aaafdd 100644 --- a/ui-cra/src/components/Policies/PolicyViolationPage.tsx +++ b/ui-cra/src/components/Policies/PolicyViolationPage.tsx @@ -17,7 +17,7 @@ const getPath = (kind?: string, violation?: PolicyValidation): Breadcrumb[] => { if (!violation) return [{ label: '' }]; const { name, entity, namespace, clusterName, policyId } = violation; if (!kind) { - return [{ label: 'Clusters', url: `${Routes.Clusters}/violations` }]; + return [{ label: 'Policies', url: `${Routes.Policies}/enforcement` }]; } if (kind === Kind.Policy) { @@ -65,7 +65,6 @@ const PolicyViolationPage = ({ id, name, clusterName, kind }: Props) => { const entityObject = new FluxObject({ payload: violation?.violatingEntity, }); - return ( { const errorCount = screen.queryByTestId('errorsCount'); expect(errorCount?.textContent).toEqual('2'); }); - it('renders a list of policies', async () => { - const filterTable = new TestFilterableTable('policy-list', fireEvent); + it('renders Tabs of policies Page', async () => { api.ListPoliciesReturns = listPoliciesResponse; await act(async () => { const c = wrap(); render(c); }); - expect(await screen.findByText('Policies')).toBeTruthy(); + const tabs = await screen.getAllByRole('tab'); + expect(await screen.getByTestId('text-Policies')).toHaveTextContent( + 'Policies', + ); + expect(tabs).toHaveLength(3); + expect(tabs[0]).toHaveTextContent('Policies'); + expect(tabs[1]).toHaveTextContent('Policy Audit'); + expect(tabs[2]).toHaveTextContent('Enforcement Events'); + }); + + it('renders a list of policies', async () => { + const filterTable = new TestFilterableTable('policy-list', fireEvent); + api.ListPoliciesReturns = listPoliciesResponse; + await act(async () => { + const c = wrap(); + render(c); + }); filterTable.testRenderTable( [ diff --git a/ui-cra/src/components/Workspaces/WorkspaceDetails/Tabs/WorkspaceTabsWrapper.tsx b/ui-cra/src/components/Workspaces/WorkspaceDetails/Tabs/WorkspaceTabsWrapper.tsx index dc2cd286a..7f1c10c70 100644 --- a/ui-cra/src/components/Workspaces/WorkspaceDetails/Tabs/WorkspaceTabsWrapper.tsx +++ b/ui-cra/src/components/Workspaces/WorkspaceDetails/Tabs/WorkspaceTabsWrapper.tsx @@ -19,11 +19,13 @@ const LoadingWrapper: FC = ({ return ( {loading && ( - - - - - + + + + + + + )} {(errors?.length || errorMessage) && ( diff --git a/ui-cra/src/routes.tsx b/ui-cra/src/routes.tsx index 2ddfa8b91..849fb1eaf 100644 --- a/ui-cra/src/routes.tsx +++ b/ui-cra/src/routes.tsx @@ -218,7 +218,7 @@ const AppRoutes = () => { path={V2Routes.ImagePolicyDetails} component={withSearchParams(ImagePolicyDetails)} /> - +