@@ -85,6 +94,7 @@ export const LogLevelFilter: React.FunctionComponent<{
Date: Wed, 15 Feb 2023 13:26:08 +0000
Subject: [PATCH 011/210] Change description for metadata field in ingest
pipelines (#150935)
## Summary
Improves the description of the `_meta` field in the Ingest pipeline
form as per the decision made in
https://github.com/elastic/kibana/pull/149976#issuecomment-1426083304
The 'Learn more' link leads to [the Ingest API documentation for
Metadata](https://www.elastic.co/guide/en/elasticsearch/reference/8.6/put-pipeline-api.html#pipeline-metadata).
### Checklist
Delete any items that are not applicable to this PR.
- [X] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
cc: @gchaps
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/pipeline_form/pipeline_form_fields.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx
index ce9a05f666be0..0c8177715c10f 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx
@@ -139,7 +139,7 @@ export const PipelineFormFields: React.FunctionComponent = ({
<>
From 858eec506cff891e3919ec15f701ef4d8c814a5e Mon Sep 17 00:00:00 2001
From: Yan Savitski
Date: Wed, 15 Feb 2023 14:43:41 +0100
Subject: [PATCH 012/210] Behavioral analytics view in discover (#151157)
Issue [[Behavioral Analytics] View collection events in Discover
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../common/types/analytics.ts | 4 +
.../__mocks__/kea_logic/kibana_logic.mock.ts | 7 +-
...ytics_collection_data_view_id_api_logic.ts | 31 +++++
...tics_collection_data_view_id_logic.test.ts | 46 +++++++
...analytics_collection_data_view_id_logic.ts | 53 +++++++
.../analytics_collection_view.test.tsx | 31 ++++-
.../analytics_collection_view.tsx | 42 +++++-
.../public/applications/index.tsx | 1 +
.../shared/kibana/kibana_logic.ts | 2 +
..._analytics_collection_data_view_id.test.ts | 84 ++++++++++++
...fetch_analytics_collection_data_view_id.ts | 31 +++++
.../enterprise_search/analytics.test.ts | 129 ++++++++++++++----
.../routes/enterprise_search/analytics.ts | 37 +++++
13 files changed, 467 insertions(+), 31 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/analytics/api/fetch_analytics_collection_data_view_id/fetch_analytics_collection_data_view_id_api_logic.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_id_logic.test.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_id_logic.ts
create mode 100644 x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.test.ts
create mode 100644 x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.ts
diff --git a/x-pack/plugins/enterprise_search/common/types/analytics.ts b/x-pack/plugins/enterprise_search/common/types/analytics.ts
index 49c00830a7fc9..3c716a545a9c5 100644
--- a/x-pack/plugins/enterprise_search/common/types/analytics.ts
+++ b/x-pack/plugins/enterprise_search/common/types/analytics.ts
@@ -17,3 +17,7 @@ export type AnalyticsCollectionDocument = Omit;
export interface AnalyticsEventsIndexExists {
exists: boolean;
}
+
+export interface AnalyticsCollectionDataViewId {
+ data_view_id: string | null;
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts
index 4689317cfd61a..ff4760c11f5c1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts
@@ -8,13 +8,18 @@
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
-import { Capabilities } from '@kbn/core/public';
+import { ApplicationStart, Capabilities } from '@kbn/core/public';
import { securityMock } from '@kbn/security-plugin/public/mocks';
import { mockHistory } from '../react_router/state.mock';
export const mockKibanaValues = {
+ application: {
+ getUrlForApp: jest.fn(
+ (appId: string, options?: { path?: string }) => `/app/${appId}${options?.path}`
+ ),
+ } as unknown as ApplicationStart,
capabilities: {} as Capabilities,
config: { host: 'http://localhost:3002' },
charts: chartPluginMock.createStartContract(),
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/api/fetch_analytics_collection_data_view_id/fetch_analytics_collection_data_view_id_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/api/fetch_analytics_collection_data_view_id/fetch_analytics_collection_data_view_id_api_logic.ts
new file mode 100644
index 0000000000000..6b4853f2aa581
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/api/fetch_analytics_collection_data_view_id/fetch_analytics_collection_data_view_id_api_logic.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { AnalyticsCollectionDataViewId } from '../../../../../common/types/analytics';
+import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
+import { HttpLogic } from '../../../shared/http';
+
+export interface FetchAnalyticsCollectionDataViewIdAPILogicArgs {
+ id: string;
+}
+
+export type FetchAnalyticsCollectionDataViewIdApiLogicResponse = AnalyticsCollectionDataViewId;
+
+export const fetchAnalyticsCollectionDataViewId = async ({
+ id,
+}: FetchAnalyticsCollectionDataViewIdAPILogicArgs): Promise => {
+ const { http } = HttpLogic.values;
+ const route = `/internal/enterprise_search/analytics/collections/${id}/data_view_id`;
+ const response = await http.get(route);
+
+ return response;
+};
+
+export const FetchAnalyticsCollectionDataViewIdAPILogic = createApiLogic(
+ ['analytics', 'analytics_collection_data_view_id_api_logic'],
+ fetchAnalyticsCollectionDataViewId
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_id_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_id_logic.test.ts
new file mode 100644
index 0000000000000..52d0dc2efc7f3
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_id_logic.test.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { LogicMounter } from '../../../__mocks__/kea_logic';
+
+import { Status } from '../../../../../common/types/api';
+
+import { AnalyticsCollectionDataViewIdLogic } from './analytics_collection_data_view_id_logic';
+
+describe('analyticsCollectionDataViewIdLogic', () => {
+ const { mount } = new LogicMounter(AnalyticsCollectionDataViewIdLogic);
+ const dataViewIdMock = '0c3edf-0c3edf-0c3edf-0c3edf-0c3edf';
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useRealTimers();
+ mount();
+ });
+
+ const DEFAULT_VALUES = {
+ data: undefined,
+ dataViewId: null,
+ status: Status.IDLE,
+ };
+
+ it('has expected default values', () => {
+ expect(AnalyticsCollectionDataViewIdLogic.values).toEqual(DEFAULT_VALUES);
+ });
+
+ describe('selectors', () => {
+ it('updates when apiSuccess listener triggered', () => {
+ AnalyticsCollectionDataViewIdLogic.actions.apiSuccess({ data_view_id: dataViewIdMock });
+
+ expect(AnalyticsCollectionDataViewIdLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ data: { data_view_id: dataViewIdMock },
+ dataViewId: dataViewIdMock,
+ status: Status.SUCCESS,
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_id_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_id_logic.ts
new file mode 100644
index 0000000000000..147f43c53e03e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_id_logic.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { kea, MakeLogicType } from 'kea';
+
+import { Status } from '../../../../../common/types/api';
+
+import { Actions } from '../../../shared/api_logic/create_api_logic';
+import {
+ FetchAnalyticsCollectionDataViewIdAPILogic,
+ FetchAnalyticsCollectionDataViewIdApiLogicResponse,
+} from '../../api/fetch_analytics_collection_data_view_id/fetch_analytics_collection_data_view_id_api_logic';
+
+export interface AnalyticsCollectionDataViewIdActions {
+ apiSuccess: Actions<{}, FetchAnalyticsCollectionDataViewIdApiLogicResponse>['apiSuccess'];
+ fetchAnalyticsCollectionDataViewId(id: string): { id: string };
+ makeRequest: Actions<{}, FetchAnalyticsCollectionDataViewIdApiLogicResponse>['makeRequest'];
+}
+export interface AnalyticsCollectionDataViewIdValues {
+ data: typeof FetchAnalyticsCollectionDataViewIdAPILogic.values.data;
+
+ dataViewId: string | null;
+
+ status: Status;
+}
+
+export const AnalyticsCollectionDataViewIdLogic = kea<
+ MakeLogicType
+>({
+ actions: {
+ fetchAnalyticsCollectionDataViewId: (id) => ({ id }),
+ },
+ connect: {
+ actions: [
+ FetchAnalyticsCollectionDataViewIdAPILogic,
+ ['makeRequest', 'apiSuccess', 'apiError'],
+ ],
+ values: [FetchAnalyticsCollectionDataViewIdAPILogic, ['status', 'data']],
+ },
+ listeners: ({ actions }) => ({
+ fetchAnalyticsCollectionDataViewId: ({ id }) => {
+ actions.makeRequest({ id });
+ },
+ }),
+ path: ['enterprise_search', 'analytics', 'collection_data_view_id'],
+ selectors: ({ selectors }) => ({
+ dataViewId: [() => [selectors.data], (data) => data?.data_view_id || null],
+ }),
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx
index ebbd898f08641..4fe1828378736 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx
@@ -10,11 +10,12 @@ import '../../../__mocks__/shallow_useeffect.mock';
import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../__mocks__/react_router';
-import React from 'react';
+import React, { ReactElement } from 'react';
import { shallow } from 'enzyme';
import { AnalyticsCollection } from '../../../../../common/types/analytics';
+import { EnterpriseSearchAnalyticsPageTemplate } from '../layout/page_template';
import { AnalyticsCollectionIntegrate } from './analytics_collection_integrate/analytics_collection_integrate';
import { AnalyticsCollectionSettings } from './analytics_collection_settings';
@@ -27,10 +28,12 @@ const mockValues = {
id: '1',
name: 'Analytics Collection 1',
} as AnalyticsCollection,
+ dataViewId: '1234-1234-1234',
};
const mockActions = {
fetchAnalyticsCollection: jest.fn(),
+ fetchAnalyticsCollectionDataViewId: jest.fn(),
};
describe('AnalyticsOverview', () => {
@@ -73,5 +76,31 @@ describe('AnalyticsOverview', () => {
expect(wrapper.prop('pageViewTelemetry')).toBe('View Analytics Collection - settings');
});
+
+ it('send correct pageHeader rightSideItems when dataViewId exists', async () => {
+ setMockValues(mockValues);
+ setMockActions(mockActions);
+
+ const rightSideItems = shallow()
+ ?.find(EnterpriseSearchAnalyticsPageTemplate)
+ ?.prop('pageHeader')?.rightSideItems;
+
+ expect(rightSideItems).toHaveLength(1);
+
+ expect((rightSideItems?.[0] as ReactElement).props?.children?.props?.href).toBe(
+ "/app/discover#/?_a=(index:'1234-1234-1234')"
+ );
+ });
+
+ it('hide pageHeader rightSideItems when dataViewId not exists', async () => {
+ setMockValues({ ...mockValues, dataViewId: null });
+ setMockActions(mockActions);
+
+ const wrapper = shallow();
+
+ expect(
+ wrapper?.find(EnterpriseSearchAnalyticsPageTemplate)?.prop('pageHeader')?.rightSideItems
+ ).toBeUndefined();
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx
index ae5d3e4166224..17e89d97195a2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx
@@ -10,8 +10,19 @@ import { useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
-import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
+import {
+ EuiEmptyPrompt,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIconTip,
+ EuiLink,
+ EuiSpacer,
+ EuiTitle,
+} from '@elastic/eui';
+
import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { RedirectAppLinks } from '@kbn/kibana-react-plugin/public';
import { generateEncodedPath } from '../../../shared/encode_path_params';
import { KibanaLogic } from '../../../shared/kibana';
@@ -20,6 +31,8 @@ import { COLLECTION_CREATION_PATH, COLLECTION_VIEW_PATH } from '../../routes';
import { EnterpriseSearchAnalyticsPageTemplate } from '../layout/page_template';
+import { AnalyticsCollectionDataViewIdLogic } from './analytics_collection_data_view_id_logic';
+
import { AnalyticsCollectionEvents } from './analytics_collection_events';
import { AnalyticsCollectionIntegrate } from './analytics_collection_integrate/analytics_collection_integrate';
import { AnalyticsCollectionSettings } from './analytics_collection_settings';
@@ -34,9 +47,11 @@ export const collectionViewBreadcrumbs = [
export const AnalyticsCollectionView: React.FC = () => {
const { fetchAnalyticsCollection } = useActions(FetchAnalyticsCollectionLogic);
+ const { fetchAnalyticsCollectionDataViewId } = useActions(AnalyticsCollectionDataViewIdLogic);
const { analyticsCollection, isLoading } = useValues(FetchAnalyticsCollectionLogic);
+ const { dataViewId } = useValues(AnalyticsCollectionDataViewIdLogic);
const { id, section } = useParams<{ id: string; section: string }>();
- const { navigateToUrl } = useValues(KibanaLogic);
+ const { navigateToUrl, application } = useValues(KibanaLogic);
const collectionViewTabs = [
{
id: 'events',
@@ -84,6 +99,7 @@ export const AnalyticsCollectionView: React.FC = () => {
useEffect(() => {
fetchAnalyticsCollection(id);
+ fetchAnalyticsCollectionDataViewId(id);
}, []);
return (
@@ -101,6 +117,28 @@ export const AnalyticsCollectionView: React.FC = () => {
}
),
pageTitle: analyticsCollection?.name,
+ rightSideItems: dataViewId
+ ? [
+
+
+
+ }
+ type="inspect"
+ />
+
+ ,
+ ]
+ : undefined,
tabs: [...collectionViewTabs],
}}
>
diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx
index 2b25fabe056f0..11250d05a845e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx
@@ -57,6 +57,7 @@ export const renderApp = (
const store = getContext().store;
const unmountKibanaLogic = mountKibanaLogic({
+ application: core.application,
capabilities: core.application.capabilities,
config,
productAccess,
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts
index db80e75fbcf20..070c273884772 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts
@@ -30,6 +30,7 @@ type RequiredFieldsOnly = {
[K in keyof T as T[K] extends Required[K] ? K : never]: T[K];
};
interface KibanaLogicProps {
+ application: ApplicationStart;
config: { host?: string };
productAccess: ProductAccess;
// Kibana core
@@ -57,6 +58,7 @@ export interface KibanaValues extends Omit {
export const KibanaLogic = kea>({
path: ['enterprise_search', 'kibana_logic'],
reducers: ({ props }) => ({
+ application: [props.application || {}, {}],
capabilities: [props.capabilities || {}, {}],
config: [props.config || {}, {}],
charts: [props.charts, {}],
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.test.ts
new file mode 100644
index 0000000000000..1db6ccedfd920
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.test.ts
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
+
+import { DataViewsService } from '@kbn/data-views-plugin/common';
+
+import { ErrorCode } from '../../../common/types/error_codes';
+
+import { fetchAnalyticsCollectionById } from './fetch_analytics_collection';
+import { fetchAnalyticsCollectionDataViewId } from './fetch_analytics_collection_data_view_id';
+
+jest.mock('./fetch_analytics_collection', () => ({
+ fetchAnalyticsCollectionById: jest.fn(),
+}));
+
+describe('fetch analytics collection data view id', () => {
+ const mockClient = {};
+ const dataViewService = { find: jest.fn() };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return data view id of analytics collection by Id', async () => {
+ const mockCollectionId = 'collectionId';
+ const mockDataViewId = 'dataViewId';
+ const mockCollection = { events_datastream: 'log-collection-data-stream' };
+ (fetchAnalyticsCollectionById as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve(mockCollection)
+ );
+
+ dataViewService.find.mockImplementationOnce(() => Promise.resolve([{ id: mockDataViewId }]));
+
+ await expect(
+ fetchAnalyticsCollectionDataViewId(
+ mockClient as unknown as IScopedClusterClient,
+ dataViewService as unknown as DataViewsService,
+ mockCollectionId
+ )
+ ).resolves.toEqual({ data_view_id: mockDataViewId });
+ expect(fetchAnalyticsCollectionById).toHaveBeenCalledWith(mockClient, mockCollectionId);
+ expect(dataViewService.find).toHaveBeenCalledWith(mockCollection.events_datastream, 1);
+ });
+
+ it('should return null when data view not found', async () => {
+ const mockCollectionId = 'collectionId';
+ const mockCollection = { events_datastream: 'log-collection-data-stream' };
+ (fetchAnalyticsCollectionById as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve(mockCollection)
+ );
+
+ dataViewService.find.mockImplementationOnce(() => Promise.resolve([]));
+
+ await expect(
+ fetchAnalyticsCollectionDataViewId(
+ mockClient as unknown as IScopedClusterClient,
+ dataViewService as unknown as DataViewsService,
+ mockCollectionId
+ )
+ ).resolves.toEqual({ data_view_id: null });
+ expect(fetchAnalyticsCollectionById).toHaveBeenCalledWith(mockClient, mockCollectionId);
+ expect(dataViewService.find).toHaveBeenCalledWith(mockCollection.events_datastream, 1);
+ });
+
+ it('should throw an error when analytics collection not found', async () => {
+ const mockCollectionId = 'collectionId';
+ (fetchAnalyticsCollectionById as jest.Mock).mockImplementationOnce(() => Promise.resolve(null));
+
+ await expect(
+ fetchAnalyticsCollectionDataViewId(
+ mockClient as unknown as IScopedClusterClient,
+ dataViewService as unknown as DataViewsService,
+ mockCollectionId
+ )
+ ).rejects.toThrowError(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
+ expect(fetchAnalyticsCollectionById).toHaveBeenCalledWith(mockClient, mockCollectionId);
+ expect(dataViewService.find).not.toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.ts
new file mode 100644
index 0000000000000..0ec07139d7b41
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
+import { DataViewsService } from '@kbn/data-views-plugin/common';
+
+import { AnalyticsCollectionDataViewId } from '../../../common/types/analytics';
+
+import { ErrorCode } from '../../../common/types/error_codes';
+
+import { fetchAnalyticsCollectionById } from './fetch_analytics_collection';
+
+export const fetchAnalyticsCollectionDataViewId = async (
+ elasticsearchClient: IScopedClusterClient,
+ dataViewsService: DataViewsService,
+ collectionId: string
+): Promise => {
+ const collection = await fetchAnalyticsCollectionById(elasticsearchClient, collectionId);
+
+ if (!collection) {
+ throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
+ }
+
+ const collectionDataView = await dataViewsService.find(collection.events_datastream, 1);
+
+ return { data_view_id: collectionDataView?.[0]?.id || null };
+};
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts
index 09195df9768dc..f216a23e58e0a 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts
@@ -14,8 +14,18 @@ import { DataPluginStart } from '@kbn/data-plugin/server/plugin';
jest.mock('../../lib/analytics/fetch_analytics_collection', () => ({
fetchAnalyticsCollectionById: jest.fn(),
}));
-import { AnalyticsCollection } from '../../../common/types/analytics';
+
+jest.mock('../../lib/analytics/fetch_analytics_collection_data_view_id', () => ({
+ fetchAnalyticsCollectionDataViewId: jest.fn(),
+}));
+
+import {
+ AnalyticsCollection,
+ AnalyticsCollectionDataViewId,
+} from '../../../common/types/analytics';
+import { ErrorCode } from '../../../common/types/error_codes';
import { fetchAnalyticsCollectionById } from '../../lib/analytics/fetch_analytics_collection';
+import { fetchAnalyticsCollectionDataViewId } from '../../lib/analytics/fetch_analytics_collection_data_view_id';
import { registerAnalyticsRoutes } from './analytics';
@@ -23,36 +33,36 @@ describe('Enterprise Search Analytics API', () => {
let mockRouter: MockRouter;
const mockClient = {};
- beforeEach(() => {
- const context = {
- core: Promise.resolve({ elasticsearch: { client: mockClient } }),
- } as jest.Mocked;
+ describe('GET /internal/enterprise_search/analytics/collections/{id}', () => {
+ beforeEach(() => {
+ const context = {
+ core: Promise.resolve({ elasticsearch: { client: mockClient } }),
+ } as jest.Mocked;
- mockRouter = new MockRouter({
- context,
- method: 'get',
- path: '/internal/enterprise_search/analytics/collections/{id}',
- });
+ mockRouter = new MockRouter({
+ context,
+ method: 'get',
+ path: '/internal/enterprise_search/analytics/collections/{id}',
+ });
- const mockDataPlugin = {
- indexPatterns: {
- dataViewsServiceFactory: jest.fn(),
- },
- };
-
- const mockedSavedObjects = {
- getScopedClient: jest.fn(),
- };
-
- registerAnalyticsRoutes({
- ...mockDependencies,
- data: mockDataPlugin as unknown as DataPluginStart,
- savedObjects: mockedSavedObjects as unknown as SavedObjectsServiceStart,
- router: mockRouter.router,
+ const mockDataPlugin = {
+ indexPatterns: {
+ dataViewsServiceFactory: jest.fn(),
+ },
+ };
+
+ const mockedSavedObjects = {
+ getScopedClient: jest.fn(),
+ };
+
+ registerAnalyticsRoutes({
+ ...mockDependencies,
+ data: mockDataPlugin as unknown as DataPluginStart,
+ savedObjects: mockedSavedObjects as unknown as SavedObjectsServiceStart,
+ router: mockRouter.router,
+ });
});
- });
- describe('GET /internal/enterprise_search/analytics/collections/{id}', () => {
it('fetches a defined analytics collection name', async () => {
const mockData: AnalyticsCollection = {
event_retention_day_length: 30,
@@ -90,4 +100,69 @@ describe('Enterprise Search Analytics API', () => {
});
});
});
+
+ describe('GET /internal/enterprise_search/analytics/collections/{id}/data_view_id', () => {
+ beforeEach(() => {
+ const context = {
+ core: Promise.resolve({ elasticsearch: { client: mockClient } }),
+ } as jest.Mocked;
+
+ mockRouter = new MockRouter({
+ context,
+ method: 'get',
+ path: '/internal/enterprise_search/analytics/collections/{id}/data_view_id',
+ });
+
+ const mockDataPlugin = {
+ indexPatterns: {
+ dataViewsServiceFactory: jest.fn(),
+ },
+ };
+
+ const mockedSavedObjects = {
+ getScopedClient: jest.fn(),
+ };
+
+ registerAnalyticsRoutes({
+ ...mockDependencies,
+ data: mockDataPlugin as unknown as DataPluginStart,
+ savedObjects: mockedSavedObjects as unknown as SavedObjectsServiceStart,
+ router: mockRouter.router,
+ });
+ });
+
+ it('fetches a defined data view id by collection id', async () => {
+ const mockData: AnalyticsCollectionDataViewId = {
+ data_view_id: '03fca-1234-5678-9abc-1234',
+ };
+
+ (fetchAnalyticsCollectionDataViewId as jest.Mock).mockImplementationOnce(() => {
+ return Promise.resolve(mockData);
+ });
+ await mockRouter.callRoute({ params: { id: '1' } });
+
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({
+ body: mockData,
+ });
+ });
+
+ it('throws a 404 error if collection not found by id', async () => {
+ (fetchAnalyticsCollectionDataViewId as jest.Mock).mockImplementationOnce(() => {
+ throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
+ });
+ await mockRouter.callRoute({
+ params: { id: '1' },
+ });
+
+ expect(mockRouter.response.customError).toHaveBeenCalledWith({
+ body: {
+ attributes: {
+ error_code: 'analytics_collection_not_found',
+ },
+ message: 'Analytics collection not found',
+ },
+ statusCode: 404,
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts
index 1e84a1b81845e..9306a1958956a 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts
@@ -20,6 +20,7 @@ import {
fetchAnalyticsCollectionById,
fetchAnalyticsCollections,
} from '../../lib/analytics/fetch_analytics_collection';
+import { fetchAnalyticsCollectionDataViewId } from '../../lib/analytics/fetch_analytics_collection_data_view_id';
import { RouteDependencies } from '../../plugin';
import { createError } from '../../utils/create_error';
import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handler';
@@ -180,4 +181,40 @@ export function registerAnalyticsRoutes({
return response.ok({ body: { exists: true } });
})
);
+
+ router.get(
+ {
+ path: '/internal/enterprise_search/analytics/collections/{id}/data_view_id',
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ },
+ },
+ elasticsearchErrorHandler(log, async (context, request, response) => {
+ const core = await context.core;
+ const elasticsearchClient = core.elasticsearch.client;
+ const dataViewsService = await data.indexPatterns.dataViewsServiceFactory(
+ savedObjects.getScopedClient(request),
+ elasticsearchClient.asCurrentUser,
+ request
+ );
+
+ try {
+ const dataViewId = await fetchAnalyticsCollectionDataViewId(
+ elasticsearchClient,
+ dataViewsService,
+ request.params.id
+ );
+
+ return response.ok({ body: dataViewId });
+ } catch (error) {
+ if ((error as Error).message === ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND) {
+ return createIndexNotFoundError(error, response);
+ }
+
+ throw error;
+ }
+ })
+ );
}
From c6dc97a80a8176fdad81080c71a2104e2c9995fc Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Wed, 15 Feb 2023 13:51:47 +0000
Subject: [PATCH 013/210] [ML] Fix module setup apply to all spaces (#151270)
The shared module setup function is not passing across the
`applyToAllSpaces` parameter.
This only affects plugins calling `setup` via the server side shared
function, not the ML plugin itself or any plugins calling the kibana
endpoint.
---
x-pack/plugins/ml/server/shared_services/providers/modules.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/ml/server/shared_services/providers/modules.ts b/x-pack/plugins/ml/server/shared_services/providers/modules.ts
index 7180e0c0094e6..a9672b2ffdc67 100644
--- a/x-pack/plugins/ml/server/shared_services/providers/modules.ts
+++ b/x-pack/plugins/ml/server/shared_services/providers/modules.ts
@@ -112,7 +112,8 @@ export function getModulesProvider(
payload.end,
payload.jobOverrides,
payload.datafeedOverrides,
- payload.estimateModelMemory
+ payload.estimateModelMemory,
+ payload.applyToAllSpaces
);
});
},
From d04dc43513d747a8f64ea42168e5537486b95ef3 Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Wed, 15 Feb 2023 13:52:12 +0000
Subject: [PATCH 014/210] [ML] AIOPs: Improve log categorization discover
filter label (#151036)
Fixes the Discover filters generated by the Log Categorisation page,
adding an alias so the whole query isn't displayed.
Label is `Categorization - `
Before:
![image](https://user-images.githubusercontent.com/22172091/218516903-2831e416-1b7f-42ee-b296-601d4bb99825.png)
After:
![image](https://user-images.githubusercontent.com/22172091/218516727-a7327a20-332f-45bd-907d-40595dfec945.png)
---
.../log_categorization/use_discover_links.ts | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts
index a56d5c0cdfc6e..2bc63415f9b95 100644
--- a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts
+++ b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts
@@ -9,6 +9,7 @@ import rison from '@kbn/rison';
import moment from 'moment';
import type { TimeRangeBounds } from '@kbn/data-plugin/common';
+import { i18n } from '@kbn/i18n';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import type { Category } from './use_categorize_request';
import type { QueryMode } from './category_table';
@@ -55,6 +56,16 @@ export function useDiscoverLinks() {
})),
},
},
+ meta: {
+ alias: i18n.translate('xpack.aiops.logCategorization.filterAliasLabel', {
+ defaultMessage: 'Categorization - {field}',
+ values: {
+ field,
+ },
+ }),
+ index,
+ disabled: false,
+ },
},
],
index,
From cab487d7c8fa8c12373e11efa6281dff84b9c1f8 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau
Date: Wed, 15 Feb 2023 09:00:00 -0500
Subject: [PATCH 015/210] [RAM] Uptime allow rac api (#151207)
## Summary
Allow `rac` api for uptime privileges and to avoid that
---
x-pack/plugins/synthetics/server/feature.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/synthetics/server/feature.ts b/x-pack/plugins/synthetics/server/feature.ts
index f68b97668841d..4026bbdb19fb9 100644
--- a/x-pack/plugins/synthetics/server/feature.ts
+++ b/x-pack/plugins/synthetics/server/feature.ts
@@ -30,7 +30,7 @@ export const uptimeFeature = {
all: {
app: ['uptime', 'kibana', 'synthetics'],
catalogue: ['uptime'],
- api: ['uptime-read', 'uptime-write', 'lists-all'],
+ api: ['uptime-read', 'uptime-write', 'lists-all', 'rac'],
savedObject: {
all: [
umDynamicSettings.name,
From 5fa79fc9b364058464603f527eb30ab1f5d376b4 Mon Sep 17 00:00:00 2001
From: Thomas Watson
Date: Wed, 15 Feb 2023 15:03:37 +0100
Subject: [PATCH 016/210] Remove @loaders.gl/polyfills dev-dependency (#151287)
---
package.json | 2 -
renovate.json | 1 -
.../geojson_importer/geojson_importer.test.js | 1 -
yarn.lock | 230 +-----------------
4 files changed, 9 insertions(+), 225 deletions(-)
diff --git a/package.json b/package.json
index bbd67fbd89633..66dd8cb08e49f 100644
--- a/package.json
+++ b/package.json
@@ -82,7 +82,6 @@
"**/hoist-non-react-statics": "^3.3.2",
"**/isomorphic-fetch/node-fetch": "^2.6.7",
"**/istanbul-lib-coverage": "^3.2.0",
- "**/json-schema": "^0.4.0",
"**/minimatch": "^3.1.2",
"**/minimist": "^1.2.6",
"**/pdfkit/crypto-js": "4.0.0",
@@ -1108,7 +1107,6 @@
"@kbn/web-worker-stub": "link:packages/kbn-web-worker-stub",
"@kbn/whereis-pkg-cli": "link:packages/kbn-whereis-pkg-cli",
"@kbn/yarn-lock-validator": "link:packages/kbn-yarn-lock-validator",
- "@loaders.gl/polyfills": "^2.3.5",
"@mapbox/vector-tile": "1.3.1",
"@octokit/rest": "^16.35.0",
"@openpgp/web-stream-tools": "^0.0.10",
diff --git a/renovate.json b/renovate.json
index ce12b9dd5a248..c72e955dde18d 100644
--- a/renovate.json
+++ b/renovate.json
@@ -88,7 +88,6 @@
"groupName": "polyfills",
"matchPackageNames": ["core-js"],
"matchPackagePatterns": ["polyfill"],
- "excludePackageNames": ["@loaders.gl/polyfills"],
"reviewers": ["team:kibana-operations"],
"matchBaseBranches": ["main"],
"labels": ["Team:Operations", "release_note:skip"],
diff --git a/x-pack/plugins/file_upload/public/importer/geo/geojson_importer/geojson_importer.test.js b/x-pack/plugins/file_upload/public/importer/geo/geojson_importer/geojson_importer.test.js
index 7b621c4ccbcad..eb6594422fb10 100644
--- a/x-pack/plugins/file_upload/public/importer/geo/geojson_importer/geojson_importer.test.js
+++ b/x-pack/plugins/file_upload/public/importer/geo/geojson_importer/geojson_importer.test.js
@@ -6,7 +6,6 @@
*/
import { GeoJsonImporter } from './geojson_importer';
-import '@loaders.gl/polyfills';
const FEATURE_COLLECTION = {
type: 'FeatureCollection',
diff --git a/yarn.lock b/yarn.lock
index 99e9088e676e4..188c3c05eec83 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5571,19 +5571,6 @@
"@babel/runtime" "^7.3.1"
"@probe.gl/stats" "^3.3.0"
-"@loaders.gl/polyfills@^2.3.5":
- version "2.3.5"
- resolved "https://registry.yarnpkg.com/@loaders.gl/polyfills/-/polyfills-2.3.5.tgz#5f32b732c8f2a7e80c221f19ee2d01725f09b5c3"
- integrity sha512-sSoeqtH2UJ1eqyM18AnYppLb5dapvWxt4hEv7xXmhXfVXxjBzUonLEg8d3UxfGEsxtxVmbS5DXHyL+uhYcLarw==
- dependencies:
- "@babel/runtime" "^7.3.1"
- get-pixels "^3.3.2"
- ndarray "^1.0.18"
- save-pixels "^2.3.2"
- stream-to-async-iterator "^0.2.0"
- through "^2.3.8"
- web-streams-polyfill "^3.0.0"
-
"@loaders.gl/shapefile@^2.3.1":
version "2.3.13"
resolved "https://registry.yarnpkg.com/@loaders.gl/shapefile/-/shapefile-2.3.13.tgz#5c6c9cc4a113c2f6739c0eb553ae9ceb9d1840ae"
@@ -9960,7 +9947,7 @@ ajv-keywords@^5.0.0:
dependencies:
fast-deep-equal "^3.1.3"
-ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5:
+ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -12371,13 +12358,6 @@ content-type@~1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
-contentstream@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/contentstream/-/contentstream-1.0.0.tgz#0bdcfa46da30464a86ce8fa7ece565410dc6f9a5"
- integrity sha1-C9z6RtowRkqGzo+n7OVlQQ3G+aU=
- dependencies:
- readable-stream "~1.0.33-1"
-
convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
@@ -12900,13 +12880,6 @@ currently-unhandled@^0.4.1:
dependencies:
array-find-index "^1.0.1"
-cwise-compiler@^1.0.0, cwise-compiler@^1.1.2:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/cwise-compiler/-/cwise-compiler-1.1.3.tgz#f4d667410e850d3a313a7d2db7b1e505bb034cc5"
- integrity sha1-9NZnQQ6FDToxOn0tt7HlBbsDTMU=
- dependencies:
- uniq "^1.0.0"
-
cyclist@~0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
@@ -13390,11 +13363,6 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
-data-uri-to-buffer@0.0.3:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz#18ae979a6a0ca994b0625853916d2662bbae0b1a"
- integrity sha1-GK6XmmoMqZSwYlhTkW0mYruuCxo=
-
data-urls@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
@@ -16302,23 +16270,6 @@ get-package-type@^0.1.0:
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
-get-pixels@^3.3.2:
- version "3.3.2"
- resolved "https://registry.yarnpkg.com/get-pixels/-/get-pixels-3.3.2.tgz#3f62fb8811932c69f262bba07cba72b692b4ff03"
- integrity sha512-6ar+8yPxRd1pskEcl2GSEu1La0+xYRjjnkby6AYiRDDwZ0tJbPQmHnSeH9fGLskT8kvR0OukVgtZLcsENF9YKQ==
- dependencies:
- data-uri-to-buffer "0.0.3"
- jpeg-js "^0.3.2"
- mime-types "^2.0.1"
- ndarray "^1.0.13"
- ndarray-pack "^1.1.1"
- node-bitmap "0.0.1"
- omggif "^1.0.5"
- parse-data-uri "^0.2.0"
- pngjs "^3.3.3"
- request "^2.44.0"
- through "^2.3.4"
-
get-port@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
@@ -16390,13 +16341,6 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
-gif-encoder@~0.4.1:
- version "0.4.3"
- resolved "https://registry.yarnpkg.com/gif-encoder/-/gif-encoder-0.4.3.tgz#8a2b4fe8ca895a48e3a0b6cbb340a0a6a3571899"
- integrity sha1-iitP6MqJWkjjoLbLs0CgpqNXGJk=
- dependencies:
- readable-stream "~1.1.9"
-
git-hooks-list@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-1.0.3.tgz#be5baaf78203ce342f2f844a9d2b03dba1b45156"
@@ -16803,19 +16747,6 @@ handlebars@4.7.7, handlebars@^4.7.7:
optionalDependencies:
uglify-js "^3.1.4"
-har-schema@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
- integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
-
-har-validator@~5.1.3:
- version "5.1.3"
- resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
- integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
- dependencies:
- ajv "^6.5.5"
- har-schema "^2.0.0"
-
hard-rejection@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
@@ -17367,15 +17298,6 @@ http-proxy@^1.18.1:
follow-redirects "^1.0.0"
requires-port "^1.0.0"
-http-signature@~1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
- integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
- dependencies:
- assert-plus "^1.0.0"
- jsprim "^1.2.2"
- sshpk "^1.7.0"
-
http-signature@~1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9"
@@ -17715,11 +17637,6 @@ io-ts@^2.0.5:
resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.0.5.tgz#e6e3db9df8b047f9cbd6b69e7d2ad3e6437a0b13"
integrity sha512-pL7uUptryanI5Glv+GUv7xh+aLBjxGEDmLwmEYNSx0yOD3djK0Nw5Bt0N6BAkv9LadOUU7QKpRsLcqnTh3UlLA==
-iota-array@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/iota-array/-/iota-array-1.0.0.tgz#81ef57fe5d05814cd58c2483632a99c30a0e8087"
- integrity sha1-ge9X/l0FgUzVjCSDYyqZwwoOgIc=
-
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -17829,7 +17746,7 @@ is-boolean-object@^1.0.1, is-boolean-object@^1.1.0:
dependencies:
call-bind "^1.0.0"
-is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.1:
+is-buffer@^1.1.5, is-buffer@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
@@ -19252,16 +19169,6 @@ joi@^17.3.0, joi@^17.7.1:
"@sideway/formula" "^3.0.1"
"@sideway/pinpoint" "^2.0.0"
-jpeg-js@0.0.4:
- version "0.0.4"
- resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.0.4.tgz#06aaf47efec7af0b1924a59cd695a6d2b5ed870e"
- integrity sha1-Bqr0fv7HrwsZJKWc1pWm0rXthw4=
-
-jpeg-js@^0.3.2:
- version "0.3.7"
- resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.3.7.tgz#471a89d06011640592d314158608690172b1028d"
- integrity sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==
-
jquery@^3.5.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470"
@@ -19443,7 +19350,7 @@ json-schema-typed@^8.0.1:
resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-8.0.1.tgz#826ee39e3b6cef536f85412ff048d3ff6f19dfa0"
integrity sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==
-json-schema@0.2.3, json-schema@0.4.0, json-schema@^0.4.0:
+json-schema@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
@@ -19538,16 +19445,6 @@ jsonwebtoken@^9.0.0:
ms "^2.1.1"
semver "^7.3.8"
-jsprim@^1.2.2:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
- integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
- dependencies:
- assert-plus "1.0.0"
- extsprintf "1.3.0"
- json-schema "0.2.3"
- verror "1.10.0"
-
jsprim@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d"
@@ -20928,7 +20825,7 @@ mime-db@1.51.0, mime-db@1.x.x, "mime-db@>= 1.40.0 < 2":
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==
-mime-types@^2.0.1, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
+mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.34"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24"
integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==
@@ -21468,29 +21365,6 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
-ndarray-ops@^1.2.2:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/ndarray-ops/-/ndarray-ops-1.2.2.tgz#59e88d2c32a7eebcb1bc690fae141579557a614e"
- integrity sha1-WeiNLDKn7ryxvGkPrhQVeVV6YU4=
- dependencies:
- cwise-compiler "^1.0.0"
-
-ndarray-pack@^1.1.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/ndarray-pack/-/ndarray-pack-1.2.1.tgz#8caebeaaa24d5ecf70ff86020637977da8ee585a"
- integrity sha1-jK6+qqJNXs9w/4YCBjeXfajuWFo=
- dependencies:
- cwise-compiler "^1.1.2"
- ndarray "^1.0.13"
-
-ndarray@^1.0.13, ndarray@^1.0.18:
- version "1.0.19"
- resolved "https://registry.yarnpkg.com/ndarray/-/ndarray-1.0.19.tgz#6785b5f5dfa58b83e31ae5b2a058cfd1ab3f694e"
- integrity sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==
- dependencies:
- iota-array "^1.0.0"
- is-buffer "^1.0.2"
-
nearley@^2.7.10:
version "2.16.0"
resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.16.0.tgz#77c297d041941d268290ec84b739d0ee297e83a7"
@@ -21591,11 +21465,6 @@ node-addon-api@^5.0.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501"
integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==
-node-bitmap@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/node-bitmap/-/node-bitmap-0.0.1.tgz#180eac7003e0c707618ef31368f62f84b2a69091"
- integrity sha1-GA6scAPgxwdhjvMTaPYvhLKmkJE=
-
node-cache@^5.1.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
@@ -21931,11 +21800,6 @@ nyc@15.1.0, nyc@^15.1.0:
test-exclude "^6.0.0"
yargs "^15.0.2"
-oauth-sign@~0.9.0:
- version "0.9.0"
- resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
- integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
-
object-assign@4.X, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -22087,11 +21951,6 @@ octokit-pagination-methods@^1.1.0:
resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4"
integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==
-omggif@^1.0.5:
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19"
- integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==
-
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@@ -22502,13 +22361,6 @@ parse-asn1@^5.0.0:
evp_bytestokey "^1.0.0"
pbkdf2 "^3.0.3"
-parse-data-uri@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/parse-data-uri/-/parse-data-uri-0.2.0.tgz#bf04d851dd5c87b0ab238e5d01ace494b604b4c9"
- integrity sha1-vwTYUd1ch7CrI45dAazklLYEtMk=
- dependencies:
- data-uri-to-buffer "0.0.3"
-
parse-entities@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
@@ -22926,12 +22778,7 @@ png-js@^1.0.0:
resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d"
integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==
-pngjs-nozlib@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/pngjs-nozlib/-/pngjs-nozlib-1.0.0.tgz#9e64d602cfe9cce4d9d5997d0687429a73f0b7d7"
- integrity sha1-nmTWAs/pzOTZ1Zl9BodCmnPwt9c=
-
-pngjs@^3.3.3, pngjs@^3.4.0:
+pngjs@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
@@ -24592,7 +24439,7 @@ read-pkg@^5.2.0:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
-readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17, readable-stream@~1.0.27-1, readable-stream@~1.0.33-1:
+readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17, readable-stream@~1.0.27-1:
version "1.0.34"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
@@ -24611,16 +24458,6 @@ readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
-readable-stream@~1.1.9:
- version "1.1.14"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
- integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.1"
- isarray "0.0.1"
- string_decoder "~0.10.x"
-
readdir-glob@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4"
@@ -25117,32 +24954,6 @@ request-progress@^3.0.0:
dependencies:
throttleit "^1.0.0"
-request@^2.44.0:
- version "2.88.2"
- resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
- integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
- dependencies:
- aws-sign2 "~0.7.0"
- aws4 "^1.8.0"
- caseless "~0.12.0"
- combined-stream "~1.0.6"
- extend "~3.0.2"
- forever-agent "~0.6.1"
- form-data "~2.3.2"
- har-validator "~5.1.3"
- http-signature "~1.2.0"
- is-typedarray "~1.0.0"
- isstream "~0.1.2"
- json-stringify-safe "~5.0.1"
- mime-types "~2.1.19"
- oauth-sign "~0.9.0"
- performance-now "^2.1.0"
- qs "~6.5.2"
- safe-buffer "^5.1.2"
- tough-cookie "~2.5.0"
- tunnel-agent "^0.6.0"
- uuid "^3.3.2"
-
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -25540,19 +25351,6 @@ sass-loader@^10.4.1:
schema-utils "^3.0.0"
semver "^7.3.2"
-save-pixels@^2.3.2:
- version "2.3.4"
- resolved "https://registry.yarnpkg.com/save-pixels/-/save-pixels-2.3.4.tgz#49d349c06b8d7c0127dbf0da24b44aca5afb59fe"
- integrity sha1-SdNJwGuNfAEn2/DaJLRKylr7Wf4=
- dependencies:
- contentstream "^1.0.0"
- gif-encoder "~0.4.1"
- jpeg-js "0.0.4"
- ndarray "^1.0.18"
- ndarray-ops "^1.2.2"
- pngjs-nozlib "^1.0.0"
- through "^2.3.4"
-
sax@>=0.6.0, sax@^1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -26398,7 +26196,7 @@ sql-summary@^1.0.1:
resolved "https://registry.yarnpkg.com/sql-summary/-/sql-summary-1.0.1.tgz#a2dddb5435bae294eb11424a7330dc5bafe09c2b"
integrity sha512-IpCr2tpnNkP3Jera4ncexsZUp0enJBLr+pHCyTweMUBrbJsTgQeLWx1FXLhoBj/MvcnUQpkgOn2EY8FKOkUzww==
-sshpk@^1.14.1, sshpk@^1.7.0:
+sshpk@^1.14.1:
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
@@ -26599,11 +26397,6 @@ stream-slicer@0.0.6:
resolved "https://registry.yarnpkg.com/stream-slicer/-/stream-slicer-0.0.6.tgz#f86b2ac5c2440b7a0a87b71f33665c0788046138"
integrity sha1-+GsqxcJEC3oKh7cfM2ZcB4gEYTg=
-stream-to-async-iterator@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/stream-to-async-iterator/-/stream-to-async-iterator-0.2.0.tgz#bef5c885e9524f98b2fa5effecc357bd58483780"
- integrity sha1-vvXIhelST5iy+l7/7MNXvVhIN4A=
-
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
@@ -27379,7 +27172,7 @@ through2@~0.4.1:
readable-stream "~1.0.17"
xtend "~2.1.1"
-"through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3.4:
+"through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3.4:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@@ -28025,11 +27818,6 @@ union-value@^1.0.0:
is-extendable "^0.1.1"
set-value "^2.0.1"
-uniq@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
- integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
-
unique-filename@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
@@ -29042,7 +28830,7 @@ web-namespaces@^1.0.0:
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
-web-streams-polyfill@^3.0.0, web-streams-polyfill@^3.2.0:
+web-streams-polyfill@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"
integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==
From 24efb8597e8ed598f930373a32238e4b54c7309c Mon Sep 17 00:00:00 2001
From: Drew Tate
Date: Wed, 15 Feb 2023 08:23:59 -0600
Subject: [PATCH 017/210] [Lens] better support for user messages on embeddable
(#149458)
---
...et_application_user_messages.test.tsx.snap | 3 +-
.../get_application_user_messages.tsx | 2 +-
.../lens/public/embeddable/embeddable.tsx | 229 +++++++++++++-----
.../public/embeddable/expression_wrapper.tsx | 143 ++++-------
.../kbn_archiver/lens/missing_fields.json | 2 +-
5 files changed, 211 insertions(+), 168 deletions(-)
diff --git a/x-pack/plugins/lens/public/app_plugin/__snapshots__/get_application_user_messages.test.tsx.snap b/x-pack/plugins/lens/public/app_plugin/__snapshots__/get_application_user_messages.test.tsx.snap
index 0219edefe4b23..437d331f5bdec 100644
--- a/x-pack/plugins/lens/public/app_plugin/__snapshots__/get_application_user_messages.test.tsx.snap
+++ b/x-pack/plugins/lens/public/app_plugin/__snapshots__/get_application_user_messages.test.tsx.snap
@@ -77,7 +77,8 @@ Array [
href="fake/url"
style={
Object {
- "display": "block",
+ "textAlign": "center",
+ "width": "100%",
}
}
>
diff --git a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx
index f5190ec334157..b56736eabe7ac 100644
--- a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx
@@ -153,7 +153,7 @@ function getMissingIndexPatternsErrors(
href={core.application.getUrlForApp('management', {
path: '/kibana/indexPatterns/create',
})}
- style={{ display: 'block' }}
+ style={{ width: '100%', textAlign: 'center' }}
data-test-subj="configuration-failure-reconfigure-indexpatterns"
>
{i18n.translate('xpack.lens.editorFrame.dataViewReconfigure', {
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
index e89e8c61e3185..a430f8ab66e1e 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
@@ -73,6 +73,8 @@ import {
MultiClickTriggerEvent,
} from '@kbn/charts-plugin/public';
import { DataViewSpec } from '@kbn/data-views-plugin/common';
+import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
+import { EuiEmptyPrompt } from '@elastic/eui';
import { useEuiFontSize, useEuiTheme } from '@elastic/eui';
import { getExecutionContextEvents, trackUiCounterEvents } from '../lens_ui_telemetry';
import { Document } from '../persistence';
@@ -96,6 +98,7 @@ import {
AddUserMessages,
isMessageRemovable,
UserMessagesGetter,
+ UserMessagesDisplayLocationId,
} from '../types';
import { getEditPath, DOC_TYPE } from '../../common';
@@ -194,6 +197,52 @@ export interface ViewUnderlyingDataArgs {
columns: string[];
}
+function VisualizationErrorPanel({ errors, canEdit }: { errors: UserMessage[]; canEdit: boolean }) {
+ const showMore = errors.length > 1;
+ const canFixInLens = canEdit && errors.some(({ fixableInEditor }) => fixableInEditor);
+ return (
+
+
+ {errors.length ? (
+ <>
+ {errors[0].longMessage}
+ {showMore && !canFixInLens ? (
+
+
+
+ ) : null}
+ {canFixInLens ? (
+
+
+
+ ) : null}
+ >
+ ) : (
+
+
+
+ )}
+ >
+ }
+ />
+
+ );
+}
+
const getExpressionFromDocument = async (
document: Document,
documentToExpression: LensEmbeddableDeps['documentToExpression']
@@ -296,6 +345,27 @@ const EmbeddableMessagesPopover = ({ messages }: { messages: UserMessage[] }) =>
);
};
+const blockingMessageDisplayLocations: UserMessagesDisplayLocationId[] = [
+ 'visualization',
+ 'visualizationOnEmbeddable',
+];
+
+const MessagesBadge = ({ onMount }: { onMount: (el: HTMLDivElement) => void }) => (
+ {
+ if (el) {
+ onMount(el);
+ }
+ }}
+ />
+);
+
export class Embeddable
extends AbstractEmbeddable
implements
@@ -311,7 +381,6 @@ export class Embeddable
private savedVis: Document | undefined;
private expression: string | undefined | null;
private domNode: HTMLElement | Element | undefined;
- private badgeDomNode: HTMLElement | Element | undefined;
private subscription: Subscription;
private isInitialized = false;
private inputReloadSubscriptions: Subscription[];
@@ -503,10 +572,6 @@ export class Embeddable
);
};
- private get hasAnyErrors() {
- return this.getUserMessages(undefined, { severity: 'error' }).length > 0;
- }
-
private _userMessages: UserMessage[] = [];
// loads all available user messages
@@ -583,7 +648,7 @@ export class Embeddable
if (addedMessageIds.length) {
this.additionalUserMessages = newMessageMap;
- this.renderBadgeMessages();
+ this.renderUserMessages();
}
return () => {
@@ -837,22 +902,22 @@ export class Embeddable
this.domNode.setAttribute('data-shared-item', '');
- const errors = this.getUserMessages(['visualization', 'visualizationOnEmbeddable'], {
+ const blockingErrors = this.getUserMessages(blockingMessageDisplayLocations, {
severity: 'error',
});
this.updateOutput({
loading: true,
- error: errors.length
+ error: blockingErrors.length
? new Error(
- typeof errors[0].longMessage === 'string'
- ? errors[0].longMessage
- : errors[0].shortMessage
+ typeof blockingErrors[0].longMessage === 'string'
+ ? blockingErrors[0].longMessage
+ : blockingErrors[0].shortMessage
)
: undefined,
});
- if (errors.length) {
+ if (blockingErrors.length) {
this.renderComplete.dispatchError();
} else {
this.renderComplete.dispatchInProgress();
@@ -860,59 +925,94 @@ export class Embeddable
const input = this.getInput();
- render(
-
- {
- this.updateOutput({ error: new Error(message) });
- this.logError('runtime');
- }}
- noPadding={this.visDisplayOptions.noPadding}
- />
- {
- if (el) {
+ if (this.expression && !blockingErrors.length) {
+ render(
+ <>
+
+ this.addUserMessages(messages)}
+ onRuntimeError={(message) => {
+ this.updateOutput({ error: new Error(message) });
+ this.logError('runtime');
+ }}
+ noPadding={this.visDisplayOptions.noPadding}
+ />
+
+
{
this.badgeDomNode = el;
this.renderBadgeMessages();
- }
- }}
- />
- ,
- domNode
- );
+ }}
+ />
+ >,
+ domNode
+ );
+ }
+
+ this.renderUserMessages();
+ }
+
+ private renderUserMessages() {
+ const errors = this.getUserMessages(['visualization', 'visualizationOnEmbeddable'], {
+ severity: 'error',
+ });
+
+ if (errors.length && this.domNode) {
+ render(
+ <>
+
+
+
+
+
+ {
+ this.badgeDomNode = el;
+ this.renderBadgeMessages();
+ }}
+ />
+ >,
+ this.domNode
+ );
+ }
+
+ this.renderBadgeMessages();
}
- private renderBadgeMessages() {
+ badgeDomNode?: HTMLDivElement;
+
+ /**
+ * This method is called on every render, and also whenever the badges dom node is created
+ * That happens after either the expression renderer or the visualization error panel is rendered.
+ *
+ * You should not call this method on its own. Use renderUserMessages instead.
+ */
+ private renderBadgeMessages = () => {
const messages = this.getUserMessages('embeddableBadge');
if (messages.length && this.badgeDomNode) {
@@ -923,7 +1023,7 @@ export class Embeddable
this.badgeDomNode
);
}
- }
+ };
private readonly hasCompatibleActions = async (
event: ExpressionRendererEvent
@@ -1186,7 +1286,10 @@ export class Embeddable
]);
}
- if (this.hasAnyErrors) {
+ const blockingErrors = this.getUserMessages(blockingMessageDisplayLocations, {
+ severity: 'error',
+ });
+ if (blockingErrors.length) {
this.logError('validation');
}
diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx
index 717e28d94ab7a..dbe950661b2fa 100644
--- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx
+++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx
@@ -7,8 +7,6 @@
import React from 'react';
import { I18nProvider } from '@kbn/i18n-react';
-import { FormattedMessage } from '@kbn/i18n-react';
-import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon, EuiEmptyPrompt } from '@elastic/eui';
import {
ExpressionRendererEvent,
ReactExpressionRendererProps,
@@ -20,12 +18,11 @@ import { DefaultInspectorAdapters, RenderMode } from '@kbn/expressions-plugin/co
import classNames from 'classnames';
import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper';
import { LensInspector } from '../lens_inspector_service';
-import { UserMessage } from '../types';
+import { AddUserMessages } from '../types';
export interface ExpressionWrapperProps {
ExpressionRenderer: ReactExpressionRendererType;
expression: string | null;
- errors: UserMessage[];
variables?: Record;
interactive?: boolean;
searchContext: ExecutionContextSearch;
@@ -44,64 +41,13 @@ export interface ExpressionWrapperProps {
getCompatibleCellValueActions?: ReactExpressionRendererProps['getCompatibleCellValueActions'];
style?: React.CSSProperties;
className?: string;
- canEdit: boolean;
+ addUserMessages: AddUserMessages;
onRuntimeError: (message?: string) => void;
executionContext?: KibanaExecutionContext;
lensInspector: LensInspector;
noPadding?: boolean;
}
-interface VisualizationErrorProps {
- errors: ExpressionWrapperProps['errors'];
- canEdit: boolean;
-}
-
-export function VisualizationErrorPanel({ errors, canEdit }: VisualizationErrorProps) {
- const showMore = errors.length > 1;
- const canFixInLens = canEdit && errors.some(({ fixableInEditor }) => fixableInEditor);
- return (
-
-
- {errors.length ? (
- <>
- {errors[0].longMessage}
- {showMore && !canFixInLens ? (
-
-
-
- ) : null}
- {canFixInLens ? (
-
-
-
- ) : null}
- >
- ) : (
-
-
-
- )}
- >
- }
- />
-
- );
-}
-
export function ExpressionWrapper({
ExpressionRenderer: ExpressionRendererComponent,
expression,
@@ -120,60 +66,53 @@ export function ExpressionWrapper({
getCompatibleCellValueActions,
style,
className,
- errors,
- canEdit,
onRuntimeError,
+ addUserMessages,
executionContext,
lensInspector,
noPadding,
}: ExpressionWrapperProps) {
+ if (!expression) return null;
return (
- {errors.length || expression === null || expression === '' ? (
-
- ) : (
-
-
{
- const messages = getOriginalRequestErrorMessages(error);
- onRuntimeError(messages[0] ?? errorMessage);
+
+
{
+ const messages = getOriginalRequestErrorMessages(error);
+ addUserMessages(
+ messages.map((message) => ({
+ uniqueId: message,
+ severity: 'error',
+ displayLocations: [{ id: 'visualizationOnEmbeddable' }],
+ longMessage: message,
+ shortMessage: message,
+ fixableInEditor: false,
+ }))
+ );
+ onRuntimeError(messages[0] ?? errorMessage);
- return (
-
-
-
-
-
-
- {messages.map((message) => (
- {message}
- ))}
-
-
-
- );
- }}
- onEvent={handleEvent}
- hasCompatibleActions={hasCompatibleActions}
- getCompatibleCellValueActions={getCompatibleCellValueActions}
- />
-
- )}
+ return <>>; // the embeddable will take care of displaying the messages
+ }}
+ onEvent={handleEvent}
+ hasCompatibleActions={hasCompatibleActions}
+ getCompatibleCellValueActions={getCompatibleCellValueActions}
+ />
+
);
}
diff --git a/x-pack/test/functional/fixtures/kbn_archiver/lens/missing_fields.json b/x-pack/test/functional/fixtures/kbn_archiver/lens/missing_fields.json
index 5668dea137400..20f8c90f92ec0 100644
--- a/x-pack/test/functional/fixtures/kbn_archiver/lens/missing_fields.json
+++ b/x-pack/test/functional/fixtures/kbn_archiver/lens/missing_fields.json
@@ -289,4 +289,4 @@
"type": "dashboard",
"updated_at": "2023-02-07T14:59:20.822Z",
"version": "WzM5MCwxXQ=="
-}
+}
\ No newline at end of file
From 10c2f4ae5af85f040c47ba2e310b570d230ee567 Mon Sep 17 00:00:00 2001
From: jennypavlova
Date: Wed, 15 Feb 2023 15:33:15 +0100
Subject: [PATCH 018/210] [Logs and Metrics UI] Make spaces optional plugin
(#151053)
Closes #149973
# Summary
This PR makes `spaces` an optional plugin. In Logs UI we use only the
space id and there we want to fallback to the default space id when the
space plugin is disabled. The difference to the changes in the other PRs
is that we will consider the default space as "active" space if the
space plugin is disabled: so in this case we will return the default
space id as this is the only property we need from the `getActiveSpace`
response.
# Testing
1. In the `kibana.dev.yaml` add `xpack.spaces.enabled: false`
2. Before we have this
[PR](https://github.com/elastic/kibana/pull/151147) merged you should do
[this
change](https://github.com/elastic/kibana/pull/151147/files#diff-1b17eae66f358505fae8d86df37e155a25e8db996fce93ee6016582fb341092e)
on the branch while testing
3. Inside Logs the `Anomalies` and `Categories` pages should load the
default space
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/infra/kibana.jsonc | 2 +-
.../plugins/infra/public/hooks/use_kibana_space.ts | 14 ++++++++++++--
x-pack/plugins/infra/public/types.ts | 2 +-
.../adapters/framework/kibana_framework_adapter.ts | 13 ++-----------
x-pack/plugins/infra/server/plugin.ts | 3 ++-
5 files changed, 18 insertions(+), 16 deletions(-)
diff --git a/x-pack/plugins/infra/kibana.jsonc b/x-pack/plugins/infra/kibana.jsonc
index 4e0845922ad8c..bb0e19db45e97 100644
--- a/x-pack/plugins/infra/kibana.jsonc
+++ b/x-pack/plugins/infra/kibana.jsonc
@@ -15,7 +15,6 @@
"share",
"features",
"usageCollection",
- "spaces",
"embeddable",
"data",
"dataViews",
@@ -28,6 +27,7 @@
"unifiedSearch"
],
"optionalPlugins": [
+ "spaces",
"ml",
"home",
"embeddable",
diff --git a/x-pack/plugins/infra/public/hooks/use_kibana_space.ts b/x-pack/plugins/infra/public/hooks/use_kibana_space.ts
index 5a90327ff0581..1b468c2b76c11 100644
--- a/x-pack/plugins/infra/public/hooks/use_kibana_space.ts
+++ b/x-pack/plugins/infra/public/hooks/use_kibana_space.ts
@@ -5,8 +5,8 @@
* 2.0.
*/
-import useAsync from 'react-use/lib/useAsync';
import type { Space } from '@kbn/spaces-plugin/public';
+import useAsync from 'react-use/lib/useAsync';
import { useKibanaContextForPlugin } from './use_kibana';
export type ActiveSpace =
@@ -14,10 +14,20 @@ export type ActiveSpace =
| { isLoading: false; error: Error; space: undefined }
| { isLoading: false; error: undefined; space: Space };
+// Fallback to default if spaces plugin is not available
+const getDefaultSpaceAsPromise = () =>
+ Promise.resolve({
+ id: 'default',
+ name: 'Default',
+ disabledFeatures: [],
+ });
+
export const useActiveKibanaSpace = (): ActiveSpace => {
const kibana = useKibanaContextForPlugin();
+ const getActiveSpaceOrDefault =
+ kibana.services?.spaces?.getActiveSpace ?? getDefaultSpaceAsPromise;
- const asyncActiveSpace = useAsync(kibana.services.spaces.getActiveSpace);
+ const asyncActiveSpace = useAsync(getActiveSpaceOrDefault);
if (asyncActiveSpace.loading) {
return {
diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts
index cf4f3a0c8b934..97bf31d102558 100644
--- a/x-pack/plugins/infra/public/types.ts
+++ b/x-pack/plugins/infra/public/types.ts
@@ -71,7 +71,7 @@ export interface InfraClientStartDeps {
unifiedSearch: UnifiedSearchPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
observability: ObservabilityPublicStart;
- spaces: SpacesPluginStart;
+ spaces?: SpacesPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
usageCollection: UsageCollectionStart;
ml: MlPluginStart;
diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts
index 5c6cde7480c7f..7080a513da82c 100644
--- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts
+++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts
@@ -11,6 +11,7 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/serve
import { CoreSetup, IRouter, KibanaRequest, RequestHandler, RouteMethod } from '@kbn/core/server';
import { UI_SETTINGS } from '@kbn/data-plugin/server';
import { TimeseriesVisData } from '@kbn/vis-type-timeseries-plugin/server';
+import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { TSVBMetricModel } from '../../../../common/inventory_models/types';
import { InfraConfig } from '../../../plugin';
import type { InfraPluginRequestHandlerContext } from '../../../types';
@@ -213,17 +214,7 @@ export class KibanaFramework {
}
public getSpaceId(request: KibanaRequest): string {
- const spacesPlugin = this.plugins.spaces;
-
- if (
- spacesPlugin &&
- spacesPlugin.spacesService &&
- typeof spacesPlugin.spacesService.getSpaceId === 'function'
- ) {
- return spacesPlugin.spacesService.getSpaceId(request);
- } else {
- return 'default';
- }
+ return this.plugins.spaces?.spacesService?.getSpaceId(request) ?? DEFAULT_SPACE_ID;
}
public async makeTSVBRequest(
diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts
index a7fa9ceacd3c9..67563f857aedb 100644
--- a/x-pack/plugins/infra/server/plugin.ts
+++ b/x-pack/plugins/infra/server/plugin.ts
@@ -16,6 +16,7 @@ import {
import { handleEsError } from '@kbn/es-ui-shared-plugin/server';
import { i18n } from '@kbn/i18n';
import { Logger } from '@kbn/logging';
+import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants';
import { defaultLogViewsStaticConfig } from '../common/log_views';
import { publicConfigKeys } from '../common/plugin_config_types';
@@ -196,7 +197,7 @@ export class InfraServerPlugin
const soClient = (await context.core).savedObjects.client;
const mlSystem = plugins.ml?.mlSystemProvider(request, soClient);
const mlAnomalyDetectors = plugins.ml?.anomalyDetectorsProvider(request, soClient);
- const spaceId = plugins.spaces?.spacesService.getSpaceId(request) || 'default';
+ const spaceId = plugins.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
return {
mlAnomalyDetectors,
From d579b2fa9c75b76c82572c1c1e7817efdbcc245d Mon Sep 17 00:00:00 2001
From: Tim Grein
Date: Wed, 15 Feb 2023 15:36:23 +0100
Subject: [PATCH 019/210] [Enterprise Search] Fix sync flyout (#151175)
---
x-pack/plugins/enterprise_search/common/types/connectors.ts | 2 +-
.../components/search_index/sync_jobs/sync_job_flyout.tsx | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/common/types/connectors.ts b/x-pack/plugins/enterprise_search/common/types/connectors.ts
index d400259cc56ce..5869d19ae42dc 100644
--- a/x-pack/plugins/enterprise_search/common/types/connectors.ts
+++ b/x-pack/plugins/enterprise_search/common/types/connectors.ts
@@ -170,7 +170,7 @@ export interface ConnectorSyncJob {
completed_at: string | null;
connector: {
configuration: ConnectorConfiguration;
- filtering: FilteringRules[] | null;
+ filtering: FilteringRules | FilteringRules[] | null;
id: string;
index_name: string;
language: string;
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_job_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_job_flyout.tsx
index 13a89d7d4eedd..95fbefa8c773b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_job_flyout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_job_flyout.tsx
@@ -33,7 +33,11 @@ interface SyncJobFlyoutProps {
}
export const SyncJobFlyout: React.FC = ({ onClose, syncJob }) => {
- const filtering = syncJob?.connector.filtering ? syncJob.connector.filtering[0] : null;
+ const filtering = syncJob?.connector.filtering
+ ? Array.isArray(syncJob?.connector.filtering)
+ ? syncJob?.connector.filtering?.[0]
+ : syncJob?.connector.filtering
+ : null;
const visible = !!syncJob;
return visible ? (
From de87ac12c83e933eabe8334849ea1d580dfa3e91 Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Wed, 15 Feb 2023 09:56:21 -0500
Subject: [PATCH 020/210] [Fleet] Deleting a FleetProxy should bump related
agent policies (#151205)
---
.../server/routes/fleet_proxies/handler.ts | 87 ++++++----
.../server/services/fleet_proxies.test.ts | 155 ++++++++++++++++++
.../fleet/server/services/fleet_proxies.ts | 75 ++++++++-
.../apis/fleet_proxies/crud.ts | 74 +++++++++
4 files changed, 353 insertions(+), 38 deletions(-)
create mode 100644 x-pack/plugins/fleet/server/services/fleet_proxies.test.ts
diff --git a/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts b/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts
index ed77dddb4bf88..5b00baf9bf2c1 100644
--- a/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts
+++ b/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts
@@ -6,7 +6,11 @@
*/
import type { RequestHandler } from '@kbn/core/server';
-import { SavedObjectsErrorHelpers } from '@kbn/core/server';
+import {
+ SavedObjectsErrorHelpers,
+ type SavedObjectsClientContract,
+ type ElasticsearchClient,
+} from '@kbn/core/server';
import type { TypeOf } from '@kbn/config-schema';
import pMap from 'p-map';
@@ -16,15 +20,51 @@ import {
deleteFleetProxy,
getFleetProxy,
updateFleetProxy,
+ getFleetProxyRelatedSavedObjects,
} from '../../services/fleet_proxies';
import { defaultFleetErrorHandler } from '../../errors';
import type {
GetOneFleetProxyRequestSchema,
PostFleetProxyRequestSchema,
PutFleetProxyRequestSchema,
+ FleetServerHost,
+ Output,
} from '../../types';
-import { listFleetServerHostsForProxyId } from '../../services/fleet_server_host';
-import { agentPolicyService, outputService } from '../../services';
+import { agentPolicyService } from '../../services';
+
+async function bumpRelatedPolicies(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ fleetServerHosts: FleetServerHost[],
+ outputs: Output[]
+) {
+ if (
+ fleetServerHosts.some((host) => host.is_default) ||
+ outputs.some((output) => output.is_default || output.is_default_monitoring)
+ ) {
+ await agentPolicyService.bumpAllAgentPolicies(soClient, esClient);
+ } else {
+ await pMap(
+ outputs,
+ (output) => agentPolicyService.bumpAllAgentPoliciesForOutput(soClient, esClient, output.id),
+ {
+ concurrency: 20,
+ }
+ );
+ await pMap(
+ fleetServerHosts,
+ (fleetServerHost) =>
+ agentPolicyService.bumpAllAgentPoliciesForFleetServerHosts(
+ soClient,
+ esClient,
+ fleetServerHost.id
+ ),
+ {
+ concurrency: 20,
+ }
+ );
+ }
+}
export const postFleetProxyHandler: RequestHandler<
undefined,
@@ -64,36 +104,8 @@ export const putFleetProxyHandler: RequestHandler<
};
// Bump all the agent policy that use that proxy
- const [{ items: fleetServerHosts }, { items: outputs }] = await Promise.all([
- listFleetServerHostsForProxyId(soClient, proxyId),
- outputService.listAllForProxyId(soClient, proxyId),
- ]);
- if (
- fleetServerHosts.some((host) => host.is_default) ||
- outputs.some((output) => output.is_default || output.is_default_monitoring)
- ) {
- await agentPolicyService.bumpAllAgentPolicies(soClient, esClient);
- } else {
- await pMap(
- outputs,
- (output) => agentPolicyService.bumpAllAgentPoliciesForOutput(soClient, esClient, output.id),
- {
- concurrency: 20,
- }
- );
- await pMap(
- fleetServerHosts,
- (fleetServerHost) =>
- agentPolicyService.bumpAllAgentPoliciesForFleetServerHosts(
- soClient,
- esClient,
- fleetServerHost.id
- ),
- {
- concurrency: 20,
- }
- );
- }
+ const { fleetServerHosts, outputs } = await getFleetProxyRelatedSavedObjects(soClient, proxyId);
+ await bumpRelatedPolicies(soClient, esClient, fleetServerHosts, outputs);
return response.ok({ body });
} catch (error) {
@@ -109,6 +121,7 @@ export const putFleetProxyHandler: RequestHandler<
export const getAllFleetProxyHandler: RequestHandler = async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
+
try {
const res = await listFleetProxies(soClient);
const body = {
@@ -128,9 +141,17 @@ export const deleteFleetProxyHandler: RequestHandler<
TypeOf
> = async (context, request, response) => {
try {
+ const proxyId = request.params.itemId;
const coreContext = await context.core;
const soClient = coreContext.savedObjects.client;
+ const esClient = coreContext.elasticsearch.client.asInternalUser;
+
+ const { fleetServerHosts, outputs } = await getFleetProxyRelatedSavedObjects(soClient, proxyId);
+
await deleteFleetProxy(soClient, request.params.itemId);
+
+ await bumpRelatedPolicies(soClient, esClient, fleetServerHosts, outputs);
+
const body = {
id: request.params.itemId,
};
diff --git a/x-pack/plugins/fleet/server/services/fleet_proxies.test.ts b/x-pack/plugins/fleet/server/services/fleet_proxies.test.ts
new file mode 100644
index 0000000000000..024860e0b6490
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/fleet_proxies.test.ts
@@ -0,0 +1,155 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { savedObjectsClientMock } from '@kbn/core/server/mocks';
+
+import { FLEET_PROXY_SAVED_OBJECT_TYPE } from '../constants';
+
+import { deleteFleetProxy } from './fleet_proxies';
+import { listFleetServerHostsForProxyId, updateFleetServerHost } from './fleet_server_host';
+import { outputService } from './output';
+
+jest.mock('./output');
+jest.mock('./fleet_server_host');
+
+const mockedListFleetServerHostsForProxyId = listFleetServerHostsForProxyId as jest.MockedFunction<
+ typeof listFleetServerHostsForProxyId
+>;
+
+const mockedUpdateFleetServerHost = updateFleetServerHost as jest.MockedFunction<
+ typeof updateFleetServerHost
+>;
+
+const mockedOutputService = outputService as jest.Mocked;
+
+const PROXY_IDS = {
+ PRECONFIGURED: 'test-preconfigured',
+ RELATED_PRECONFIGURED: 'test-related-preconfigured',
+};
+
+describe('Fleet proxies service', () => {
+ const soClientMock = savedObjectsClientMock.create();
+
+ beforeEach(() => {
+ mockedOutputService.update.mockReset();
+ soClientMock.delete.mockReset();
+ mockedUpdateFleetServerHost.mockReset();
+ mockedOutputService.listAllForProxyId.mockImplementation(async (_, proxyId) => {
+ if (proxyId === PROXY_IDS.RELATED_PRECONFIGURED) {
+ return {
+ items: [
+ {
+ id: 'test',
+ is_preconfigured: true,
+ type: 'elasticsearch',
+ name: 'test',
+ proxy_id: proxyId,
+ is_default: false,
+ is_default_monitoring: false,
+ },
+ ],
+ total: 1,
+ page: 1,
+ perPage: 10,
+ };
+ }
+
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ perPage: 10,
+ };
+ });
+ mockedListFleetServerHostsForProxyId.mockImplementation(async (_, proxyId) => {
+ if (proxyId === PROXY_IDS.RELATED_PRECONFIGURED) {
+ return {
+ items: [
+ {
+ id: 'test',
+ is_preconfigured: true,
+ host_urls: ['http://test.fr'],
+ is_default: false,
+ name: 'test',
+ proxy_id: proxyId,
+ },
+ ],
+ total: 1,
+ page: 1,
+ perPage: 10,
+ };
+ }
+
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ perPage: 10,
+ };
+ });
+ soClientMock.get.mockImplementation(async (type, id) => {
+ if (type !== FLEET_PROXY_SAVED_OBJECT_TYPE) {
+ throw new Error(`${type} not mocked in SO client`);
+ }
+
+ if (id === PROXY_IDS.PRECONFIGURED) {
+ return {
+ id,
+ type,
+ attributes: {
+ is_preconfigured: true,
+ },
+ references: [],
+ };
+ }
+
+ if (id === PROXY_IDS.RELATED_PRECONFIGURED) {
+ return {
+ id,
+ type,
+ attributes: {
+ is_preconfigured: false,
+ },
+ references: [],
+ };
+ }
+
+ throw new Error(`${id} not found`);
+ });
+ });
+
+ describe('delete', () => {
+ it('should not allow to delete preconfigured proxy', async () => {
+ await expect(() =>
+ deleteFleetProxy(soClientMock, PROXY_IDS.PRECONFIGURED)
+ ).rejects.toThrowError(/Cannot delete test-preconfigured preconfigured proxy/);
+ });
+
+ it('should allow to delete preconfigured proxy with option fromPreconfiguration:true', async () => {
+ await deleteFleetProxy(soClientMock, PROXY_IDS.PRECONFIGURED, { fromPreconfiguration: true });
+
+ expect(soClientMock.delete).toBeCalled();
+ });
+
+ it('should not allow to delete proxy wiht related preconfigured saved object', async () => {
+ await expect(() =>
+ deleteFleetProxy(soClientMock, PROXY_IDS.RELATED_PRECONFIGURED)
+ ).rejects.toThrowError(
+ /Cannot delete a proxy used in a preconfigured fleet server hosts or output./
+ );
+ });
+
+ it('should allow to delete proxy wiht related preconfigured saved object option fromPreconfiguration:true', async () => {
+ await deleteFleetProxy(soClientMock, PROXY_IDS.RELATED_PRECONFIGURED, {
+ fromPreconfiguration: true,
+ });
+ expect(mockedOutputService.update).toBeCalled();
+ expect(mockedUpdateFleetServerHost).toBeCalled();
+ expect(soClientMock.delete).toBeCalled();
+ });
+ });
+});
diff --git a/x-pack/plugins/fleet/server/services/fleet_proxies.ts b/x-pack/plugins/fleet/server/services/fleet_proxies.ts
index 17ab88a889034..09868568e84a8 100644
--- a/x-pack/plugins/fleet/server/services/fleet_proxies.ts
+++ b/x-pack/plugins/fleet/server/services/fleet_proxies.ts
@@ -6,10 +6,21 @@
*/
import type { SavedObjectsClientContract, SavedObject } from '@kbn/core/server';
+import { omit } from 'lodash';
+import pMap from 'p-map';
import { FLEET_PROXY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../constants';
import { FleetProxyUnauthorizedError } from '../errors';
-import type { FleetProxy, FleetProxySOAttributes, NewFleetProxy } from '../types';
+import type {
+ FleetProxy,
+ FleetProxySOAttributes,
+ FleetServerHost,
+ NewFleetProxy,
+ Output,
+} from '../types';
+
+import { listFleetServerHostsForProxyId, updateFleetServerHost } from './fleet_server_host';
+import { outputService } from './output';
function savedObjectToFleetProxy(so: SavedObject): FleetProxy {
const { proxy_headers: proxyHeaders, ...rest } = so.attributes;
@@ -79,14 +90,25 @@ export async function deleteFleetProxy(
id: string,
options?: { fromPreconfiguration?: boolean }
) {
- const fleetServerHost = await getFleetProxy(soClient, id);
+ const fleetProxy = await getFleetProxy(soClient, id);
- if (fleetServerHost.is_preconfigured && !options?.fromPreconfiguration) {
+ if (fleetProxy.is_preconfigured && !options?.fromPreconfiguration) {
throw new FleetProxyUnauthorizedError(`Cannot delete ${id} preconfigured proxy`);
}
+ const { outputs, fleetServerHosts } = await getFleetProxyRelatedSavedObjects(soClient, id);
+
+ if (
+ [...fleetServerHosts, ...outputs].some(
+ (fleetServerHostOrOutput) => fleetServerHostOrOutput.is_preconfigured
+ ) &&
+ !options?.fromPreconfiguration
+ ) {
+ throw new FleetProxyUnauthorizedError(
+ 'Cannot delete a proxy used in a preconfigured fleet server hosts or output.'
+ );
+ }
- // TODO remove from all outputs and fleet server
- // await agentPolicyService.removeFleetServerHostFromAll(soClient, esClient, id);
+ await updateRelatedSavedObject(soClient, fleetServerHosts, outputs);
return await soClient.delete(FLEET_PROXY_SAVED_OBJECT_TYPE, id);
}
@@ -147,3 +169,46 @@ export async function bulkGetFleetProxies(
typeof fleetProxyOrUndefined !== 'undefined'
);
}
+
+async function updateRelatedSavedObject(
+ soClient: SavedObjectsClientContract,
+ fleetServerHosts: FleetServerHost[],
+ outputs: Output[]
+) {
+ await pMap(
+ fleetServerHosts,
+ (fleetServerHost) => {
+ updateFleetServerHost(soClient, fleetServerHost.id, {
+ ...omit(fleetServerHost, 'id'),
+ proxy_id: null,
+ });
+ },
+ { concurrency: 20 }
+ );
+
+ await pMap(
+ outputs,
+ (output) => {
+ outputService.update(soClient, output.id, {
+ ...omit(output, 'id'),
+ proxy_id: null,
+ });
+ },
+ { concurrency: 20 }
+ );
+}
+
+export async function getFleetProxyRelatedSavedObjects(
+ soClient: SavedObjectsClientContract,
+ proxyId: string
+) {
+ const [{ items: fleetServerHosts }, { items: outputs }] = await Promise.all([
+ listFleetServerHostsForProxyId(soClient, proxyId),
+ outputService.listAllForProxyId(soClient, proxyId),
+ ]);
+
+ return {
+ fleetServerHosts,
+ outputs,
+ };
+}
diff --git a/x-pack/test/fleet_api_integration/apis/fleet_proxies/crud.ts b/x-pack/test/fleet_api_integration/apis/fleet_proxies/crud.ts
index 91895867404be..b769b9fdb2618 100644
--- a/x-pack/test/fleet_api_integration/apis/fleet_proxies/crud.ts
+++ b/x-pack/test/fleet_api_integration/apis/fleet_proxies/crud.ts
@@ -15,6 +15,21 @@ export default function (providerContext: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
+ const es = getService('es');
+
+ async function getLatestFleetPolicies(policyId: string): Promise {
+ const policyDocRes = await es.search({
+ index: '.fleet-policies',
+ query: {
+ term: {
+ policy_id: policyId,
+ },
+ },
+ sort: [{ '@timestamp': 'desc' }],
+ });
+
+ return policyDocRes.hits.hits[0]?._source;
+ }
describe('fleet_proxies_crud', async function () {
skipIfNoDockerRegistry(providerContext);
@@ -25,6 +40,9 @@ export default function (providerContext: FtrProviderContext) {
setupFleetAndAgents(providerContext);
const existingId = 'test-default-123';
+ const fleetServerHostId = 'test-fleetserver-123';
+ const policyId = 'test-policy-123';
+ const outputId = 'test-output-123';
before(async function () {
await kibanaServer.savedObjects.clean({
@@ -39,10 +57,46 @@ export default function (providerContext: FtrProviderContext) {
url: 'https://test.fr:3232',
})
.expect(200);
+ await supertest
+ .post(`/api/fleet/fleet_server_hosts`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ id: fleetServerHostId,
+ name: 'Test 123',
+ host_urls: ['https://fleetserverhost.fr:3232'],
+ proxy_id: existingId,
+ })
+ .expect(200);
+ await supertest
+ .post(`/api/fleet/outputs`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ id: outputId,
+ name: 'Test 123',
+ type: 'elasticsearch',
+ hosts: ['http://es:9200'],
+ proxy_id: existingId,
+ })
+ .expect(200);
+
+ await supertest
+ .post(`/api/fleet/agent_policies`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ id: policyId,
+ name: 'Test 123',
+ namespace: 'default',
+ fleet_server_host_id: fleetServerHostId,
+ data_output_id: outputId,
+ })
+ .expect(200);
});
after(async () => {
await kibanaServer.savedObjects.cleanStandardList();
+ await kibanaServer.savedObjects.clean({
+ types: ['fleet-proxy'],
+ });
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
});
@@ -82,6 +136,7 @@ export default function (providerContext: FtrProviderContext) {
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Test 123 updated',
+ url: 'https://testupdated.fr:3232',
})
.expect(200);
@@ -90,6 +145,12 @@ export default function (providerContext: FtrProviderContext) {
} = await supertest.get(`/api/fleet/proxies/${existingId}`).expect(200);
expect(fleetServerHost.name).to.eql('Test 123 updated');
+
+ const fleetPolicyAfter = await getLatestFleetPolicies(policyId);
+ expect(fleetPolicyAfter?.data?.fleet?.proxy_url).to.be('https://testupdated.fr:3232');
+ expect(fleetPolicyAfter?.data?.outputs?.[outputId].proxy_url).to.be(
+ 'https://testupdated.fr:3232'
+ );
});
it('should return a 404 when updating a non existing fleet proxy', async function () {
@@ -102,5 +163,18 @@ export default function (providerContext: FtrProviderContext) {
.expect(404);
});
});
+
+ describe('DELETE /proxies/{itemId}', () => {
+ it('should allow to delete an existing fleet proxy', async function () {
+ await supertest
+ .delete(`/api/fleet/proxies/${existingId}`)
+ .set('kbn-xsrf', 'xxxx')
+ .expect(200);
+
+ const fleetPolicyAfter = await getLatestFleetPolicies(policyId);
+ expect(fleetPolicyAfter?.data?.fleet?.proxy_url).to.be(undefined);
+ expect(fleetPolicyAfter?.data?.outputs?.[outputId].proxy_url).to.be(undefined);
+ });
+ });
});
}
From d37a4d6fbed4b490c9d5753679dcc8eb3b57f89e Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Wed, 15 Feb 2023 08:02:38 -0700
Subject: [PATCH 021/210] [Maps] fix Map crashes on field edit when hitting
backspace key (#151203)
Fixes https://github.com/elastic/kibana/issues/151121
There were 2 issues
1) user could delete right field, putting layer in non-working state. To
fix this, update handler is not called when selected value is undefined
2) createJoinTermSource guard was not working for `{ term: undefined }`.
PR updated guard to properly handle this case.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../public/classes/joins/inner_join.test.js | 60 +++++++++++++++++--
.../maps/public/classes/joins/inner_join.ts | 6 +-
.../sources/es_term_source/es_term_source.ts | 5 +-
.../join_editor/resources/join_expression.tsx | 10 +++-
4 files changed, 68 insertions(+), 13 deletions(-)
diff --git a/x-pack/plugins/maps/public/classes/joins/inner_join.test.js b/x-pack/plugins/maps/public/classes/joins/inner_join.test.js
index f8ca092e9a164..b01d977972a68 100644
--- a/x-pack/plugins/maps/public/classes/joins/inner_join.test.js
+++ b/x-pack/plugins/maps/public/classes/joins/inner_join.test.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { InnerJoin } from './inner_join';
+import { createJoinTermSource, InnerJoin } from './inner_join';
import { SOURCE_TYPES } from '../../../common/constants';
jest.mock('../../kibana_services', () => {});
@@ -38,8 +38,56 @@ const leftJoin = new InnerJoin(
);
const COUNT_PROPERTY_NAME = '__kbnjoin__count__d3625663-5b34-4d50-a784-0d743f676a0c';
+describe('createJoinTermSource', () => {
+ test('Should return undefined when descriptor is not provided', () => {
+ expect(createJoinTermSource(undefined)).toBe(undefined);
+ });
+
+ test('Should return undefined with unmatched source type', () => {
+ expect(
+ createJoinTermSource({
+ type: SOURCE_TYPES.WMS,
+ })
+ ).toBe(undefined);
+ });
+
+ describe('EsTermSource', () => {
+ test('Should return EsTermSource', () => {
+ expect(createJoinTermSource(rightSource).constructor.name).toBe('ESTermSource');
+ });
+
+ test('Should return undefined when indexPatternId is undefined', () => {
+ expect(
+ createJoinTermSource({
+ ...rightSource,
+ indexPatternId: undefined,
+ })
+ ).toBe(undefined);
+ });
+
+ test('Should return undefined when term is undefined', () => {
+ expect(
+ createJoinTermSource({
+ ...rightSource,
+ term: undefined,
+ })
+ ).toBe(undefined);
+ });
+ });
+
+ describe('TableSource', () => {
+ test('Should return TableSource', () => {
+ expect(
+ createJoinTermSource({
+ type: SOURCE_TYPES.TABLE_SOURCE,
+ }).constructor.name
+ ).toBe('TableSource');
+ });
+ });
+});
+
describe('joinPropertiesToFeature', () => {
- it('Should add join property to features in feature collection', () => {
+ test('Should add join property to features in feature collection', () => {
const feature = {
properties: {
iso2: 'CN',
@@ -59,7 +107,7 @@ describe('joinPropertiesToFeature', () => {
});
});
- it('Should delete previous join property values from feature', () => {
+ test('Should delete previous join property values from feature', () => {
const feature = {
properties: {
iso2: 'CN',
@@ -79,7 +127,7 @@ describe('joinPropertiesToFeature', () => {
});
});
- it('Should coerce to string before joining', () => {
+ test('Should coerce to string before joining', () => {
const leftJoin = new InnerJoin(
{
leftField: 'zipcode',
@@ -107,7 +155,7 @@ describe('joinPropertiesToFeature', () => {
});
});
- it('Should handle undefined values', () => {
+ test('Should handle undefined values', () => {
const feature = {
//this feature does not have the iso2 field
properties: {
@@ -127,7 +175,7 @@ describe('joinPropertiesToFeature', () => {
});
});
- it('Should handle falsy values', () => {
+ test('Should handle falsy values', () => {
const leftJoin = new InnerJoin(
{
leftField: 'code',
diff --git a/x-pack/plugins/maps/public/classes/joins/inner_join.ts b/x-pack/plugins/maps/public/classes/joins/inner_join.ts
index b8b96352625fc..b3a55ea55decd 100644
--- a/x-pack/plugins/maps/public/classes/joins/inner_join.ts
+++ b/x-pack/plugins/maps/public/classes/joins/inner_join.ts
@@ -26,7 +26,7 @@ import { PropertiesMap } from '../../../common/elasticsearch_util';
import { ITermJoinSource } from '../sources/term_join_source';
import { TableSource } from '../sources/table_source';
-function createJoinTermSource(
+export function createJoinTermSource(
descriptor: Partial | undefined
): ITermJoinSource | undefined {
if (!descriptor) {
@@ -35,8 +35,8 @@ function createJoinTermSource(
if (
descriptor.type === SOURCE_TYPES.ES_TERM_SOURCE &&
- 'indexPatternId' in descriptor &&
- 'term' in descriptor
+ descriptor.indexPatternId !== undefined &&
+ descriptor.term !== undefined
) {
return new ESTermSource(descriptor as ESTermSourceDescriptor);
} else if (descriptor.type === SOURCE_TYPES.TABLE_SOURCE) {
diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts
index 71ce42be22ab0..a8a756eeb3aed 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import type { Query } from '@kbn/es-query';
import { ISearchSource } from '@kbn/data-plugin/public';
@@ -42,7 +41,7 @@ type ESTermSourceSyncMeta = Pick();
- const buckets: any[] = _.get(rawEsData, ['aggregations', TERMS_AGG_NAME, 'buckets'], []);
+ const buckets: any[] = rawEsData?.aggregations?.[TERMS_AGG_NAME]?.buckets ?? [];
buckets.forEach((termBucket: any) => {
const properties = extractPropertiesFromBucket(termBucket, TERMS_BUCKET_KEYS_TO_IGNORE);
if (countPropertyName) {
@@ -83,7 +82,7 @@ export class ESTermSource extends AbstractESAggSource implements ITermJoinSource
}
hasCompleteConfig(): boolean {
- return _.has(this._descriptor, 'indexPatternId') && _.has(this._descriptor, 'term');
+ return this._descriptor.indexPatternId !== undefined && this._descriptor.term !== undefined;
}
getTermField(): ESDocField {
diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx
index 9e55833698c7d..c70730c0f3f62 100644
--- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx
+++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx
@@ -84,6 +84,14 @@ export class JoinExpression extends Component {
this.props.onLeftFieldChange(_.get(selectedFields, '[0].value.name', null));
};
+ _onRightFieldChange = (term?: string) => {
+ if (!term || term.length === 0) {
+ return;
+ }
+
+ this.props.onRightFieldChange(term);
+ };
+
_renderLeftFieldSelect() {
const { leftValue, leftFields } = this.props;
@@ -167,7 +175,7 @@ export class JoinExpression extends Component {
From 56d64544bbb683cd380c18c6bbdf64f199c41567 Mon Sep 17 00:00:00 2001
From: Pablo Machado
Date: Wed, 15 Feb 2023 16:05:15 +0100
Subject: [PATCH 022/210] [SecuritySolutions] Fix show-top-n not rendered for
some security fields (#151257)
I went through every usage of `CellActions` and added the `aggregatable`
prop back to fields that previously had it.
### Why?
When I created CellActions I removed `isAgreggatable` from some fields
because every field with an aggregatable type would automatically
display `showTopN`. But later, I discovered that this approach didn't
work for some edge cases. I reintroduced the `isAgreggatable` prop but
forgot to add `isAgreggatable` back to those fields I had previously
removed it.
### Extra
It also fixes the network page country flag type from`geo_point` to
`keyword` to fix a bug.
It also adds an extra check to add_to_timeline `isCompatible` function
so it doesn't show for `geo_point` and `message` fields.
![Screenshot 2023-02-15 at 11 53
35](https://user-images.githubusercontent.com/1490444/219011030-c3be4cc4-c095-4b89-8a64-5e29d0b3ba1b.png)
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
.../actions/add_to_timeline/cell_action/add_to_timeline.ts | 5 +++--
.../public/actions/add_to_timeline/data_provider.ts | 5 ++++-
.../hosts/components/host_risk_score_table/columns.tsx | 1 +
.../public/explore/hosts/components/hosts_table/columns.tsx | 1 +
.../explore/network/components/network_dns_table/columns.tsx | 1 +
.../components/network_top_countries_table/columns.tsx | 1 +
.../network/components/network_top_n_flow_table/columns.tsx | 4 +++-
.../users/components/user_risk_score_table/columns.tsx | 1 +
8 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.ts
index c444a46cd6407..1fb2f5cee22f9 100644
--- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.ts
+++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_timeline.ts
@@ -18,7 +18,7 @@ import {
ADD_TO_TIMELINE_ICON,
ADD_TO_TIMELINE_SUCCESS_TITLE,
} from '../constants';
-import { createDataProviders } from '../data_provider';
+import { createDataProviders, isValidDataProviderField } from '../data_provider';
import { SecurityCellActionType } from '../../constants';
import type { StartServices } from '../../../types';
import type { SecurityCellAction } from '../../types';
@@ -38,7 +38,8 @@ export const createAddToTimelineCellActionFactory = createCellActionFactory(
getIconType: () => ADD_TO_TIMELINE_ICON,
getDisplayName: () => ADD_TO_TIMELINE,
getDisplayNameTooltip: () => ADD_TO_TIMELINE,
- isCompatible: async ({ field }) => fieldHasCellActions(field.name),
+ isCompatible: async ({ field }) =>
+ fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type),
execute: async ({ field, metadata }) => {
const dataProviders =
createDataProviders({
diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/data_provider.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/data_provider.ts
index a7593a93849de..e7c6cfcbe46be 100644
--- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/data_provider.ts
+++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/data_provider.ts
@@ -85,7 +85,7 @@ export const createDataProviders = ({
value ? `-${value}` : ''
}`;
- if (fieldType === GEO_FIELD_TYPE || field === MESSAGE_FIELD_NAME) {
+ if (!isValidDataProviderField(field, fieldType)) {
return dataProviders;
}
@@ -131,6 +131,9 @@ export const createDataProviders = ({
}, []);
};
+export const isValidDataProviderField = (fieldName: string, fieldType: string | undefined) =>
+ fieldType !== GEO_FIELD_TYPE && fieldName !== MESSAGE_FIELD_NAME;
+
const getIdForField = ({
field,
fieldFormat,
diff --git a/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx
index 3d7729a5e9ac7..557dcff247a6b 100644
--- a/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx
+++ b/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx
@@ -44,6 +44,7 @@ export const getHostRiskScoreColumns = ({
name: 'host.name',
value: hostName,
type: 'keyword',
+ aggregatable: true,
}}
>
diff --git a/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx
index 1dcff6fbb0afd..9078a103b389d 100644
--- a/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx
+++ b/x-pack/plugins/security_solution/public/explore/hosts/components/hosts_table/columns.tsx
@@ -44,6 +44,7 @@ export const getHostsColumns = (
name: 'host.name',
value: hostName[0],
type: 'keyword',
+ aggregatable: true,
}}
>
diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx
index c343742143ac2..ee87bab1208fd 100644
--- a/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx
+++ b/x-pack/plugins/security_solution/public/explore/network/components/network_dns_table/columns.tsx
@@ -50,6 +50,7 @@ export const getNetworkDnsColumns = (): NetworkDnsColumns => [
name: 'dns.question.registered_domain',
value: dnsName,
type: 'keyword',
+ aggregatable: true,
}}
>
{defaultToEmptyTag(dnsName)}
diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx
index d7b443e1ab326..314bd13042dc6 100644
--- a/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx
+++ b/x-pack/plugins/security_solution/public/explore/network/components/network_top_countries_table/columns.tsx
@@ -68,6 +68,7 @@ export const getNetworkTopCountriesColumns = (
name: geoAttr,
value: geo,
type: 'keyword',
+ aggregatable: true,
}}
>
diff --git a/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx
index 6d11df641286b..9ece66b14df97 100644
--- a/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx
+++ b/x-pack/plugins/security_solution/public/explore/network/components/network_top_n_flow_table/columns.tsx
@@ -76,6 +76,7 @@ export const getNetworkTopNFlowColumns = (
name: ipAttr,
value: ip,
type: 'keyword',
+ aggregatable: true,
}}
>
@@ -91,7 +92,8 @@ export const getNetworkTopNFlowColumns = (
field={{
name: geoAttrName,
value: geo,
- type: 'geo_point',
+ type: 'keyword',
+ aggregatable: true,
}}
>
{' '}
diff --git a/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx
index 3fd6d86cc0b92..86a57109ac431 100644
--- a/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx
+++ b/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx
@@ -47,6 +47,7 @@ export const getUserRiskScoreColumns = ({
name: 'user.name',
value: userName,
type: 'keyword',
+ aggregatable: true,
}}
>
From 8244a9f71362a49af0c3fe2a9da0b7f206c5b5fc Mon Sep 17 00:00:00 2001
From: Thomas Watson
Date: Wed, 15 Feb 2023 16:20:46 +0100
Subject: [PATCH 023/210] Remove unused minimist resolution (#151291)
---
package.json | 1 -
1 file changed, 1 deletion(-)
diff --git a/package.json b/package.json
index 66dd8cb08e49f..996b65f226ccb 100644
--- a/package.json
+++ b/package.json
@@ -83,7 +83,6 @@
"**/isomorphic-fetch/node-fetch": "^2.6.7",
"**/istanbul-lib-coverage": "^3.2.0",
"**/minimatch": "^3.1.2",
- "**/minimist": "^1.2.6",
"**/pdfkit/crypto-js": "4.0.0",
"**/react-syntax-highlighter": "^15.3.1",
"**/react-syntax-highlighter/**/highlight.js": "^10.4.1",
From 5de13d49acb69977d92498cc08e5bf03a7b365ff Mon Sep 17 00:00:00 2001
From: Jeramy Soucy
Date: Wed, 15 Feb 2023 10:25:05 -0500
Subject: [PATCH 024/210] [Saved Objects] Migrates authorization logic from
repository to security extension (#148165)
Closes #147049
Closes #149897
Migrates authorization and audit logic from the Saved Objects Repository
to the Saved Objects Security Extension. This is achieved by
implementing action-specific authorization methods within the security
extension. The SO repository is no longer responsible for making any
authorization decisions, but It is still responsible to know how to call
the extension methods. I've tried to make this as straightforward as
possible such that there is a clear ownership delineation between the
repository and the extension, by keeping the interface simple and
(hopefully) obvious.
### Security Extension Interface
New Public Extension Methods:
- authorizeCreate
- authorizeBulkCreate
- authorizeUpdate
- authorizeBulkUpdate
- authorizeDelete
- authorizeBulkDelete
- authorizeGet
- authorizeBulkGet
- authorizeCheckConflicts
- authorizeRemoveReferences
- authorizeOpenPointInTime
- auditClosePointInTime
- authorizeAndRedactMultiNamespaceReferences
- authorizeAndRedactInternalBulkResolve
- authorizeUpdateSpaces
- authorizeFind
- getFindRedactTypeMap
- authorizeDisableLegacyUrlAliases (for secure spaces client)
- auditObjectsForSpaceDeletion (for secure spaces client)
Removed from public interface:
- authorize
- enforceAuthorization
- addAuditEvent
### Tests
- Most test coverage moved from `repository.security_extension.test.ts`
to `saved_objects_security_extension.test.ts`
- `repository.security_extension.test.ts` tests extension call,
parameters, and return
- Updates repository unit tests to check that all security extension
calls are made with the current space when the spaces extension is also
enabled
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
---
docs/user/security/audit-logging.asciidoc | 14 +-
.../src/saved_objects_client.ts | 1 -
.../tsconfig.json | 2 +-
...collect_multi_namespace_references.test.ts | 190 +-
.../lib/collect_multi_namespace_references.ts | 211 +-
.../src/lib/decorate_es_error.test.ts | 2 +-
.../src/lib/decorate_es_error.ts | 2 +-
.../src/lib/filter_utils.ts | 2 +-
.../src/lib/internal_bulk_resolve.test.ts | 140 +-
.../src/lib/internal_bulk_resolve.ts | 119 +-
.../src/lib/internal_utils.ts | 15 +-
.../src/lib/preflight_check_for_create.ts | 12 +-
.../lib/repository.security_extension.test.ts | 1477 ++--
.../lib/repository.spaces_extension.test.ts | 313 +-
.../src/lib/repository.test.ts | 13 +-
.../src/lib/repository.ts | 450 +-
.../src/lib/repository_es_client.test.ts | 2 +-
.../src/lib/update_objects_spaces.test.ts | 401 +-
.../src/lib/update_objects_spaces.ts | 76 +-
.../mocks/saved_objects_extensions.mock.ts | 22 +-
.../src/saved_objects_client.ts | 3 +-
.../test_helpers/repository.test.common.ts | 92 +-
.../src/saved_objects_client.mock.ts | 2 +-
.../src/saved_objects_extensions.mock.ts | 22 +-
.../tsconfig.json | 1 -
.../src/version/decode_version.ts | 2 +-
.../src/saved_objects_client.ts | 1 -
.../core-saved-objects-common/index.ts | 2 +-
.../src/server_types.ts | 5 +
.../core-saved-objects-common/src/types.ts | 19 +
.../export/collect_exported_objects.test.ts | 2 +-
.../src/export/collect_exported_objects.ts | 2 +-
.../src/export/saved_objects_exporter.ts | 2 +-
.../src/import/import_saved_objects.test.ts | 2 +-
.../src/import/lib/check_conflicts.test.ts | 7 +-
.../src/import/lib/collect_saved_objects.ts | 1 -
.../import/lib/create_saved_objects.test.ts | 3 +-
.../src/import/lib/create_saved_objects.ts | 3 +-
.../import/lib/execute_import_hooks.test.ts | 3 +-
.../src/import/lib/execute_import_hooks.ts | 3 +-
.../src/import/lib/extract_errors.test.ts | 7 +-
.../src/import/lib/extract_errors.ts | 3 +-
.../get_import_state_map_for_retries.test.ts | 1 +
.../lib/get_import_state_map_for_retries.ts | 1 +
.../import/lib/validate_references.test.ts | 2 +-
.../src/import/resolve_import_errors.test.ts | 3 +-
.../src/import/resolve_import_errors.ts | 2 +-
.../tsconfig.json | 1 -
.../src/routes/utils.ts | 4 +-
.../tsconfig.json | 1 -
.../core-saved-objects-server/index.ts | 30 +-
.../src/extensions/extensions.ts | 3 +
.../src/extensions/security.ts | 526 +-
.../src/saved_objects_error_helpers.test.ts | 0
.../src/saved_objects_error_helpers.ts | 448 ++
.../src/saved_objects_management.ts | 2 +-
.../core-saved-objects-server/tsconfig.json | 1 -
.../core-saved-objects-utils-server/index.ts | 1 -
.../src/saved_objects_error_helpers.ts | 217 -
.../src/clients/ui_settings_client_common.ts | 2 +-
.../create_or_upgrade_saved_config.test.ts | 2 +-
.../create_or_upgrade_saved_config.ts | 2 +-
.../src/routes/delete.ts | 2 +-
.../src/routes/get.ts | 2 +-
.../src/routes/set.ts | 2 +-
.../src/routes/set_many.ts | 2 +-
.../src/saved_objects/transforms.test.ts | 2 +-
.../src/saved_objects/transforms.ts | 2 +-
.../tsconfig.json | 1 -
src/core/server/index.ts | 6 +-
.../saved_objects/routes/import.test.ts | 2 +-
.../get_telemetry_saved_object.ts | 2 +-
src/plugins/telemetry/tsconfig.json | 1 -
.../services/epm/archive/storage.test.ts | 2 +-
x-pack/plugins/fleet/tsconfig.json | 1 -
.../server/services/slo/slo_repository.ts | 2 +-
x-pack/plugins/observability/tsconfig.json | 1 -
.../server/audit/audit_events.test.ts | 2 +-
.../security/server/audit/audit_events.ts | 8 +-
.../saved_objects_security_extension.test.ts | 6213 ++++++++++++++++-
.../saved_objects_security_extension.ts | 1190 +++-
.../secure_spaces_client_wrapper.test.ts | 92 +-
.../spaces/secure_spaces_client_wrapper.ts | 62 +-
x-pack/plugins/security/tsconfig.json | 4 +
.../server/__mocks__/core.mock.ts | 3 +-
x-pack/plugins/spaces/common/index.ts | 8 +-
x-pack/plugins/spaces/common/types.ts | 18 -
.../components/types.ts | 2 +-
.../public/spaces_manager/spaces_manager.ts | 8 +-
x-pack/plugins/spaces/server/index.ts | 8 +-
.../server/spaces_client/spaces_client.ts | 9 +-
x-pack/plugins/spaces/tsconfig.json | 1 +
92 files changed, 9507 insertions(+), 3051 deletions(-)
rename packages/core/saved-objects/{core-saved-objects-utils-server => core-saved-objects-server}/src/saved_objects_error_helpers.test.ts (100%)
create mode 100644 packages/core/saved-objects/core-saved-objects-server/src/saved_objects_error_helpers.ts
delete mode 100644 packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_error_helpers.ts
diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc
index e4fd9111b216a..61ecb7aee11bb 100644
--- a/docs/user/security/audit-logging.asciidoc
+++ b/docs/user/security/audit-logging.asciidoc
@@ -123,18 +123,18 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is updating a saved object.
| `failure` | User is not authorized to update a saved object.
-.2+| `saved_object_add_to_spaces`
-| `unknown` | User is adding a saved object to other spaces.
-| `failure` | User is not authorized to add a saved object to other spaces.
-
-.2+| `saved_object_delete_from_spaces`
-| `unknown` | User is removing a saved object from other spaces.
-| `failure` | User is not authorized to remove a saved object from other spaces.
+.2+| `saved_object_update_objects_spaces`
+| `unknown` | User is adding and/or removing a saved object to/from other spaces.
+| `failure` | User is not authorized to add or remove a saved object to or from other spaces.
.2+| `saved_object_remove_references`
| `unknown` | User is removing references to a saved object.
| `failure` | User is not authorized to remove references to a saved object.
+.2+| `saved_object_collect_multinamespace_references`
+| `success` | User has accessed references to a multi-space saved object.
+| `failure` | User is not authorized to access references to a multi-space saved object.
+
.2+| `connector_update`
| `unknown` | User is updating a connector.
| `failure` | User is not authorized to update a connector.
diff --git a/packages/core/saved-objects/core-saved-objects-api-browser/src/saved_objects_client.ts b/packages/core/saved-objects/core-saved-objects-api-browser/src/saved_objects_client.ts
index d92b39f6c15a6..9a3d5f1e23317 100644
--- a/packages/core/saved-objects/core-saved-objects-api-browser/src/saved_objects_client.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-browser/src/saved_objects_client.ts
@@ -22,7 +22,6 @@ import type {
SavedObjectsBulkDeleteResponse,
SavedObjectsBulkDeleteOptions,
} from './apis';
-
import type { SimpleSavedObject } from './simple_saved_object';
/**
diff --git a/packages/core/saved-objects/core-saved-objects-api-browser/tsconfig.json b/packages/core/saved-objects/core-saved-objects-api-browser/tsconfig.json
index 271f341183fce..5d64239e4a8f3 100644
--- a/packages/core/saved-objects/core-saved-objects-api-browser/tsconfig.json
+++ b/packages/core/saved-objects/core-saved-objects-api-browser/tsconfig.json
@@ -12,7 +12,7 @@
],
"kbn_references": [
"@kbn/core-saved-objects-common",
- "@kbn/core-saved-objects-api-server"
+ "@kbn/core-saved-objects-api-server",
],
"exclude": [
"target/**/*",
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts
index c9529a6828d7a..b4e788dd2973a 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts
@@ -17,11 +17,6 @@ import type {
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsCollectMultiNamespaceReferencesOptions,
} from '@kbn/core-saved-objects-api-server';
-import {
- setMapsAreEqual,
- SavedObjectsErrorHelpers,
- setsAreEqual,
-} from '@kbn/core-saved-objects-utils-server';
import { SavedObjectsSerializer } from '@kbn/core-saved-objects-base-server-internal';
import { typeRegistryMock } from '@kbn/core-saved-objects-base-server-mocks';
import {
@@ -30,16 +25,16 @@ import {
} from './collect_multi_namespace_references';
import { collectMultiNamespaceReferences } from './collect_multi_namespace_references';
import type { CreatePointInTimeFinderFn } from './point_in_time_finder';
-import { AuditAction, type ISavedObjectsSecurityExtension } from '@kbn/core-saved-objects-server';
-
import {
- authMap,
enforceError,
- setupPerformAuthFullyAuthorized,
- setupPerformAuthEnforceFailure,
- setupRedactPassthrough,
+ setupAuthorizeAndRedactMultiNamespaceReferenencesFailure,
+ setupAuthorizeAndRedactMultiNamespaceReferenencesSuccess,
} from '../test_helpers/repository.test.common';
import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock';
+import {
+ type ISavedObjectsSecurityExtension,
+ SavedObjectsErrorHelpers,
+} from '@kbn/core-saved-objects-server';
const SPACES = ['default', 'another-space'];
const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 };
@@ -474,8 +469,38 @@ describe('collectMultiNamespaceReferences', () => {
const obj1LegacySpaces = ['space-1', 'space-2', 'space-3', 'space-4'];
let params: CollectMultiNamespaceReferencesParams;
+ const expectedObjects = [
+ {
+ id: 'id-1',
+ inboundReferences: [],
+ originId: undefined,
+ spaces: ['default', 'another-space'],
+ spacesWithMatchingAliases: ['space-1', 'space-2', 'space-3', 'space-4'],
+ spacesWithMatchingOrigins: undefined,
+ type: 'type-a',
+ },
+ {
+ id: 'id-2',
+ inboundReferences: [],
+ originId: undefined,
+ spaces: ['default', 'another-space'],
+ spacesWithMatchingAliases: undefined,
+ spacesWithMatchingOrigins: undefined,
+ type: 'type-a',
+ },
+ {
+ id: 'id-3',
+ inboundReferences: [{ id: 'id-1', name: 'ref-name', type: 'type-a' }],
+ originId: undefined,
+ spaces: ['default', 'another-space'],
+ spacesWithMatchingAliases: undefined,
+ spacesWithMatchingOrigins: undefined,
+ type: 'type-a',
+ },
+ ];
+
beforeEach(() => {
- params = setup([obj1, obj2], {}, mockSecurityExt);
+ params = setup(objects, {}, mockSecurityExt);
mockMgetResults({ found: true, references: [obj3] }, { found: true, references: [] }); // results for obj1 and obj2
mockMgetResults({ found: true, references: [] }); // results for obj3
mockFindLegacyUrlAliases.mockResolvedValue(
@@ -487,71 +512,46 @@ describe('collectMultiNamespaceReferences', () => {
});
afterEach(() => {
- mockSecurityExt.performAuthorization.mockReset();
- mockSecurityExt.enforceAuthorization.mockReset();
- mockSecurityExt.redactNamespaces.mockReset();
- mockSecurityExt.addAuditEvent.mockReset();
+ mockSecurityExt.authorizeAndRedactMultiNamespaceReferences.mockReset();
});
describe(`errors`, () => {
test(`propagates decorated error when not authorized`, async () => {
// Unlike other functions, it doesn't validate the level of authorization first, so we need to
- // carry on and mock the enforce function as well to create an unauthorized condition
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ // carry on and mock the security function to create an unauthorized condition
+ setupAuthorizeAndRedactMultiNamespaceReferenencesFailure(mockSecurityExt);
await expect(collectMultiNamespaceReferences(params)).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- });
-
- test(`adds audit event per object when not successful`, async () => {
- // Unlike other functions, it doesn't validate the level of authorization first, so we need to
- // carry on and mock the enforce function as well to create an unauthorized condition
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
- await expect(collectMultiNamespaceReferences(params)).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.COLLECT_MULTINAMESPACE_REFERENCES,
- savedObject: { type: obj.type, id: obj.id },
- error: enforceError,
- });
- });
+ expect(mockSecurityExt.authorizeAndRedactMultiNamespaceReferences).toHaveBeenCalledTimes(1);
});
});
- describe('checks privileges', () => {
+ describe('calls authorizeAndRedactMultiNamespaceReferences of the security extension', () => {
beforeEach(() => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ setupAuthorizeAndRedactMultiNamespaceReferenencesFailure(mockSecurityExt);
});
- test(`in the default state`, async () => {
- await expect(collectMultiNamespaceReferences(params)).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedSpaces = new Set(['default', ...SPACES, ...obj1LegacySpaces]);
- const expectedEnforceMap = new Map([[objects[0].type, new Set(['default'])]]);
+ test(`in the default space`, async () => {
+ await expect(collectMultiNamespaceReferences(params)).rejects.toThrow(enforceError);
+ expect(mockSecurityExt.authorizeAndRedactMultiNamespaceReferences).toHaveBeenCalledTimes(1);
- const { spaces: actualSpaces, enforceMap: actualEnforceMap } =
- mockSecurityExt.performAuthorization.mock.calls[0][0];
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
+ const { namespace: actualNamespace, objects: actualObjects } =
+ mockSecurityExt.authorizeAndRedactMultiNamespaceReferences.mock.calls[0][0];
+ expect(actualNamespace).toEqual('default');
+ expect(actualObjects).toEqual(expectedObjects);
});
- test(`in a non-default state`, async () => {
+ test(`in a non-default space`, async () => {
const namespace = 'space-X';
await expect(
collectMultiNamespaceReferences({ ...params, options: { namespace } })
).rejects.toThrow(enforceError);
+ expect(mockSecurityExt.authorizeAndRedactMultiNamespaceReferences).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedSpaces = new Set([namespace, ...SPACES, ...obj1LegacySpaces]);
- const expectedEnforceMap = new Map([[objects[0].type, new Set([namespace])]]);
- const { spaces: actualSpaces, enforceMap: actualEnforceMap } =
- mockSecurityExt.performAuthorization.mock.calls[0][0];
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
+ const { namespace: actualNamespace, objects: actualObjects } =
+ mockSecurityExt.authorizeAndRedactMultiNamespaceReferences.mock.calls[0][0];
+ expect(actualNamespace).toEqual(namespace);
+ expect(actualObjects).toEqual(expectedObjects);
});
test(`with purpose 'collectMultiNamespaceReferences'`, async () => {
@@ -559,15 +559,13 @@ describe('collectMultiNamespaceReferences', () => {
purpose: 'collectMultiNamespaceReferences',
};
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
await expect(collectMultiNamespaceReferences({ ...params, options })).rejects.toThrow(
enforceError
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.performAuthorization).toBeCalledWith(
+ expect(mockSecurityExt.authorizeAndRedactMultiNamespaceReferences).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeAndRedactMultiNamespaceReferences).toBeCalledWith(
expect.objectContaining({
- actions: new Set(['bulk_get']),
+ options: { purpose: 'collectMultiNamespaceReferences' },
})
);
});
@@ -577,15 +575,13 @@ describe('collectMultiNamespaceReferences', () => {
purpose: 'updateObjectsSpaces',
};
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
await expect(collectMultiNamespaceReferences({ ...params, options })).rejects.toThrow(
enforceError
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.performAuthorization).toBeCalledWith(
+ expect(mockSecurityExt.authorizeAndRedactMultiNamespaceReferences).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeAndRedactMultiNamespaceReferences).toBeCalledWith(
expect.objectContaining({
- actions: new Set(['share_to_space']),
+ options: { purpose: 'updateObjectsSpaces' },
})
);
});
@@ -593,59 +589,21 @@ describe('collectMultiNamespaceReferences', () => {
describe('success', () => {
beforeEach(async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
- setupRedactPassthrough(mockSecurityExt);
- await collectMultiNamespaceReferences(params);
+ setupAuthorizeAndRedactMultiNamespaceReferenencesSuccess(mockSecurityExt);
});
- test(`calls redactNamespaces with type, spaces, and authorization map`, async () => {
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedSpaces = new Set(['default', ...SPACES, ...obj1LegacySpaces]);
- const { spaces: actualSpaces } = mockSecurityExt.performAuthorization.mock.calls[0][0];
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
-
- const resultObjects = [obj1, obj2, obj3];
-
- // enforce is called once for all objects/spaces, then once per object
- expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(resultObjects.length);
- const expectedTypesAndSpaces = new Map([[objects[0].type, new Set(['default'])]]);
- const { typesAndSpaces: actualTypesAndSpaces } =
- mockSecurityExt.enforceAuthorization.mock.calls[0][0];
- expect(setMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy();
-
- // Redact is called once per object, but an additional time for object 1 because it has legacy URL aliases in another set of spaces
- expect(mockSecurityExt.redactNamespaces).toBeCalledTimes(resultObjects.length + 1);
- const expectedRedactParams = [
- { type: obj1.type, spaces: SPACES },
- { type: obj1.type, spaces: obj1LegacySpaces },
- { type: obj2.type, spaces: SPACES },
- { type: obj3.type, spaces: SPACES },
- ];
-
- expectedRedactParams.forEach((expected, i) => {
- const { savedObject, typeMap } = mockSecurityExt.redactNamespaces.mock.calls[i][0];
- expect(savedObject).toEqual(
- expect.objectContaining({
- type: expected.type,
- namespaces: expected.spaces,
- })
- );
- expect(typeMap).toBe(authMap);
- });
+ // Note: this test doesn't seem particularly useful as it only verifies the mock passthrough, but
+ // I am not sure what else can be done at this level now that the extension handles everything
+ test(`returns a result when successful`, async () => {
+ const result = await collectMultiNamespaceReferences(params);
+ expect(mockSecurityExt.authorizeAndRedactMultiNamespaceReferences).toHaveBeenCalledTimes(1);
+ expect(result.objects).toEqual(expectedObjects);
});
+ test(`returns empty array when no objects are provided`, async () => {
+ setupAuthorizeAndRedactMultiNamespaceReferenencesSuccess(mockSecurityExt);
- test(`adds audit event per object when successful`, async () => {
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
-
- const resultObjects = [obj1, obj2, obj3];
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(resultObjects.length);
- resultObjects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.COLLECT_MULTINAMESPACE_REFERENCES,
- savedObject: { type: obj.type, id: obj.id },
- error: undefined,
- });
- });
+ const result = await collectMultiNamespaceReferences({ ...params, objects: [] });
+ expect(result).toEqual({ objects: [] });
+ expect(mockSecurityExt.authorizeAndRedactMultiNamespaceReferences).not.toHaveBeenCalled();
});
});
});
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts
index 183d2509ad48d..d8b25dc886f20 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts
@@ -7,7 +7,6 @@
*/
import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal';
-import type { SavedObject } from '@kbn/core-saved-objects-server';
import type {
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsCollectMultiNamespaceReferencesOptions,
@@ -15,11 +14,12 @@ import type {
SavedObjectReferenceWithContext,
} from '@kbn/core-saved-objects-api-server';
import {
- AuditAction,
type ISavedObjectsSecurityExtension,
type ISavedObjectTypeRegistry,
+ type SavedObject,
+ SavedObjectsErrorHelpers,
} from '@kbn/core-saved-objects-server';
-import { SavedObjectsErrorHelpers, SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
+import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import {
type SavedObjectsSerializer,
getObjectKey,
@@ -72,7 +72,7 @@ export interface CollectMultiNamespaceReferencesParams {
export async function collectMultiNamespaceReferences(
params: CollectMultiNamespaceReferencesParams
): Promise {
- const { createPointInTimeFinder, objects } = params;
+ const { createPointInTimeFinder, objects, securityExtension, options } = params;
if (!objects.length) {
return { objects: [] };
}
@@ -123,10 +123,16 @@ export async function collectMultiNamespaceReferences(
return { ...obj, spacesWithMatchingAliases, spacesWithMatchingOrigins };
});
+ if (!securityExtension) return { objects: results };
+
// Now that we have *all* information for the object graph, if the Security extension is enabled, we can: check/enforce authorization,
// write audit events, filter the object graph, and redact spaces from the objects.
- const filteredAndRedactedResults = await optionallyUseSecurity(results, params);
-
+ const filteredAndRedactedResults =
+ await securityExtension.authorizeAndRedactMultiNamespaceReferences({
+ namespace: SavedObjectsUtils.namespaceIdToString(options?.namespace),
+ objects: results,
+ options: { purpose: options?.purpose },
+ });
return {
objects: filteredAndRedactedResults,
};
@@ -220,196 +226,3 @@ async function getObjectsAndReferences({
return { objectMap, inboundReferencesMap };
}
-
-/**
- * Checks/enforces authorization, writes audit events, filters the object graph, and redacts spaces from the share_to_space/bulk_get
- * response. In other SavedObjectsRepository functions we do this before decrypting attributes. However, because of the
- * share_to_space/bulk_get response logic involved in deciding between the exact match or alias match, it's cleaner to do authorization,
- * auditing, filtering, and redaction all afterwards.
- */
-async function optionallyUseSecurity(
- objectsWithContext: SavedObjectReferenceWithContext[],
- params: CollectMultiNamespaceReferencesParams
-) {
- const { securityExtension, objects, options = {} } = params;
- const { purpose, namespace } = options;
- const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
- if (!securityExtension) {
- return objectsWithContext;
- }
-
- // Check authorization based on all *found* object types / spaces
- const typesToAuthorize = new Set();
- const spacesToAuthorize = new Set([namespaceString]);
- const addSpacesToAuthorize = (spaces: string[] = []) => {
- for (const space of spaces) spacesToAuthorize.add(space);
- };
- for (const obj of objectsWithContext) {
- typesToAuthorize.add(obj.type);
- addSpacesToAuthorize(obj.spaces);
- addSpacesToAuthorize(obj.spacesWithMatchingAliases);
- addSpacesToAuthorize(obj.spacesWithMatchingOrigins);
- }
- const action =
- purpose === 'updateObjectsSpaces' ? ('share_to_space' as const) : ('bulk_get' as const);
-
- // Enforce authorization based on all *requested* object types and the current space
- const typesAndSpaces = objects.reduce(
- (acc, { type }) => (acc.has(type) ? acc : acc.set(type, new Set([namespaceString]))), // Always enforce authZ for the active space
- new Map>()
- );
-
- const { typeMap } = await securityExtension?.performAuthorization({
- actions: new Set([action]),
- types: typesToAuthorize,
- spaces: spacesToAuthorize,
- enforceMap: typesAndSpaces,
- auditCallback: (error) => {
- if (!error) return; // We will audit success results below, after redaction
- for (const { type, id } of objects) {
- securityExtension!.addAuditEvent({
- action: AuditAction.COLLECT_MULTINAMESPACE_REFERENCES,
- savedObject: { type, id },
- error,
- });
- }
- },
- });
-
- // Now, filter/redact the results. Most SOR functions just redact the `namespaces` field from each returned object. However, this function
- // will actually filter the returned object graph itself.
- // This is done in two steps: (1) objects which the user can't access *in this space* are filtered from the graph, and the
- // graph is rearranged to avoid leaking information. (2) any spaces that the user can't access are redacted from each individual object.
- // After we finish filtering, we can write audit events for each object that is going to be returned to the user.
- const requestedObjectsSet = objects.reduce(
- (acc, { type, id }) => acc.add(`${type}:${id}`),
- new Set()
- );
- const retrievedObjectsSet = objectsWithContext.reduce(
- (acc, { type, id }) => acc.add(`${type}:${id}`),
- new Set()
- );
- const traversedObjects = new Set();
- const filteredObjectsMap = new Map();
- const getIsAuthorizedForInboundReference = (inbound: { type: string; id: string }) => {
- const found = filteredObjectsMap.get(`${inbound.type}:${inbound.id}`);
- return found && !found.isMissing; // If true, this object can be linked back to one of the requested objects
- };
- let objectsToProcess = [...objectsWithContext];
- while (objectsToProcess.length > 0) {
- const obj = objectsToProcess.shift()!;
- const { type, id, spaces, inboundReferences } = obj;
- const objKey = `${type}:${id}`;
- traversedObjects.add(objKey);
- // Is the user authorized to access this object in this space?
- let isAuthorizedForObject = true;
- try {
- // ToDo: this is the only remaining call to enforceAuthorization outside of the security extension
- // This was a bit complicated to change now, but can ultimately be removed when authz logic is
- // migrated from the repo level to the extension level.
- securityExtension.enforceAuthorization({
- typesAndSpaces: new Map([[type, new Set([namespaceString])]]),
- action,
- typeMap,
- });
- } catch (err) {
- isAuthorizedForObject = false;
- }
- // Redact the inbound references so we don't leak any info about other objects that the user is not authorized to access
- const redactedInboundReferences = inboundReferences.filter((inbound) => {
- if (inbound.type === type && inbound.id === id) {
- // circular reference, don't redact it
- return true;
- }
- return getIsAuthorizedForInboundReference(inbound);
- });
- // If the user is not authorized to access at least one inbound reference of this object, then we should omit this object.
- const isAuthorizedForGraph =
- requestedObjectsSet.has(objKey) || // If true, this is one of the requested objects, and we checked authorization above
- redactedInboundReferences.some(getIsAuthorizedForInboundReference);
-
- if (isAuthorizedForObject && isAuthorizedForGraph) {
- if (spaces.length) {
- // Only generate success audit records for "non-empty results" with 1+ spaces
- // ("empty result" means the object was a non-multi-namespace type, or hidden type, or not found)
- securityExtension.addAuditEvent({
- action: AuditAction.COLLECT_MULTINAMESPACE_REFERENCES,
- savedObject: { type, id },
- });
- }
- filteredObjectsMap.set(objKey, obj);
- } else if (!isAuthorizedForObject && isAuthorizedForGraph) {
- filteredObjectsMap.set(objKey, { ...obj, spaces: [], isMissing: true });
- } else if (isAuthorizedForObject && !isAuthorizedForGraph) {
- const hasUntraversedInboundReferences = inboundReferences.some(
- (ref) =>
- !traversedObjects.has(`${ref.type}:${ref.id}`) &&
- retrievedObjectsSet.has(`${ref.type}:${ref.id}`)
- );
-
- if (hasUntraversedInboundReferences) {
- // this object has inbound reference(s) that we haven't traversed yet; bump it to the back of the list
- objectsToProcess = [...objectsToProcess, obj];
- } else {
- // There should never be a missing inbound reference.
- // If there is, then something has gone terribly wrong.
- const missingInboundReference = inboundReferences.find(
- (ref) =>
- !traversedObjects.has(`${ref.type}:${ref.id}`) &&
- !retrievedObjectsSet.has(`${ref.type}:${ref.id}`)
- );
-
- if (missingInboundReference) {
- throw new Error(
- `Unexpected inbound reference to "${missingInboundReference.type}:${missingInboundReference.id}"`
- );
- }
- }
- }
- }
-
- const filteredAndRedactedObjects = [
- ...filteredObjectsMap.values(),
- ].map((obj) => {
- const {
- type,
- id,
- spaces,
- spacesWithMatchingAliases,
- spacesWithMatchingOrigins,
- inboundReferences,
- } = obj;
- // Redact the inbound references so we don't leak any info about other objects that the user is not authorized to access
- const redactedInboundReferences = inboundReferences.filter((inbound) => {
- if (inbound.type === type && inbound.id === id) {
- // circular reference, don't redact it
- return true;
- }
- return getIsAuthorizedForInboundReference(inbound);
- });
-
- /** Simple wrapper for the `redactNamespaces` function that expects a saved object in its params. */
- const getRedactedSpaces = (spacesArray: string[] | undefined) => {
- if (!spacesArray) return;
- const savedObject = { type, namespaces: spacesArray } as SavedObject; // Other SavedObject attributes aren't required
- const result = securityExtension.redactNamespaces({ savedObject, typeMap });
- return result.namespaces;
- };
- const redactedSpaces = getRedactedSpaces(spaces)!;
- const redactedSpacesWithMatchingAliases = getRedactedSpaces(spacesWithMatchingAliases);
- const redactedSpacesWithMatchingOrigins = getRedactedSpaces(spacesWithMatchingOrigins);
- return {
- ...obj,
- spaces: redactedSpaces,
- ...(redactedSpacesWithMatchingAliases && {
- spacesWithMatchingAliases: redactedSpacesWithMatchingAliases,
- }),
- ...(redactedSpacesWithMatchingOrigins && {
- spacesWithMatchingOrigins: redactedSpacesWithMatchingOrigins,
- }),
- inboundReferences: redactedInboundReferences,
- };
- });
-
- return filteredAndRedactedObjects;
-}
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/decorate_es_error.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/decorate_es_error.test.ts
index 8290f7345190d..6516fe39f016e 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/decorate_es_error.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/decorate_es_error.test.ts
@@ -8,7 +8,7 @@
import { errors as esErrors } from '@elastic/elasticsearch';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
-import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-utils-server';
+import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { decorateEsError } from './decorate_es_error';
describe('savedObjectsClient/decorateEsError', () => {
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/decorate_es_error.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/decorate_es_error.ts
index 9cfdffc13a5dd..cc1d18fc7aebd 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/decorate_es_error.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/decorate_es_error.ts
@@ -10,7 +10,7 @@ import { get } from 'lodash';
import { errors as esErrors } from '@elastic/elasticsearch';
import type { ElasticsearchErrorDetails } from '@kbn/es-errors';
import { isSupportedEsServer } from '@kbn/core-elasticsearch-server-internal';
-import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-utils-server';
+import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
const responseErrors = {
isServiceUnavailable: (statusCode?: number) => statusCode === 503,
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/filter_utils.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/filter_utils.ts
index ae7bb08039850..1cf27251a1549 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/filter_utils.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/filter_utils.ts
@@ -9,7 +9,7 @@
import { set } from '@kbn/safer-lodash-set';
import { get, cloneDeep } from 'lodash';
import * as esKuery from '@kbn/es-query';
-import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-utils-server';
+import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import type { IndexMapping } from '@kbn/core-saved-objects-base-server-internal';
type KueryNode = any;
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts
index cd888ddb4c2e4..232c19fa7a840 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts
@@ -13,17 +13,11 @@ import {
} from './internal_bulk_resolve.test.mock';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
-import type { SavedObject } from '@kbn/core-saved-objects-server';
import type {
SavedObjectsBulkResolveObject,
SavedObjectsBaseOptions,
} from '@kbn/core-saved-objects-api-server';
-import {
- setMapsAreEqual,
- SavedObjectsErrorHelpers,
- SavedObjectsUtils,
- setsAreEqual,
-} from '@kbn/core-saved-objects-utils-server';
+import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import {
SavedObjectsSerializer,
LEGACY_URL_ALIAS_TYPE,
@@ -32,17 +26,16 @@ import { typeRegistryMock } from '@kbn/core-saved-objects-base-server-mocks';
import { internalBulkResolve, type InternalBulkResolveParams } from './internal_bulk_resolve';
import { normalizeNamespace } from './internal_utils';
import {
- AuditAction,
type ISavedObjectsEncryptionExtension,
type ISavedObjectsSecurityExtension,
type ISavedObjectTypeRegistry,
+ type SavedObject,
+ SavedObjectsErrorHelpers,
} from '@kbn/core-saved-objects-server';
import {
- authMap,
enforceError,
- setupPerformAuthFullyAuthorized,
- setupPerformAuthEnforceFailure,
- setupRedactPassthrough,
+ setupAuthorizeAndRedactInternalBulkResolveFailure,
+ setupAuthorizeAndRedactInternalBulkResolveSuccess,
} from '../test_helpers/repository.test.common';
import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock';
@@ -437,6 +430,17 @@ describe('internalBulkResolve', () => {
let mockSecurityExt: jest.Mocked;
let params: InternalBulkResolveParams;
+ const expectedObjects = [
+ expect.objectContaining({
+ outcome: 'exactMatch',
+ saved_object: expect.objectContaining({ id: objects[0].id }),
+ }),
+ expect.objectContaining({
+ outcome: 'exactMatch',
+ saved_object: expect.objectContaining({ id: objects[1].id }),
+ }),
+ ];
+
beforeEach(() => {
mockGetSavedObjectFromSource.mockReset();
mockGetSavedObjectFromSource.mockImplementation((_registry, type, id) => {
@@ -465,110 +469,58 @@ describe('internalBulkResolve', () => {
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
+ setupAuthorizeAndRedactInternalBulkResolveFailure(mockSecurityExt);
await expect(internalBulkResolve(params)).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeAndRedactInternalBulkResolve).toHaveBeenCalledTimes(1);
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
- setupRedactPassthrough(mockSecurityExt);
+ test(`returns result when successful`, async () => {
+ setupAuthorizeAndRedactInternalBulkResolveSuccess(mockSecurityExt);
const result = await internalBulkResolve(params);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeAndRedactInternalBulkResolve).toHaveBeenCalledTimes(1);
const bulkIds = objects.map((obj) => obj.id);
const expectedNamespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
expectBulkArgs(expectedNamespaceString, bulkIds);
const mgetIds = bulkIds;
expectMgetArgs(namespace, mgetIds);
- expect(result.resolved_objects).toEqual([
- expect.objectContaining({
- outcome: 'exactMatch',
- saved_object: expect.objectContaining({ id: objects[0].id }),
- }),
- expect.objectContaining({
- outcome: 'exactMatch',
- saved_object: expect.objectContaining({ id: objects[1].id }),
- }),
- ]);
+ expect(result.resolved_objects).toEqual(expectedObjects);
});
- test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns empty array when no objects are provided`, async () => {
+ setupAuthorizeAndRedactInternalBulkResolveSuccess(mockSecurityExt);
- await internalBulkResolve(params);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['bulk_get']);
- const expectedSpaces = new Set([namespace]);
- const expectedTypes = new Set([objects[0].type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(objects[0].type, new Set([namespace]));
-
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
-
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toBeUndefined();
+ const result = await internalBulkResolve({ ...params, objects: [] });
+ expect(result).toEqual({ resolved_objects: [] });
+ expect(mockSecurityExt.authorizeAndRedactInternalBulkResolve).not.toHaveBeenCalled();
});
- test(`calls redactNamespaces with authorization map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
- setupRedactPassthrough(mockSecurityExt);
-
- await internalBulkResolve(params);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
-
- expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj, i) => {
- const { savedObject, typeMap } = mockSecurityExt.redactNamespaces.mock.calls[i][0];
- expect(savedObject).toEqual(
- expect.objectContaining({
- type: obj.type,
- id: obj.id,
- namespaces: [namespace],
- })
- );
- expect(typeMap).toBe(authMap);
+ describe('calls authorizeAndRedactInternalBulkResolve of the security extension', () => {
+ beforeEach(() => {
+ setupAuthorizeAndRedactInternalBulkResolveFailure(mockSecurityExt);
});
- });
- test(`adds audit event per object when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`in the default space`, async () => {
+ await expect(
+ internalBulkResolve({ ...params, options: { namespace: 'default' } })
+ ).rejects.toThrow(enforceError);
+ expect(mockSecurityExt.authorizeAndRedactInternalBulkResolve).toHaveBeenCalledTimes(1);
- await internalBulkResolve(params);
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.RESOLVE,
- savedObject: { type: obj.type, id: obj.id },
- error: undefined,
- });
+ const { namespace: actualNamespace, objects: actualObjects } =
+ mockSecurityExt.authorizeAndRedactInternalBulkResolve.mock.calls[0][0];
+ expect(actualNamespace).toBeUndefined();
+ expect(actualObjects).toEqual(expectedObjects);
});
- });
- test(`adds audit event per object when not successful`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
- await expect(internalBulkResolve(params)).rejects.toThrow(enforceError);
+ test(`in a non-default space`, async () => {
+ await expect(internalBulkResolve(params)).rejects.toThrow(enforceError);
+ expect(mockSecurityExt.authorizeAndRedactInternalBulkResolve).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.RESOLVE,
- savedObject: { type: obj.type, id: obj.id },
- error: enforceError,
- });
+ const { namespace: actualNamespace, objects: actualObjects } =
+ mockSecurityExt.authorizeAndRedactInternalBulkResolve.mock.calls[0][0];
+ expect(actualNamespace).toEqual(namespace);
+ expect(actualObjects).toEqual(expectedObjects);
});
});
});
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts
index 2d4e75654b1fe..3b2e934854a52 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts
@@ -9,7 +9,6 @@
import type { MgetResponseItem } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal';
-import type { SavedObject } from '@kbn/core-saved-objects-server';
import type {
SavedObjectsBaseOptions,
SavedObjectsBulkResolveObject,
@@ -18,17 +17,14 @@ import type {
SavedObjectsIncrementCounterOptions,
} from '@kbn/core-saved-objects-api-server';
import {
- AuditAction,
type ISavedObjectsEncryptionExtension,
type ISavedObjectsSecurityExtension,
type ISavedObjectTypeRegistry,
type SavedObjectsRawDocSource,
-} from '@kbn/core-saved-objects-server';
-import {
+ type SavedObject,
+ type BulkResolveError,
SavedObjectsErrorHelpers,
- SavedObjectsUtils,
- type DecoratedError,
-} from '@kbn/core-saved-objects-utils-server';
+} from '@kbn/core-saved-objects-server';
import {
LEGACY_URL_ALIAS_TYPE,
type LegacyUrlAlias,
@@ -83,25 +79,14 @@ export interface InternalBulkResolveParams {
* @public
*/
export interface InternalSavedObjectsBulkResolveResponse {
- resolved_objects: Array | InternalBulkResolveError>;
-}
-
-/**
- * Error result for the internal bulkResolve function.
- *
- * @internal
- */
-export interface InternalBulkResolveError {
- type: string;
- id: string;
- error: DecoratedError;
+ resolved_objects: Array | BulkResolveError>;
}
/** Type guard used in the repository. */
export function isBulkResolveError(
- result: SavedObjectsResolveResponse | InternalBulkResolveError
-): result is InternalBulkResolveError {
- return !!(result as InternalBulkResolveError).error;
+ result: SavedObjectsResolveResponse | BulkResolveError
+): result is BulkResolveError {
+ return !!(result as BulkResolveError).error;
}
type AliasInfo = Pick;
@@ -201,9 +186,7 @@ export async function internalBulkResolve(
}
// map function for pMap below
- const mapper = async (
- either: Either
- ) => {
+ const mapper = async (either: Either) => {
if (isLeft(either)) {
return either.value;
}
@@ -271,92 +254,18 @@ export async function internalBulkResolve(
{ refresh: false }
).catch(() => {}); // if the call fails for some reason, intentionally swallow the error
- const redacted = await authorizeAuditAndRedact(resolvedObjects, securityExtension, namespace);
- return { resolved_objects: redacted };
-}
-
-/**
- * Checks authorization, writes audit events, and redacts namespaces from the bulkResolve response. In other SavedObjectsRepository
- * functions we do this before decrypting attributes. However, because of the bulkResolve logic involved in deciding between the exact match
- * or alias match, it's cleaner to do authorization, auditing, and redaction all afterwards.
- */
-async function authorizeAuditAndRedact(
- resolvedObjects: Array | InternalBulkResolveError>,
- securityExtension: ISavedObjectsSecurityExtension | undefined,
- namespace: string | undefined
-) {
- if (!securityExtension) {
- return resolvedObjects;
- }
-
- const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
- const typesAndSpaces = new Map>();
- const spacesToAuthorize = new Set();
- const auditableObjects: Array<{ type: string; id: string }> = [];
+ if (!securityExtension) return { resolved_objects: resolvedObjects };
- for (const result of resolvedObjects) {
- let auditableObject: { type: string; id: string } | undefined;
- if (isBulkResolveError(result)) {
- const { type, id, error } = result;
- if (!SavedObjectsErrorHelpers.isBadRequestError(error)) {
- // Only "not found" errors should show up as audit events (not "unsupported type" errors)
- auditableObject = { type, id };
- }
- } else {
- const { type, id, namespaces = [] } = result.saved_object;
- auditableObject = { type, id };
- for (const space of namespaces) {
- spacesToAuthorize.add(space);
- }
- }
- if (auditableObject) {
- auditableObjects.push(auditableObject);
- const spacesToEnforce =
- typesAndSpaces.get(auditableObject.type) ?? new Set([namespaceString]); // Always enforce authZ for the active space
- spacesToEnforce.add(namespaceString);
- typesAndSpaces.set(auditableObject.type, spacesToEnforce);
- spacesToAuthorize.add(namespaceString);
- }
- }
-
- if (typesAndSpaces.size === 0) {
- // We only had "unsupported type" errors, there are no types to check privileges for, just return early
- return resolvedObjects;
- }
-
- const authorizationResult = await securityExtension?.performAuthorization({
- actions: new Set(['bulk_get']),
- types: new Set(typesAndSpaces.keys()),
- spaces: spacesToAuthorize,
- enforceMap: typesAndSpaces,
- auditCallback: (error) => {
- for (const { type, id } of auditableObjects) {
- securityExtension.addAuditEvent({
- action: AuditAction.RESOLVE,
- savedObject: { type, id },
- error,
- });
- }
- },
- });
-
- return resolvedObjects.map((result) => {
- if (isBulkResolveError(result)) {
- return result;
- }
- return {
- ...result,
- saved_object: securityExtension.redactNamespaces({
- typeMap: authorizationResult.typeMap,
- savedObject: result.saved_object,
- }),
- };
+ const redactedObjects = await securityExtension?.authorizeAndRedactInternalBulkResolve({
+ namespace,
+ objects: resolvedObjects,
});
+ return { resolved_objects: redactedObjects };
}
/** Separates valid and invalid object types */
function validateObjectTypes(objects: SavedObjectsBulkResolveObject[], allowedTypes: string[]) {
- return objects.map>((object) => {
+ return objects.map>((object) => {
const { type, id } = object;
if (!allowedTypes.includes(type)) {
return {
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts
index 37b29a6a94e66..7d1e8005b75ee 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts
@@ -7,17 +7,14 @@
*/
import type { Payload } from '@hapi/boom';
-import type { SavedObject } from '@kbn/core-saved-objects-server';
-import type {
- ISavedObjectTypeRegistry,
- SavedObjectsRawDoc,
- SavedObjectsRawDocSource,
-} from '@kbn/core-saved-objects-server';
import {
+ type ISavedObjectTypeRegistry,
+ type SavedObjectsRawDoc,
+ type SavedObjectsRawDocSource,
+ type SavedObject,
SavedObjectsErrorHelpers,
- SavedObjectsUtils,
- ALL_NAMESPACES_STRING,
-} from '@kbn/core-saved-objects-utils-server';
+} from '@kbn/core-saved-objects-server';
+import { SavedObjectsUtils, ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server';
import {
decodeRequestVersion,
encodeHitVersion,
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts
index 1daa7b5f37ea4..ae09b0e1e4228 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts
@@ -8,15 +8,13 @@
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal';
-import type {
- ISavedObjectTypeRegistry,
- SavedObjectsRawDoc,
- SavedObjectsRawDocSource,
-} from '@kbn/core-saved-objects-server';
import {
+ type ISavedObjectTypeRegistry,
+ type SavedObjectsRawDoc,
+ type SavedObjectsRawDocSource,
SavedObjectsErrorHelpers,
- ALL_NAMESPACES_STRING,
-} from '@kbn/core-saved-objects-utils-server';
+} from '@kbn/core-saved-objects-server';
+import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server';
import {
LEGACY_URL_ALIAS_TYPE,
getObjectKey,
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts
index bd30f9a10eeb1..71788876b4309 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts
@@ -20,18 +20,12 @@ import { estypes } from '@elastic/elasticsearch';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { SavedObjectsBulkUpdateObject } from '@kbn/core-saved-objects-api-server';
import { SavedObjectsSerializer } from '@kbn/core-saved-objects-base-server-internal';
-import { SavedObject } from '@kbn/core-saved-objects-server';
import {
ISavedObjectsSecurityExtension,
- AuditAction,
SavedObjectsRawDocSource,
AuthorizationTypeEntry,
+ SavedObject,
} from '@kbn/core-saved-objects-server';
-import {
- setMapsAreEqual,
- arrayMapsAreEqual,
- setsAreEqual,
-} from '@kbn/core-saved-objects-utils-server';
import { kibanaMigratorMock } from '../mocks';
import {
createRegistry,
@@ -48,7 +42,6 @@ import {
updateSuccess,
deleteSuccess,
removeReferencesToSuccess,
- REMOVE_REFS_COUNT,
checkConflictsSuccess,
findSuccess,
mockTimestampFields,
@@ -63,12 +56,14 @@ import {
expectUpdateResult,
bulkDeleteSuccess,
createBulkDeleteSuccessStatus,
- setupPerformAuthFullyAuthorized,
- setupPerformAuthPartiallyAuthorized,
- setupPerformAuthUnauthorized,
- setupPerformAuthEnforceFailure,
+ REMOVE_REFS_COUNT,
+ setupGetFindRedactTypeMap,
+ generateIndexPatternSearchResults,
+ setupAuthorizeFunc,
+ setupAuthorizeFind,
} from '../test_helpers/repository.test.common';
import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock';
+import { arrayMapsAreEqual } from '@kbn/core-saved-objects-utils-server';
// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository
// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient.
@@ -124,45 +119,56 @@ describe('SavedObjectsRepository Security Extension', () => {
mockSecurityExt = savedObjectsExtensionsMock.createSecurityExtension();
mockGetCurrentTime.mockReturnValue(mockTimestamp);
repository = instantiateRepository();
+ setupGetFindRedactTypeMap(mockSecurityExt);
});
afterEach(() => {
- mockSecurityExt.performAuthorization.mockClear();
mockSecurityExt.redactNamespaces.mockClear();
mockGetSearchDsl.mockClear();
});
describe('#get', () => {
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ test(`propagates decorated error when authorizeGet rejects promise`, async () => {
+ mockSecurityExt.authorizeGet.mockRejectedValueOnce(checkAuthError);
await expect(
getSuccess(client, repository, registry, type, id, { namespace })
).rejects.toThrow(checkAuthError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeGet).toHaveBeenCalledTimes(1);
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeGet, 'unauthorized');
await expect(
getSuccess(client, repository, registry, type, id, { namespace })
).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeGet).toHaveBeenCalledTimes(1);
+ });
+
+ test(`returns result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeGet, 'partially_authorized');
+ setupRedactPassthrough(mockSecurityExt);
+
+ const result = await getSuccess(client, repository, registry, type, id, { namespace });
+
+ expect(mockSecurityExt.authorizeGet).toHaveBeenCalledTimes(1);
+ expect(client.get).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(expect.objectContaining({ type, id, namespaces: [namespace] }));
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeGet, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
const result = await getSuccess(client, repository, registry, type, id, { namespace });
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeGet).toHaveBeenCalledTimes(1);
expect(client.get).toHaveBeenCalledTimes(1);
expect(result).toEqual(expect.objectContaining({ type, id, namespaces: [namespace] }));
});
- test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => {
+ test(`calls authorizeGet with correct parameters`, async () => {
await getSuccess(
client,
repository,
@@ -176,35 +182,25 @@ describe('SavedObjectsRepository Security Extension', () => {
multiNamespaceObjNamespaces // all of the object's namespaces from preflight check are added to the auth check call
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['get']);
- const expectedSpaces = new Set(multiNamespaceObjNamespaces);
- const expectedTypes = new Set([MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, new Set([namespace]));
-
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
-
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toBeUndefined();
+ expect(mockSecurityExt.authorizeGet).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeGet).toHaveBeenCalledWith({
+ namespace,
+ object: {
+ existingNamespaces: multiNamespaceObjNamespaces,
+ id,
+ type: MULTI_NAMESPACE_CUSTOM_INDEX_TYPE,
+ },
+ objectNotFound: false,
+ });
});
test(`calls redactNamespaces with authorization map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeGet, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
await getSuccess(client, repository, registry, type, id, { namespace });
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeGet).toHaveBeenCalledTimes(1);
expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(1);
expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledWith(
expect.objectContaining({
@@ -213,71 +209,57 @@ describe('SavedObjectsRepository Security Extension', () => {
})
);
});
-
- test(`adds audit event when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
- setupRedactPassthrough(mockSecurityExt);
-
- await getSuccess(client, repository, registry, type, id, { namespace });
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.GET,
- savedObject: { type, id },
- });
- });
-
- test(`adds audit event when not successful`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
- await expect(
- getSuccess(client, repository, registry, type, id, { namespace })
- ).rejects.toThrow(enforceError);
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.GET,
- savedObject: { type, id },
- error: enforceError,
- });
- });
});
describe('#update', () => {
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ test(`propagates decorated error when authorizeUpdate rejects promise`, async () => {
+ mockSecurityExt.authorizeUpdate.mockRejectedValueOnce(checkAuthError);
await expect(
updateSuccess(client, repository, registry, type, id, attributes, { namespace })
).rejects.toThrow(checkAuthError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeUpdate).toHaveBeenCalledTimes(1);
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
+ setupAuthorizeFunc(mockSecurityExt.authorizeUpdate, 'unauthorized');
await expect(
updateSuccess(client, repository, registry, type, id, attributes, { namespace })
).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeUpdate).toHaveBeenCalledTimes(1);
+ });
+
+ test(`returns result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeUpdate, 'partially_authorized');
+ setupRedactPassthrough(mockSecurityExt);
+
+ const result = await updateSuccess(client, repository, registry, type, id, attributes, {
+ namespace,
+ });
+
+ expect(mockSecurityExt.authorizeUpdate).toHaveBeenCalledTimes(1);
+ expect(client.update).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(
+ expect.objectContaining({ id, type, attributes, namespaces: [namespace] })
+ );
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeUpdate, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
const result = await updateSuccess(client, repository, registry, type, id, attributes, {
namespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeUpdate).toHaveBeenCalledTimes(1);
expect(client.update).toHaveBeenCalledTimes(1);
expect(result).toEqual(
expect.objectContaining({ id, type, attributes, namespaces: [namespace] })
);
});
- test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => {
+ test(`calls authorizeUpdate with correct parameters`, async () => {
await updateSuccess(
client,
repository,
@@ -292,35 +274,28 @@ describe('SavedObjectsRepository Security Extension', () => {
multiNamespaceObjNamespaces // all of the object's namespaces from preflight check are added to the auth check call
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['update']);
- const expectedSpaces = new Set(multiNamespaceObjNamespaces);
- const expectedTypes = new Set([MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, new Set([namespace]));
+ expect(mockSecurityExt.authorizeUpdate).toHaveBeenCalledTimes(1);
+ const expectedNamespace = namespace;
+ const expectedObject = {
+ type: 'multiNamespaceTypeCustomIndex',
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ existingNamespaces: multiNamespaceObjNamespaces,
+ };
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
+ const { namespace: actualNamespace, object: actualObject } =
+ mockSecurityExt.authorizeUpdate.mock.calls[0][0];
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toBeUndefined();
+ expect(actualNamespace).toEqual(expectedNamespace);
+ expect(actualObject).toEqual(expectedObject);
});
test(`calls redactNamespaces with authorization map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeUpdate, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
await updateSuccess(client, repository, registry, type, id, attributes, { namespace });
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeUpdate).toHaveBeenCalledTimes(1);
expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(1);
expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledWith(
expect.objectContaining({
@@ -329,65 +304,35 @@ describe('SavedObjectsRepository Security Extension', () => {
})
);
});
-
- test(`adds audit event when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
- setupRedactPassthrough(mockSecurityExt);
-
- await updateSuccess(client, repository, registry, type, id, attributes, { namespace });
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.UPDATE,
- savedObject: { type, id },
- error: undefined,
- outcome: 'unknown',
- });
- });
- test(`adds audit event when not successful`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
- await expect(
- updateSuccess(client, repository, registry, type, id, { namespace })
- ).rejects.toThrow(enforceError);
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.UPDATE,
- savedObject: { type, id },
- error: enforceError,
- });
- });
});
describe('#create', () => {
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ test(`propagates decorated error when authorizeCreate rejects promise`, async () => {
+ mockSecurityExt.authorizeCreate.mockRejectedValueOnce(checkAuthError);
await expect(repository.create(type, attributes, { namespace })).rejects.toThrow(
checkAuthError
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeCreate).toHaveBeenCalledTimes(1);
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
+ setupAuthorizeFunc(mockSecurityExt.authorizeCreate, 'unauthorized');
await expect(repository.create(type, attributes, { namespace })).rejects.toThrow(
enforceError
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeCreate).toHaveBeenCalledTimes(1);
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeCreate, 'partially_authorized');
setupRedactPassthrough(mockSecurityExt);
const result = await repository.create(type, attributes, {
namespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeCreate).toHaveBeenCalledTimes(1);
expect(client.create).toHaveBeenCalledTimes(1);
expect(result).toEqual(
expect.objectContaining({
@@ -399,71 +344,75 @@ describe('SavedObjectsRepository Security Extension', () => {
);
});
- test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => {
- await repository.create(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, attributes, {
+ test(`returns result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeCreate as jest.Mock, 'fully_authorized');
+ setupRedactPassthrough(mockSecurityExt);
+
+ const result = await repository.create(type, attributes, {
namespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['create']);
- const expectedSpaces = new Set([namespace]);
- const expectedTypes = new Set([MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, new Set([namespace]));
+ expect(mockSecurityExt.authorizeCreate).toHaveBeenCalledTimes(1);
+ expect(client.create).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(
+ expect.objectContaining({
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ type,
+ attributes,
+ namespaces: [namespace],
+ })
+ );
+ });
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
+ test(`calls authorizeCreate with correct parameters`, async () => {
+ await repository.create(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, attributes, {
+ namespace,
+ });
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true }));
+ expect(mockSecurityExt.authorizeCreate).toHaveBeenCalledTimes(1);
+ const expectedNamespace = namespace;
+ const expectedObject = {
+ type: 'multiNamespaceTypeCustomIndex',
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ initialNamespaces: undefined,
+ existingNamespaces: [],
+ };
+ const { namespace: actualNamespace, object: actualObject } =
+ mockSecurityExt.authorizeCreate.mock.calls[0][0];
+
+ expect(actualNamespace).toEqual(expectedNamespace);
+ expect(actualObject).toEqual(expectedObject);
});
- test(`calls performAuthorization with initial namespaces`, async () => {
+ test(`calls authorizeCreate with initial namespaces`, async () => {
await repository.create(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, attributes, {
namespace,
initialNamespaces: multiNamespaceObjNamespaces,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['create']);
- const expectedSpaces = new Set(multiNamespaceObjNamespaces);
- const expectedTypes = new Set([MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(
- MULTI_NAMESPACE_CUSTOM_INDEX_TYPE,
- new Set(multiNamespaceObjNamespaces)
- );
+ expect(mockSecurityExt.authorizeCreate).toHaveBeenCalledTimes(1);
+ const expectedNamespace = namespace;
+ const expectedObject = {
+ type: 'multiNamespaceTypeCustomIndex',
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ initialNamespaces: multiNamespaceObjNamespaces,
+ existingNamespaces: [],
+ };
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
+ const { namespace: actualNamespace, object: actualObject } =
+ mockSecurityExt.authorizeCreate.mock.calls[0][0];
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true }));
+ expect(actualNamespace).toEqual(expectedNamespace);
+ expect(actualObject).toEqual(expectedObject);
});
test(`calls redactNamespaces with authorization map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeCreate as jest.Mock, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
await repository.create(type, attributes, { namespace });
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeCreate).toHaveBeenCalledTimes(1);
expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(1);
expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledWith(
@@ -477,42 +426,6 @@ describe('SavedObjectsRepository Security Extension', () => {
})
);
});
-
- test(`adds audit event when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
- setupRedactPassthrough(mockSecurityExt);
-
- await repository.create(type, attributes, { namespace });
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.CREATE,
- savedObject: {
- type,
- id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
- },
- error: undefined,
- outcome: 'unknown',
- });
- });
-
- test(`adds audit event when not successful`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
- await expect(repository.create(type, attributes, { namespace })).rejects.toThrow(
- enforceError
- );
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.CREATE,
- savedObject: {
- type,
- id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
- },
- error: enforceError,
- });
- });
});
describe('#delete', () => {
@@ -524,178 +437,115 @@ describe('SavedObjectsRepository Security Extension', () => {
mockDeleteLegacyUrlAliases.mockClear();
});
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ test(`propagates decorated error when authorizeDelete rejects promise`, async () => {
+ mockSecurityExt.authorizeDelete.mockRejectedValueOnce(checkAuthError);
await expect(
deleteSuccess(client, repository, registry, type, id, { namespace })
).rejects.toThrow(checkAuthError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeDelete).toHaveBeenCalledTimes(1);
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeDelete, 'unauthorized');
await expect(
deleteSuccess(client, repository, registry, type, id, { namespace })
).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeDelete).toHaveBeenCalledTimes(1);
});
- test(`returns empty object result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns empty object result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeDelete, 'partially_authorized');
setupRedactPassthrough(mockSecurityExt);
const result = await deleteSuccess(client, repository, registry, type, id, {
namespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeDelete).toHaveBeenCalledTimes(1);
expect(client.delete).toHaveBeenCalledTimes(1);
expect(result).toEqual({});
});
- test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => {
- await deleteSuccess(client, repository, registry, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, id, {
- namespace,
- force: true,
- });
-
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['delete']);
- const expectedSpaces = new Set([namespace]);
- const expectedTypes = new Set([MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, new Set([namespace]));
-
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
-
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toBeUndefined();
- });
-
- test(`adds audit event when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns empty object result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeDelete, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
- await deleteSuccess(client, repository, registry, type, id, { namespace });
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.DELETE,
- savedObject: { type, id },
- error: undefined,
- outcome: 'unknown',
+ const result = await deleteSuccess(client, repository, registry, type, id, {
+ namespace,
});
- });
- test(`adds audit event when not successful`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ expect(mockSecurityExt.authorizeDelete).toHaveBeenCalledTimes(1);
+ expect(client.delete).toHaveBeenCalledTimes(1);
+ expect(result).toEqual({});
+ });
- await expect(
- deleteSuccess(client, repository, registry, type, id, { namespace })
- ).rejects.toThrow(enforceError);
+ test(`calls authorizeDelete with correct parameters`, async () => {
+ await deleteSuccess(client, repository, registry, type, id, {
+ namespace,
+ force: true,
+ });
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.DELETE,
- savedObject: { type, id },
- error: enforceError,
+ expect(mockSecurityExt.authorizeDelete).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeDelete).toHaveBeenCalledWith({
+ namespace,
+ object: { type, id },
});
});
});
describe('#removeReferencesTo', () => {
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ test(`propagates decorated error when authorizeRemoveReferences rejects promise`, async () => {
+ mockSecurityExt.authorizeRemoveReferences.mockRejectedValueOnce(checkAuthError);
await expect(
removeReferencesToSuccess(client, repository, type, id, { namespace })
).rejects.toThrow(checkAuthError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeRemoveReferences).toHaveBeenCalledTimes(1);
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeRemoveReferences, 'unauthorized');
await expect(
removeReferencesToSuccess(client, repository, type, id, { namespace })
).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeRemoveReferences).toHaveBeenCalledTimes(1);
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeRemoveReferences, 'partially_authorized');
setupRedactPassthrough(mockSecurityExt);
const result = await removeReferencesToSuccess(client, repository, type, id, { namespace });
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeRemoveReferences).toHaveBeenCalledTimes(1);
expect(client.updateByQuery).toHaveBeenCalledTimes(1);
expect(result).toEqual(expect.objectContaining({ updated: REMOVE_REFS_COUNT }));
});
- test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => {
- await removeReferencesToSuccess(client, repository, type, id, { namespace });
-
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['delete']);
- const expectedSpaces = new Set([namespace]);
- const expectedTypes = new Set([type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(type, new Set([namespace]));
-
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
-
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toBeUndefined();
- });
-
- test(`adds audit event when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeRemoveReferences, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
- await removeReferencesToSuccess(client, repository, type, id, { namespace });
+ const result = await removeReferencesToSuccess(client, repository, type, id, { namespace });
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.REMOVE_REFERENCES,
- savedObject: { type, id },
- error: undefined,
- outcome: 'unknown',
- });
+ expect(mockSecurityExt.authorizeRemoveReferences).toHaveBeenCalledTimes(1);
+ expect(client.updateByQuery).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(expect.objectContaining({ updated: REMOVE_REFS_COUNT }));
});
- test(`adds audit event when not successful`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
- await expect(
- removeReferencesToSuccess(client, repository, type, id, { namespace })
- ).rejects.toThrow(enforceError);
+ test(`calls authorizeRemoveReferences with correct parameters`, async () => {
+ await removeReferencesToSuccess(client, repository, type, id, { namespace });
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.REMOVE_REFERENCES,
- savedObject: { type, id },
- error: enforceError,
+ expect(mockSecurityExt.authorizeRemoveReferences).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeRemoveReferences).toHaveBeenCalledWith({
+ namespace,
+ object: {
+ id,
+ type,
+ },
});
});
});
@@ -704,159 +554,128 @@ describe('SavedObjectsRepository Security Extension', () => {
const obj1 = { type, id: 'one' };
const obj2 = { type, id: 'two' };
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ const expectedResult = {
+ errors: [
+ {
+ error: {
+ error: 'Conflict',
+ message: `Saved object [${obj1.type}/${obj1.id}] conflict`,
+ statusCode: 409,
+ },
+ id: obj1.id,
+ type: obj1.type,
+ },
+ {
+ error: {
+ error: 'Conflict',
+ message: `Saved object [${obj2.type}/${obj2.id}] conflict`,
+ statusCode: 409,
+ },
+ id: obj2.id,
+ type: obj2.type,
+ },
+ ],
+ };
+
+ test(`propagates decorated error when authorizeCheckConflicts rejects promise`, async () => {
+ mockSecurityExt.authorizeCheckConflicts.mockRejectedValueOnce(checkAuthError);
await expect(
checkConflictsSuccess(client, repository, registry, [obj1, obj2], { namespace })
).rejects.toThrow(checkAuthError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeCheckConflicts).toHaveBeenCalledTimes(1);
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeCheckConflicts, 'unauthorized');
await expect(
checkConflictsSuccess(client, repository, registry, [obj1, obj2], { namespace })
).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeCheckConflicts).toHaveBeenCalledTimes(1);
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeCheckConflicts, 'partially_authorized');
setupRedactPassthrough(mockSecurityExt);
const result = await checkConflictsSuccess(client, repository, registry, [obj1, obj2], {
namespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeCheckConflicts).toHaveBeenCalledTimes(1);
expect(client.mget).toHaveBeenCalledTimes(1);
- // Default mock mget makes each object found
- expect(result).toEqual(
- expect.objectContaining({
- errors: [
- {
- error: {
- error: 'Conflict',
- message: `Saved object [${obj1.type}/${obj1.id}] conflict`,
- statusCode: 409,
- },
- id: obj1.id,
- type: obj1.type,
- },
- {
- error: {
- error: 'Conflict',
- message: `Saved object [${obj2.type}/${obj2.id}] conflict`,
- statusCode: 409,
- },
- id: obj2.id,
- type: obj2.type,
- },
- ],
- })
- );
+ expect(result).toEqual(expectedResult);
});
- test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => {
- await checkConflictsSuccess(client, repository, registry, [obj1, obj2], { namespace });
+ test(`returns result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeCheckConflicts, 'fully_authorized');
+ setupRedactPassthrough(mockSecurityExt);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['bulk_create']);
- const expectedSpaces = new Set([namespace]);
- const expectedTypes = new Set([obj1.type, obj2.type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(obj1.type, new Set([namespace]));
- expectedEnforceMap.set(obj2.type, new Set([namespace]));
+ const result = await checkConflictsSuccess(client, repository, registry, [obj1, obj2], {
+ namespace,
+ });
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
-
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toBeUndefined();
+ expect(mockSecurityExt.authorizeCheckConflicts).toHaveBeenCalledTimes(1);
+ expect(client.mget).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(expectedResult);
+ });
+
+ test(`calls authorizeCheckConflicts with correct parameters`, async () => {
+ await checkConflictsSuccess(client, repository, registry, [obj1, obj2], { namespace });
+
+ expect(mockSecurityExt.authorizeCheckConflicts).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeCheckConflicts).toHaveBeenCalledWith({
+ namespace,
+ objects: [obj1, obj2],
+ });
});
});
describe('#openPointInTimeForType', () => {
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ test(`propagates decorated error when authorizeOpenPointInTime rejects promise`, async () => {
+ mockSecurityExt.authorizeOpenPointInTime.mockRejectedValueOnce(checkAuthError);
await expect(repository.openPointInTimeForType(type)).rejects.toThrow(checkAuthError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeOpenPointInTime).toHaveBeenCalledTimes(1);
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeOpenPointInTime, 'partially_authorized');
client.openPointInTime.mockResponseOnce({ id });
const result = await repository.openPointInTimeForType(type);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeOpenPointInTime).toHaveBeenCalledTimes(1);
expect(client.openPointInTime).toHaveBeenCalledTimes(1);
expect(result).toEqual(expect.objectContaining({ id }));
});
- test(`adds audit event when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeOpenPointInTime, 'fully_authorized');
client.openPointInTime.mockResponseOnce({ id });
- await repository.openPointInTimeForType(type);
+ const result = await repository.openPointInTimeForType(type);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.OPEN_POINT_IN_TIME,
- outcome: 'unknown',
- });
+ expect(mockSecurityExt.authorizeOpenPointInTime).toHaveBeenCalledTimes(1);
+ expect(client.openPointInTime).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(expect.objectContaining({ id }));
});
test(`throws an error when unauthorized`, async () => {
- setupPerformAuthUnauthorized(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeOpenPointInTime, 'unauthorized');
await expect(repository.openPointInTimeForType(type)).rejects.toThrowError();
});
- test(`adds audit event when unauthorized`, async () => {
- setupPerformAuthUnauthorized(mockSecurityExt);
-
- await expect(repository.openPointInTimeForType(type)).rejects.toThrowError();
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.OPEN_POINT_IN_TIME,
- error: new Error('User is unauthorized for any requested types/spaces.'),
- });
- });
-
- test(`calls performAuthorization with correct actions, types, and spaces`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`calls authorizeOpenPointInTime with correct parameters`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeOpenPointInTime, 'fully_authorized');
client.openPointInTime.mockResponseOnce({ id });
- await repository.openPointInTimeForType(type, { namespaces: [namespace] });
-
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['open_point_in_time']);
- const expectedSpaces = new Set([namespace]);
- const expectedTypes = new Set([type]);
-
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
-
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(actualEnforceMap).toBeUndefined();
- expect(actualOptions).toBeUndefined();
+ const namespaces = [namespace, 'x', 'y', 'z'];
+ await repository.openPointInTimeForType(type, { namespaces });
+ expect(mockSecurityExt.authorizeOpenPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeOpenPointInTime).toHaveBeenCalledWith({
+ namespaces: new Set(namespaces),
+ types: new Set([type]),
+ });
});
});
@@ -869,30 +688,32 @@ describe('SavedObjectsRepository Security Extension', () => {
expect(result).toEqual(expectedResult);
});
- test(`adds audit event`, async () => {
+ test(`calls auditClosePointInTime`, async () => {
await repository.closePointInTime(id);
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.CLOSE_POINT_IN_TIME,
- outcome: 'unknown',
- });
+ expect(mockSecurityExt.auditClosePointInTime).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.auditClosePointInTime).toHaveBeenCalledWith();
});
});
describe('#find', () => {
- test(`propagates decorated error when Authorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ test(`propagates decorated error when authorizeFind rejects promise`, async () => {
+ mockSecurityExt.authorizeFind.mockRejectedValueOnce(checkAuthError);
+ await expect(findSuccess(client, repository, { type })).rejects.toThrow(checkAuthError);
+ expect(mockSecurityExt.authorizeFind).toHaveBeenCalledTimes(1);
+ });
+
+ test(`propagates decorated error when getFindRedactTypeMap rejects promise`, async () => {
+ mockSecurityExt.getFindRedactTypeMap.mockRejectedValueOnce(checkAuthError);
await expect(findSuccess(client, repository, { type })).rejects.toThrow(checkAuthError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.getFindRedactTypeMap).toHaveBeenCalledTimes(1);
});
- test(`returns empty result when unauthorized`, async () => {
- setupPerformAuthUnauthorized(mockSecurityExt);
+ test(`returns empty result when preauthorization is unauthorized`, async () => {
+ setupAuthorizeFind(mockSecurityExt, 'unauthorized');
const result = await repository.find({ type });
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeFind).toHaveBeenCalledTimes(1);
expect(result).toEqual(
expect.objectContaining({
saved_objects: [],
@@ -901,18 +722,45 @@ describe('SavedObjectsRepository Security Extension', () => {
);
});
+ test(`returns result when getFindRedactTypeMap is unauthorized`, async () => {
+ setupAuthorizeFind(mockSecurityExt, 'fully_authorized');
+ setupRedactPassthrough(mockSecurityExt);
+
+ const generatedResults = generateIndexPatternSearchResults(namespace);
+ client.search.mockResponseOnce(generatedResults);
+ const result = await repository.find({ type });
+
+ expect(mockSecurityExt.authorizeFind).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.getFindRedactTypeMap).toHaveBeenCalledTimes(1);
+ expect(result.total).toBe(4);
+ expect(result.saved_objects).toHaveLength(4);
+ generatedResults.hits.hits.forEach((doc, i) => {
+ expect(result.saved_objects[i]).toEqual({
+ id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globalType)\:/, ''),
+ type: doc._source!.type,
+ originId: doc._source!.originId,
+ ...mockTimestampFields,
+ version: mockVersion,
+ score: doc._score,
+ attributes: doc._source![doc._source!.type],
+ references: [],
+ namespaces: doc._source!.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : [namespace],
+ });
+ });
+ });
+
test(`calls es search with only authorized spaces when partially authorized`, async () => {
// Setup partial authorization with the specific type and space of the current test definition
const authRecord: Record = {
find: { authorizedSpaces: [namespace] },
};
- mockSecurityExt.performAuthorization.mockResolvedValue({
+ mockSecurityExt.authorizeFind.mockResolvedValue({
status: 'partially_authorized',
typeMap: Object.freeze(new Map([[type, authRecord]])),
});
await findSuccess(client, repository, { type, namespaces: [namespace, 'ns-1'] });
- expect(mockGetSearchDsl.mock.calls[0].length).toBe(3); // Find success verifies this is called once, this shouyld always pass
+ expect(mockGetSearchDsl.mock.calls[0].length).toBe(3); // Find success verifies this is called once, this should always pass
const {
typeToNamespacesMap: actualMap,
}: { typeToNamespacesMap: Map } =
@@ -925,7 +773,7 @@ describe('SavedObjectsRepository Security Extension', () => {
});
test(`returns result of es find when fully authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFind(mockSecurityExt, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
const { result, generatedResults } = await findSuccess(
@@ -955,7 +803,7 @@ describe('SavedObjectsRepository Security Extension', () => {
});
test(`uses the authorization map when partially authorized`, async () => {
- setupPerformAuthPartiallyAuthorized(mockSecurityExt);
+ setupAuthorizeFind(mockSecurityExt, 'partially_authorized');
setupRedactPassthrough(mockSecurityExt);
await findSuccess(
@@ -979,7 +827,7 @@ describe('SavedObjectsRepository Security Extension', () => {
});
test(`returns result of es find when partially authorized`, async () => {
- setupPerformAuthPartiallyAuthorized(mockSecurityExt);
+ setupAuthorizeFind(mockSecurityExt, 'partially_authorized');
setupRedactPassthrough(mockSecurityExt);
const { result, generatedResults } = await findSuccess(
@@ -1008,47 +856,45 @@ describe('SavedObjectsRepository Security Extension', () => {
});
});
- test(`calls performAuthorization with correct actions, types, and spaces`, async () => {
- setupPerformAuthPartiallyAuthorized(mockSecurityExt);
+ test(`calls authorizeFind with correct parameters`, async () => {
+ setupAuthorizeFind(mockSecurityExt, 'partially_authorized');
setupRedactPassthrough(mockSecurityExt);
await findSuccess(client, repository, { type, namespaces: [namespace] }, 'ns-2');
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(2);
- const expectedActions = new Set(['find']);
- const expectedSpaces = new Set([namespace]);
- const expectedTypes = new Set([type]);
-
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
-
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(actualEnforceMap).toBeUndefined();
- expect(actualOptions).toBeUndefined();
+ expect(mockSecurityExt.authorizeFind).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeFind).toHaveBeenCalledWith({
+ namespaces: new Set([namespace]),
+ types: new Set(['index-pattern']),
+ });
+ });
- const {
- actions: actualActions2,
- spaces: actualSpaces2,
- types: actualTypes2,
- enforceMap: actualEnforceMap2,
- options: actualOptions2,
- } = mockSecurityExt.performAuthorization.mock.calls[1][0];
+ test(`calls GetFindRedactTypeMap with correct parameters`, async () => {
+ setupAuthorizeFind(mockSecurityExt, 'partially_authorized');
+ setupRedactPassthrough(mockSecurityExt);
+ const { generatedResults } = await findSuccess(
+ client,
+ repository,
+ { type, namespaces: [namespace] },
+ 'ns-2'
+ );
- expect(setsAreEqual(actualActions2, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces2, new Set([...expectedSpaces, 'ns-2']))).toBeTruthy();
- expect(setsAreEqual(actualTypes2, expectedTypes)).toBeTruthy();
- expect(actualEnforceMap2).toBeUndefined();
- expect(actualOptions2).toBeUndefined();
+ expect(mockSecurityExt.authorizeFind).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.getFindRedactTypeMap).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.getFindRedactTypeMap).toHaveBeenCalledWith({
+ previouslyCheckedNamespaces: new Set([namespace]),
+ objects: generatedResults.hits.hits.map((obj) => {
+ return {
+ type: obj._source?.type,
+ id: obj._id.slice(obj._id.lastIndexOf(':') + 1), // find removes the space/type from the ID in the original raw doc
+ existingNamespaces:
+ obj._source?.namespaces ?? obj._source?.namespace ? [obj._source?.namespace] : [],
+ };
+ }),
+ });
});
test(`calls redactNamespaces with authorization map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFind(mockSecurityExt, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
const { generatedResults } = await findSuccess(client, repository, {
@@ -1065,44 +911,6 @@ describe('SavedObjectsRepository Security Extension', () => {
})
);
});
-
- test(`adds audit per object event when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
- setupRedactPassthrough(mockSecurityExt);
-
- const { generatedResults } = await findSuccess(client, repository, {
- type,
- namespaces: [namespace],
- });
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(
- generatedResults.hits.hits.length
- );
-
- generatedResults.hits.hits.forEach((doc, i) => {
- expect(mockSecurityExt.addAuditEvent.mock.calls[i]).toEqual([
- {
- action: AuditAction.FIND,
- savedObject: {
- type: doc._source!.type,
- id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globalType)\:/, ''),
- },
- },
- ]);
- });
- });
-
- test(`adds audit event when not successful`, async () => {
- setupPerformAuthUnauthorized(mockSecurityExt);
-
- await repository.find({ type });
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.FIND,
- error: new Error('User is unauthorized for any requested types/spaces.'),
- });
- });
});
describe('#bulkGet', () => {
@@ -1134,26 +942,43 @@ describe('SavedObjectsRepository Security Extension', () => {
namespaces: [namespace],
};
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ const expectedAuthObjects = [
+ {
+ error: true,
+ existingNamespaces: ['default'],
+ objectNamespaces: ['ns-1', 'ns-2', namespace],
+ id: '6.0.0-alpha1',
+ type: 'multiNamespaceTypeCustomIndex',
+ },
+ {
+ error: false,
+ existingNamespaces: [],
+ objectNamespaces: ['ns-3'],
+ id: 'logstash-*',
+ type: 'index-pattern',
+ },
+ ];
+
+ test(`propagates decorated error when authorizeBulkGet rejects promise`, async () => {
+ mockSecurityExt.authorizeBulkGet.mockRejectedValueOnce(checkAuthError);
await expect(
bulkGetSuccess(client, repository, registry, [obj1, obj2], { namespace })
).rejects.toThrow(checkAuthError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledTimes(1);
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkGet, 'unauthorized');
await expect(
bulkGetSuccess(client, repository, registry, [obj1, obj2], { namespace })
).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledTimes(1);
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkGet, 'partially_authorized');
setupRedactPassthrough(mockSecurityExt);
const { result, mockResponse } = await bulkGetSuccess(
@@ -1164,7 +989,7 @@ describe('SavedObjectsRepository Security Extension', () => {
{ namespace }
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledTimes(1);
expect(client.mget).toHaveBeenCalledTimes(1);
expect(result).toEqual({
saved_objects: [
@@ -1180,7 +1005,35 @@ describe('SavedObjectsRepository Security Extension', () => {
});
});
- test(`calls performAuthorization with correct parameters in default space`, async () => {
+ test(`returns result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkGet, 'fully_authorized');
+ setupRedactPassthrough(mockSecurityExt);
+
+ const { result, mockResponse } = await bulkGetSuccess(
+ client,
+ repository,
+ registry,
+ [obj1, obj2],
+ { namespace }
+ );
+
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledTimes(1);
+ expect(client.mget).toHaveBeenCalledTimes(1);
+ expect(result).toEqual({
+ saved_objects: [
+ expectBulkGetResult(
+ obj1,
+ mockResponse.docs[0] as estypes.GetGetResult
+ ),
+ expectBulkGetResult(
+ obj2,
+ mockResponse.docs[1] as estypes.GetGetResult
+ ),
+ ],
+ });
+ });
+
+ test(`calls authorizeBulkGet with correct parameters in default space`, async () => {
const objA = {
...obj1,
type: MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, // replace the type to a mult-namespace type for this test to be thorough
@@ -1190,30 +1043,14 @@ describe('SavedObjectsRepository Security Extension', () => {
await bulkGetSuccess(client, repository, registry, [objA, objB]);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['bulk_get']);
- const expectedSpaces = new Set(['default', ...objA.namespaces, ...objB.namespaces]);
- const expectedTypes = new Set([objA.type, objB.type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(objA.type, new Set(['default', ...objA.namespaces]));
- expectedEnforceMap.set(objB.type, new Set(['default', ...objB.namespaces]));
-
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
-
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toBeUndefined();
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledWith({
+ namespace: undefined,
+ objects: expectedAuthObjects,
+ });
});
- test(`calls performAuthorization with correct parameters in non-default space`, async () => {
+ test(`calls authorize with correct parameters in non-default space`, async () => {
const objA = {
...obj1,
type: MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, // replace the type to a mult-namespace type for this test to be thorough
@@ -1226,38 +1063,37 @@ describe('SavedObjectsRepository Security Extension', () => {
namespace: optionsNamespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['bulk_get']);
- const expectedSpaces = new Set([optionsNamespace, ...objA.namespaces, ...objB.namespaces]);
- const expectedTypes = new Set([objA.type, objB.type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(objA.type, new Set([optionsNamespace, ...objA.namespaces]));
- expectedEnforceMap.set(objB.type, new Set([optionsNamespace, ...objB.namespaces]));
-
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
-
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toBeUndefined();
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledWith({
+ namespace: optionsNamespace,
+ objects: [
+ {
+ error: true,
+ existingNamespaces: [optionsNamespace],
+ objectNamespaces: objA.namespaces,
+ id: objA.id,
+ type: objA.type,
+ },
+ {
+ error: false,
+ existingNamespaces: [],
+ objectNamespaces: objB.namespaces,
+ id: objB.id,
+ type: objB.type,
+ },
+ ],
+ });
});
test(`calls redactNamespaces with authorization map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkGet, 'partially_authorized');
setupRedactPassthrough(mockSecurityExt);
const objects = [obj1, obj2];
await bulkGetSuccess(client, repository, registry, objects, { namespace });
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledTimes(1);
expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(2);
objects.forEach((obj, i) => {
@@ -1272,40 +1108,6 @@ describe('SavedObjectsRepository Security Extension', () => {
expect(typeMap).toBe(authMap);
});
});
-
- test(`adds audit event per object when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
- setupRedactPassthrough(mockSecurityExt);
-
- const objects = [obj1, obj2];
- await bulkGetSuccess(client, repository, registry, objects, { namespace });
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.GET,
- savedObject: { type: obj.type, id: obj.id },
- });
- });
- });
-
- test(`adds audit event per object when not successful`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
- const objects = [obj1, obj2];
- await expect(
- bulkGetSuccess(client, repository, registry, objects, { namespace })
- ).rejects.toThrow(enforceError);
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.GET,
- savedObject: { type: obj.type, id: obj.id },
- error: enforceError,
- });
- });
- });
});
describe('#bulkCreate', () => {
@@ -1330,69 +1132,84 @@ describe('SavedObjectsRepository Security Extension', () => {
references: [{ name: 'ref_0', type: 'test', id: '2' }],
};
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ test(`propagates decorated error when authorizeCreate rejects promise`, async () => {
+ mockSecurityExt.authorizeBulkCreate.mockRejectedValueOnce(checkAuthError);
await expect(bulkCreateSuccess(client, repository, [obj1, obj2])).rejects.toThrow(
checkAuthError
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkCreate).toHaveBeenCalledTimes(1);
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkCreate as jest.Mock, 'unauthorized');
await expect(
bulkCreateSuccess(client, repository, [obj1, obj2], { namespace })
).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkCreate).toHaveBeenCalledTimes(1);
+ });
+
+ test(`returns result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkCreate as jest.Mock, 'partially_authorized');
+ setupRedactPassthrough(mockSecurityExt);
+
+ const objects = [obj1, obj2];
+ const result = await bulkCreateSuccess(client, repository, objects);
+
+ expect(mockSecurityExt.authorizeBulkCreate).toHaveBeenCalledTimes(1);
+ expect(client.bulk).toHaveBeenCalledTimes(1);
+ expect(result).toEqual({
+ saved_objects: objects.map((obj) => expectCreateResult(obj)),
+ });
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkCreate as jest.Mock, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
const objects = [obj1, obj2];
const result = await bulkCreateSuccess(client, repository, objects);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkCreate).toHaveBeenCalledTimes(1);
expect(client.bulk).toHaveBeenCalledTimes(1);
expect(result).toEqual({
saved_objects: objects.map((obj) => expectCreateResult(obj)),
});
});
- test(`calls PerformAuthorization with correct actions, types, spaces, and enforce map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`calls authorizeCreate with correct parameters`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkCreate as jest.Mock, 'fully_authorized');
await bulkCreateSuccess(client, repository, [obj1, obj2], {
namespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['bulk_create']);
- const expectedSpaces = new Set([namespace]);
- const expectedTypes = new Set([obj1.type, obj2.type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(obj1.type, new Set([namespace]));
- expectedEnforceMap.set(obj2.type, new Set([namespace]));
+ expect(mockSecurityExt.authorizeBulkCreate).toHaveBeenCalledTimes(1);
+ const expectedNamespace = namespace;
+ const expectedObjects = [
+ {
+ type: obj1.type,
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ initialNamespaces: undefined,
+ existingNamespaces: [],
+ },
+ {
+ type: obj2.type,
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ initialNamespaces: undefined,
+ existingNamespaces: [],
+ },
+ ];
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
+ const { namespace: actualNamespace, objects: actualObjects } =
+ mockSecurityExt.authorizeBulkCreate.mock.calls[0][0];
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true }));
+ expect(expectedNamespace).toEqual(actualNamespace);
+ expect(expectedObjects).toEqual(actualObjects);
});
- test(`calls performAuthorization with initial spaces for one type`, async () => {
+ test(`calls authorizeCreate with initial spaces for one type`, async () => {
const objA = {
...obj1,
type: MULTI_NAMESPACE_TYPE,
@@ -1405,42 +1222,37 @@ describe('SavedObjectsRepository Security Extension', () => {
};
const optionsNamespace = 'ns-5';
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkCreate as jest.Mock, 'fully_authorized');
await bulkCreateSuccess(client, repository, [objA, objB], {
namespace: optionsNamespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['bulk_create']);
- const expectedSpaces = new Set([
- optionsNamespace,
- ...objA.initialNamespaces,
- ...objB.initialNamespaces,
- ]);
- const expectedTypes = new Set([objA.type, objB.type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(
- objA.type,
- new Set([optionsNamespace, ...objA.initialNamespaces, ...objB.initialNamespaces])
- );
+ expect(mockSecurityExt.authorizeBulkCreate).toHaveBeenCalledTimes(1);
+ const expectedNamespace = optionsNamespace;
+ const expectedObjects = [
+ {
+ type: objA.type,
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ initialNamespaces: objA.initialNamespaces,
+ existingNamespaces: [],
+ },
+ {
+ type: objB.type,
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ initialNamespaces: objB.initialNamespaces,
+ existingNamespaces: [],
+ },
+ ];
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
+ const { namespace: actualNamespace, objects: actualObjects } =
+ mockSecurityExt.authorizeBulkCreate.mock.calls[0][0];
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true }));
+ expect(expectedNamespace).toEqual(actualNamespace);
+ expect(expectedObjects).toEqual(actualObjects);
});
- test(`calls performAuthorization with initial spaces for multiple types`, async () => {
+ test(`calls authorizeCreate with initial spaces for multiple types`, async () => {
const objA = {
...obj1,
type: MULTI_NAMESPACE_TYPE,
@@ -1453,48 +1265,45 @@ describe('SavedObjectsRepository Security Extension', () => {
};
const optionsNamespace = 'ns-5';
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkCreate as jest.Mock, 'fully_authorized');
await bulkCreateSuccess(client, repository, [objA, objB], {
namespace: optionsNamespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkCreate).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['bulk_create']);
- const expectedSpaces = new Set([
- optionsNamespace,
- ...objA.initialNamespaces,
- ...objB.initialNamespaces,
- ]);
- const expectedTypes = new Set([objA.type, objB.type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(objA.type, new Set([optionsNamespace, ...objA.initialNamespaces]));
- expectedEnforceMap.set(objB.type, new Set([optionsNamespace, ...objB.initialNamespaces]));
+ const expectedNamespace = optionsNamespace;
+ const expectedObjects = [
+ {
+ type: objA.type,
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ initialNamespaces: objA.initialNamespaces,
+ existingNamespaces: [],
+ },
+ {
+ type: objB.type,
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ initialNamespaces: objB.initialNamespaces,
+ existingNamespaces: [],
+ },
+ ];
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
+ const { namespace: actualNamespace, objects: actualObjects } =
+ mockSecurityExt.authorizeBulkCreate.mock.calls[0][0];
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true }));
+ expect(expectedNamespace).toEqual(actualNamespace);
+ expect(expectedObjects).toEqual(actualObjects);
});
test(`calls redactNamespaces with authorization map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkCreate as jest.Mock, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
const objects = [obj1, obj2];
await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace });
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkCreate).toHaveBeenCalledTimes(1);
expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(2);
objects.forEach((obj, i) => {
@@ -1509,40 +1318,6 @@ describe('SavedObjectsRepository Security Extension', () => {
expect(typeMap).toBe(authMap);
});
});
-
- test(`adds audit event per object when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
-
- const objects = [obj1, obj2];
- await bulkCreateSuccess(client, repository, objects, { namespace });
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.CREATE,
- savedObject: { type: obj.type, id: obj.id },
- outcome: 'unknown',
- });
- });
- });
-
- test(`adds audit event per object when not successful`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
- const objects = [obj1, obj2];
- await expect(bulkCreateSuccess(client, repository, objects, { namespace })).rejects.toThrow(
- enforceError
- );
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.CREATE,
- savedObject: { type: obj.type, id: obj.id },
- error: enforceError,
- });
- });
- });
});
describe('#bulkUpdate', () => {
@@ -1557,69 +1332,82 @@ describe('SavedObjectsRepository Security Extension', () => {
attributes: { title: 'Test Two' },
};
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ test(`propagates decorated error when authorizeUpdate rejects promise`, async () => {
+ mockSecurityExt.authorizeBulkUpdate.mockRejectedValueOnce(checkAuthError);
await expect(bulkUpdateSuccess(client, repository, registry, [obj1, obj2])).rejects.toThrow(
checkAuthError
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkUpdate).toHaveBeenCalledTimes(1);
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkUpdate, 'unauthorized');
await expect(bulkUpdateSuccess(client, repository, registry, [obj1, obj2])).rejects.toThrow(
enforceError
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkUpdate).toHaveBeenCalledTimes(1);
+ });
+
+ test(`returns result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeUpdate, 'partially_authorized');
+ setupRedactPassthrough(mockSecurityExt);
+
+ const objects = [obj1, obj2];
+ const result = await bulkUpdateSuccess(client, repository, registry, objects);
+
+ expect(mockSecurityExt.authorizeBulkUpdate).toHaveBeenCalledTimes(1);
+ expect(client.bulk).toHaveBeenCalledTimes(1);
+ expect(result).toEqual({
+ saved_objects: objects.map((obj) => expectUpdateResult(obj)),
+ });
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeUpdate, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
const objects = [obj1, obj2];
const result = await bulkUpdateSuccess(client, repository, registry, objects);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkUpdate).toHaveBeenCalledTimes(1);
expect(client.bulk).toHaveBeenCalledTimes(1);
expect(result).toEqual({
saved_objects: objects.map((obj) => expectUpdateResult(obj)),
});
});
- test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`calls authorizeBulkUpdate with correct parameters`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeUpdate, 'fully_authorized');
await bulkUpdateSuccess(client, repository, registry, [obj1, obj2], {
namespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['bulk_update']);
- const expectedSpaces = new Set([namespace]);
- const expectedTypes = new Set([obj1.type, obj2.type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(obj1.type, new Set([namespace]));
- expectedEnforceMap.set(obj2.type, new Set([namespace]));
+ expect(mockSecurityExt.authorizeBulkUpdate).toHaveBeenCalledTimes(1);
+ const expectedNamespace = namespace;
+ const expectedObjects = [
+ {
+ type: obj1.type,
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ existingNamespaces: [],
+ },
+ {
+ type: obj2.type,
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ existingNamespaces: [],
+ },
+ ];
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
+ const { namespace: actualNamespace, objects: actualObjects } =
+ mockSecurityExt.authorizeBulkUpdate.mock.calls[0][0];
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toBeUndefined();
+ expect(actualNamespace).toEqual(expectedNamespace);
+ expect(actualObjects).toEqual(expectedObjects);
});
- test(`calls performAuthorization with object spaces`, async () => {
+ test(`calls authorizeBulkUpdate with object spaces`, async () => {
const objA = {
...obj1,
namespace: 'ns-1', // object namespace
@@ -1629,43 +1417,44 @@ describe('SavedObjectsRepository Security Extension', () => {
namespace: 'ns-2', // object namespace
};
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeUpdate, 'fully_authorized');
await bulkUpdateSuccess(client, repository, registry, [objA, objB], {
namespace,
});
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['bulk_update']);
- const expectedSpaces = new Set([namespace, objA.namespace, objB.namespace]);
- const expectedTypes = new Set([objA.type, objB.type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(objA.type, new Set([namespace, objA.namespace]));
- expectedEnforceMap.set(objB.type, new Set([namespace, objB.namespace]));
+ expect(mockSecurityExt.authorizeBulkUpdate).toHaveBeenCalledTimes(1);
+ const expectedNamespace = namespace;
+ const expectedObjects = [
+ {
+ type: obj1.type,
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ objectNamespace: 'ns-1',
+ existingNamespaces: [],
+ },
+ {
+ type: obj2.type,
+ id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
+ objectNamespace: 'ns-2',
+ existingNamespaces: [],
+ },
+ ];
- const {
- actions: actualActions,
- spaces: actualSpaces,
- types: actualTypes,
- enforceMap: actualEnforceMap,
- options: actualOptions,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
+ const { namespace: actualNamespace, objects: actualObjects } =
+ mockSecurityExt.authorizeBulkUpdate.mock.calls[0][0];
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- expect(actualOptions).toBeUndefined();
+ expect(actualNamespace).toEqual(expectedNamespace);
+ expect(actualObjects).toEqual(expectedObjects);
});
test(`calls redactNamespaces with authorization map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkUpdate, 'fully_authorized');
setupRedactPassthrough(mockSecurityExt);
const objects = [obj1, obj2];
await bulkUpdateSuccess(client, repository, registry, objects, { namespace });
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkUpdate).toHaveBeenCalledTimes(1);
expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(2);
objects.forEach((obj, i) => {
@@ -1680,40 +1469,6 @@ describe('SavedObjectsRepository Security Extension', () => {
expect(typeMap).toBe(authMap);
});
});
-
- test(`adds audit event per object when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
-
- const objects = [obj1, obj2];
- await bulkUpdateSuccess(client, repository, registry, objects, { namespace });
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.UPDATE,
- savedObject: { type: obj.type, id: obj.id },
- outcome: 'unknown',
- });
- });
- });
-
- test(`adds audit event per object when not successful`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
- const objects = [obj1, obj2];
- await expect(
- bulkUpdateSuccess(client, repository, registry, objects, { namespace })
- ).rejects.toThrow(enforceError);
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.UPDATE,
- savedObject: { type: obj.type, id: obj.id },
- error: enforceError,
- });
- });
- });
});
describe('#bulkDelete', () => {
@@ -1750,26 +1505,26 @@ describe('SavedObjectsRepository Security Extension', () => {
],
};
- test(`propagates decorated error when performAuthorization rejects promise`, async () => {
- mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError);
+ test(`propagates decorated error when authorizeBulkDelete rejects promise`, async () => {
+ mockSecurityExt.authorizeBulkDelete.mockRejectedValueOnce(checkAuthError);
await expect(
bulkDeleteSuccess(client, repository, registry, testObjs, options)
).rejects.toThrow(checkAuthError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkDelete).toHaveBeenCalledTimes(1);
});
test(`propagates decorated error when unauthorized`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkDelete, 'unauthorized');
await expect(
bulkDeleteSuccess(client, repository, registry, testObjs, options)
).rejects.toThrow(enforceError);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkDelete).toHaveBeenCalledTimes(1);
});
- test(`returns result when authorized`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ test(`returns result when partially authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkDelete, 'partially_authorized');
setupRedactPassthrough(mockSecurityExt);
const result = await bulkDeleteSuccess(
@@ -1781,70 +1536,44 @@ describe('SavedObjectsRepository Security Extension', () => {
internalOptions
);
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkDelete).toHaveBeenCalledTimes(1);
expect(client.bulk).toHaveBeenCalledTimes(1);
expect(result).toEqual({
statuses: testObjs.map((obj) => createBulkDeleteSuccessStatus(obj)),
});
});
- test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
-
- await bulkDeleteSuccess(client, repository, registry, testObjs, options, internalOptions);
-
- expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1);
- const expectedActions = new Set(['bulk_delete']);
- const exptectedSpaces = new Set(internalOptions.mockMGetResponseObjects[1].initialNamespaces);
- const expectedTypes = new Set([obj1.type, obj2.type]);
- const expectedEnforceMap = new Map>();
- expectedEnforceMap.set(obj1.type, new Set([namespace]));
- expectedEnforceMap.set(obj2.type, new Set([namespace]));
-
- const {
- actions: actualActions,
- types: actualTypes,
- spaces: actualSpaces,
- enforceMap: actualEnforceMap,
- } = mockSecurityExt.performAuthorization.mock.calls[0][0];
-
- expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy();
- expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy();
- expect(setsAreEqual(actualSpaces, exptectedSpaces)).toBeTruthy();
- expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy();
- });
+ test(`returns result when fully authorized`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkDelete, 'fully_authorized');
+ setupRedactPassthrough(mockSecurityExt);
- test(`adds audit event per object when successful`, async () => {
- setupPerformAuthFullyAuthorized(mockSecurityExt);
+ const result = await bulkDeleteSuccess(
+ client,
+ repository,
+ registry,
+ testObjs,
+ options,
+ internalOptions
+ );
- const objects = [obj1, obj2];
- await bulkDeleteSuccess(client, repository, registry, objects, options);
-
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.DELETE,
- savedObject: { type: obj.type, id: obj.id },
- outcome: 'unknown',
- });
+ expect(mockSecurityExt.authorizeBulkDelete).toHaveBeenCalledTimes(1);
+ expect(client.bulk).toHaveBeenCalledTimes(1);
+ expect(result).toEqual({
+ statuses: testObjs.map((obj) => createBulkDeleteSuccessStatus(obj)),
});
});
- test(`adds audit event per object when not successful`, async () => {
- setupPerformAuthEnforceFailure(mockSecurityExt);
-
- const objects = [obj1, obj2];
- await expect(
- bulkDeleteSuccess(client, repository, registry, objects, options)
- ).rejects.toThrow(enforceError);
+ test(`calls authorizeBulkDelete with correct actions, types, spaces, and enforce map`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkDelete, 'fully_authorized');
+ await bulkDeleteSuccess(client, repository, registry, testObjs, options, internalOptions);
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length);
- objects.forEach((obj) => {
- expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({
- action: AuditAction.DELETE,
- savedObject: { type: obj.type, id: obj.id },
- error: enforceError,
- });
+ expect(mockSecurityExt.authorizeBulkDelete).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkDelete).toHaveBeenCalledWith({
+ namespace,
+ objects: [
+ { type: obj1.type, id: obj1.id, existingNamespaces: [] },
+ { type: obj2.type, id: obj2.id, existingNamespaces: ['foo-namespace', 'NS-1', 'NS-2'] },
+ ],
});
});
});
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts
index adfa75a48e986..bc9c5b6e7c581 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts
@@ -27,13 +27,13 @@ import {
SavedObjectsBulkUpdateObject,
} from '@kbn/core-saved-objects-api-server';
import { SavedObjectsSerializer } from '@kbn/core-saved-objects-base-server-internal';
-import { SavedObject } from '@kbn/core-saved-objects-server';
import {
ISavedObjectsSpacesExtension,
ISavedObjectsSecurityExtension,
ISavedObjectsEncryptionExtension,
+ SavedObject,
+ SavedObjectsErrorHelpers,
} from '@kbn/core-saved-objects-server';
-import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-utils-server';
import { kibanaMigratorMock } from '../mocks';
import {
createRegistry,
@@ -54,10 +54,11 @@ import {
bulkCreateSuccess,
bulkUpdateSuccess,
findSuccess,
- setupPerformAuthUnauthorized,
generateIndexPatternSearchResults,
bulkDeleteSuccess,
ENCRYPTED_TYPE,
+ setupAuthorizeFunc,
+ setupAuthorizeFind,
} from '../test_helpers/repository.test.common';
import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock';
@@ -881,6 +882,11 @@ describe('SavedObjectsRepository Spaces Extension', () => {
});
describe(`with security extension`, () => {
+ // Note: resolve, bulkResolve, and collectMultiNamespaceReferences are not tested here because they
+ // receive parameter arguments from internal methods (internalBulkResolve and the internal
+ // implementation of collectMultiNamespaceReferences). Arguments to these methods are tested above.
+ const currentSpace = 'current_space';
+
beforeEach(() => {
pointInTimeFinderMock.mockClear();
client = elasticsearchClientMock.createElasticsearchClient();
@@ -900,7 +906,7 @@ describe('SavedObjectsRepository Spaces Extension', () => {
mockSpacesExt.getSearchableNamespaces.mockImplementation(
(namespaces: string[] | undefined): Promise => {
if (!namespaces) {
- return Promise.resolve([] as string[]);
+ return Promise.resolve([currentSpace] as string[]);
} else if (!namespaces.length) {
return Promise.resolve(namespaces);
}
@@ -915,11 +921,17 @@ describe('SavedObjectsRepository Spaces Extension', () => {
}
}
);
+ mockSpacesExt.getCurrentNamespace.mockImplementation((namespace: string | undefined) => {
+ if (namespace) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(ERROR_NAMESPACE_SPECIFIED);
+ }
+ return currentSpace;
+ });
});
describe(`#find`, () => {
test(`returns empty result if user is unauthorized`, async () => {
- setupPerformAuthUnauthorized(mockSecurityExt);
+ setupAuthorizeFind(mockSecurityExt, 'unauthorized');
const type = 'index-pattern';
const spaceOverride = 'ns-4';
const generatedResults = generateIndexPatternSearchResults(spaceOverride);
@@ -927,6 +939,289 @@ describe('SavedObjectsRepository Spaces Extension', () => {
const result = await repository.find({ type, namespaces: [spaceOverride] });
expect(result).toEqual(expect.objectContaining({ total: 0 }));
});
+
+ test(`calls authorizeFind with the current namespace`, async () => {
+ const type = 'index-pattern';
+ await findSuccess(client, repository, { type });
+ expect(mockSpacesExt.getSearchableNamespaces).toBeCalledTimes(1);
+ expect(mockSpacesExt.getSearchableNamespaces).toBeCalledWith(undefined);
+ expect(mockSecurityExt.authorizeFind).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeFind).toHaveBeenCalledWith(
+ expect.objectContaining({ namespaces: new Set([currentSpace]) })
+ );
+ });
+ });
+
+ describe(`#create`, () => {
+ test(`calls authorizeCreate with the current namespace`, async () => {
+ const type = CUSTOM_INDEX_TYPE;
+ setupAuthorizeFunc(mockSecurityExt.authorizeCreate as jest.Mock, 'fully_authorized');
+ await repository.create(type, { attr: 'value' });
+ expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
+ expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined);
+ expect(mockSecurityExt.authorizeCreate).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeCreate).toHaveBeenCalledWith(
+ expect.objectContaining({ namespace: currentSpace })
+ );
+ });
+ });
+
+ describe(`#bulkCreate`, () => {
+ const obj1 = {
+ type: 'config',
+ id: '6.0.0-alpha1',
+ attributes: { title: 'Test One' },
+ references: [{ name: 'ref_0', type: 'test', id: '1' }],
+ };
+ const obj2 = {
+ type: MULTI_NAMESPACE_TYPE,
+ id: 'logstash-*',
+ attributes: { title: 'Test Two' },
+ references: [{ name: 'ref_0', type: 'test', id: '2' }],
+ };
+
+ beforeEach(() => {
+ mockPreflightCheckForCreate.mockReset();
+ mockPreflightCheckForCreate.mockImplementation(({ objects }) => {
+ return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default
+ });
+ });
+
+ test(`calls authorizeBulkCreate with the current namespace`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkCreate, 'fully_authorized');
+ await bulkCreateSuccess(client, repository, [obj1, obj2]);
+ expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
+ expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined);
+ expect(mockSecurityExt.authorizeBulkCreate).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkCreate).toHaveBeenCalledWith(
+ expect.objectContaining({ namespace: currentSpace })
+ );
+ });
+ });
+
+ describe(`#get`, () => {
+ test(`calls authorizeGet with the current namespace`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeGet, 'fully_authorized');
+ const type = CUSTOM_INDEX_TYPE;
+ const id = 'some-id';
+
+ const response = getMockGetResponse(registry, {
+ type,
+ id,
+ });
+
+ client.get.mockResponseOnce(response);
+ await repository.get(type, id);
+ expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
+ expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined);
+ expect(mockSecurityExt.authorizeGet).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeGet).toHaveBeenCalledWith(
+ expect.objectContaining({ namespace: currentSpace })
+ );
+ });
+ });
+
+ describe(`#bulkGet`, () => {
+ const obj1: SavedObject = {
+ type: 'config',
+ id: '6.0.0-alpha1',
+ attributes: { title: 'Testing' },
+ references: [
+ {
+ name: 'ref_0',
+ type: 'test',
+ id: '1',
+ },
+ ],
+ originId: 'some-origin-id', // only one of the results has an originId, this is intentional to test both a positive and negative case
+ };
+ const obj2: SavedObject = {
+ type: MULTI_NAMESPACE_TYPE,
+ id: 'logstash-*',
+ attributes: { title: 'Testing' },
+ references: [
+ {
+ name: 'ref_0',
+ type: 'test',
+ id: '2',
+ },
+ ],
+ };
+
+ test(`calls authorizeBulkGet with the current namespace`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkGet, 'fully_authorized');
+ await bulkGetSuccess(client, repository, registry, [obj1, obj2]);
+ expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
+ expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined);
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkGet).toHaveBeenCalledWith(
+ expect.objectContaining({ namespace: currentSpace })
+ );
+ });
+ });
+
+ describe(`#update`, () => {
+ test(`calls authorizeUpdate with the current namespace`, async () => {
+ const type = CUSTOM_INDEX_TYPE;
+ const id = 'some-id';
+
+ await updateSuccess(
+ client,
+ repository,
+ registry,
+ type,
+ id,
+ {},
+ { upsert: true },
+ { mockGetResponseValue: { found: false } as estypes.GetResponse }
+ );
+ expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
+ expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined);
+ expect(mockSecurityExt.authorizeUpdate).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeUpdate).toHaveBeenCalledWith(
+ expect.objectContaining({ namespace: currentSpace })
+ );
+ });
+ });
+
+ describe(`#bulkUpdate`, () => {
+ const obj1: SavedObjectsBulkUpdateObject = {
+ type: 'config',
+ id: '6.0.0-alpha1',
+ attributes: { title: 'Test One' },
+ };
+ const obj2: SavedObjectsBulkUpdateObject = {
+ type: MULTI_NAMESPACE_TYPE,
+ id: 'logstash-*',
+ attributes: { title: 'Test Two' },
+ };
+
+ test(`calls authorizeBulkUpdate with the current namespace`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkUpdate, 'fully_authorized');
+ await bulkUpdateSuccess(
+ client,
+ repository,
+ registry,
+ [obj1, obj2],
+ undefined,
+ undefined,
+ currentSpace
+ );
+ expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
+ expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined);
+ expect(mockSecurityExt.authorizeBulkUpdate).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkUpdate).toHaveBeenCalledWith(
+ expect.objectContaining({ namespace: currentSpace })
+ );
+ });
+ });
+
+ describe(`#delete`, () => {
+ test(`calls authorizeDelete with the current namespace`, async () => {
+ const type = CUSTOM_INDEX_TYPE;
+ const id = 'some-id';
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkDelete, 'fully_authorized');
+ await deleteSuccess(client, repository, registry, type, id);
+ expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
+ expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined);
+ expect(mockSecurityExt.authorizeDelete).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeDelete).toHaveBeenCalledWith(
+ expect.objectContaining({ namespace: currentSpace })
+ );
+ });
+ });
+
+ describe(`#bulkDelete`, () => {
+ const obj1: SavedObjectsBulkUpdateObject = {
+ type: 'config',
+ id: '6.0.0-alpha1',
+ attributes: { title: 'Test One' },
+ };
+ const obj2: SavedObjectsBulkUpdateObject = {
+ type: MULTI_NAMESPACE_TYPE,
+ id: 'logstash-*',
+ attributes: { title: 'Test Two' },
+ };
+
+ const testObjs = [obj1, obj2];
+ const options = {
+ force: true,
+ };
+
+ const internalOptions = {
+ mockMGetResponseObjects: [
+ {
+ ...obj1,
+ initialNamespaces: undefined,
+ },
+ {
+ ...obj2,
+ initialNamespaces: [currentSpace, 'NS-1', 'NS-2'],
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ mockDeleteLegacyUrlAliases.mockClear();
+ mockDeleteLegacyUrlAliases.mockResolvedValue();
+ });
+
+ test(`calls authorizeBulkDelete with the current namespace`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeBulkDelete, 'fully_authorized');
+ await bulkDeleteSuccess(client, repository, registry, testObjs, options, internalOptions);
+ expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
+ expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined);
+ expect(mockSecurityExt.authorizeBulkDelete).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeBulkDelete).toHaveBeenCalledWith(
+ expect.objectContaining({ namespace: currentSpace })
+ );
+ });
+ });
+
+ describe(`#checkConflicts`, () => {
+ test(`calls authorizeCheckConflicts with the current namespace`, async () => {
+ const obj1 = { type: CUSTOM_INDEX_TYPE, id: 'one' };
+ const obj2 = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: 'two' };
+
+ await checkConflictsSuccess(client, repository, registry, [obj1, obj2]);
+ expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
+ expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined);
+ expect(mockSecurityExt.authorizeCheckConflicts).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeCheckConflicts).toHaveBeenCalledWith(
+ expect.objectContaining({ namespace: currentSpace })
+ );
+ });
+ });
+
+ describe(`#removeReferencesTo`, () => {
+ test(`calls authorizeRemoveReferences with the current namespace`, async () => {
+ const type = CUSTOM_INDEX_TYPE;
+ const id = 'some-id';
+
+ const query = { query: 1, aggregations: 2 };
+ mockGetSearchDsl.mockReturnValue(query);
+
+ await removeReferencesToSuccess(client, repository, type, id);
+ expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
+ expect(mockSpacesExt.getCurrentNamespace).toHaveBeenCalledWith(undefined);
+ expect(mockSecurityExt.authorizeRemoveReferences).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeRemoveReferences).toHaveBeenCalledWith(
+ expect.objectContaining({ namespace: currentSpace })
+ );
+ });
+ });
+
+ describe(`#openPointInTimeForType`, () => {
+ test(`calls authorizeOpenPointInTime with the current namespace`, async () => {
+ setupAuthorizeFunc(mockSecurityExt.authorizeOpenPointInTime, 'fully_authorized');
+ await repository.openPointInTimeForType(CUSTOM_INDEX_TYPE);
+ expect(mockSpacesExt.getSearchableNamespaces).toBeCalledTimes(1);
+ expect(mockSpacesExt.getSearchableNamespaces).toBeCalledWith(undefined); // will resolve current space
+ expect(mockSecurityExt.authorizeOpenPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockSecurityExt.authorizeOpenPointInTime).toHaveBeenCalledWith(
+ expect.objectContaining({ namespaces: new Set([currentSpace]) })
+ );
+ });
});
});
@@ -972,7 +1267,7 @@ describe('SavedObjectsRepository Spaces Extension', () => {
});
describe(`#create`, () => {
- test(`calls encryptAttributes with the current namespace by default`, async () => {
+ test(`calls encryptAttributes with the current namespace`, async () => {
mockEncryptionExt.isEncryptableType.mockReturnValue(true);
await repository.create(encryptedSO.type, encryptedSO.attributes);
expect(mockSpacesExt.getCurrentNamespace).toBeCalledTimes(1);
@@ -1002,7 +1297,7 @@ describe('SavedObjectsRepository Spaces Extension', () => {
references: [{ name: 'ref_0', type: 'test', id: '1' }],
};
- test(`calls encryptAttributes with the current namespace by default`, async () => {
+ test(`calls encryptAttributes with the current namespace`, async () => {
mockEncryptionExt.isEncryptableType.mockReturnValueOnce(false);
mockEncryptionExt.isEncryptableType.mockReturnValueOnce(true);
mockEncryptionExt.isEncryptableType.mockReturnValueOnce(false);
@@ -1038,7 +1333,7 @@ describe('SavedObjectsRepository Spaces Extension', () => {
});
describe(`#update`, () => {
- it('calls encryptAttributes with the current namespace by default', async () => {
+ it('calls encryptAttributes with the current namespace', async () => {
mockEncryptionExt.isEncryptableType.mockReturnValue(true);
mockEncryptionExt.decryptOrStripResponseAttributes.mockResolvedValue({
...encryptedSO,
@@ -1085,7 +1380,7 @@ describe('SavedObjectsRepository Spaces Extension', () => {
attributes: { title: 'Test Two' },
};
- it(`calls encryptAttributes with the current namespace by default`, async () => {
+ it(`calls encryptAttributes with the current namespace`, async () => {
mockEncryptionExt.isEncryptableType.mockReturnValueOnce(false);
mockEncryptionExt.isEncryptableType.mockReturnValueOnce(true);
mockEncryptionExt.isEncryptableType.mockReturnValueOnce(false);
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts
index 1a0fbb2805daf..0b395b6154819 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts
@@ -23,7 +23,6 @@ import {
import type { Payload } from '@hapi/boom';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-server';
import type {
SavedObjectsBaseOptions,
SavedObjectsFindOptions,
@@ -52,11 +51,12 @@ import type {
SavedObjectsRawDoc,
SavedObjectsRawDocSource,
SavedObjectUnsanitizedDoc,
+ SavedObject,
+ SavedObjectReference,
+ BulkResolveError,
} from '@kbn/core-saved-objects-server';
-import {
- SavedObjectsErrorHelpers,
- ALL_NAMESPACES_STRING,
-} from '@kbn/core-saved-objects-utils-server';
+import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server';
+import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { SavedObjectsRepository } from './repository';
import { PointInTimeFinder } from './point_in_time_finder';
import { loggerMock } from '@kbn/logging-mocks';
@@ -69,7 +69,6 @@ import { kibanaMigratorMock } from '../mocks';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import * as esKuery from '@kbn/es-query';
import { errors as EsErrors } from '@elastic/elasticsearch';
-import type { InternalBulkResolveError } from './internal_bulk_resolve';
import {
CUSTOM_INDEX_TYPE,
@@ -4069,7 +4068,7 @@ describe('SavedObjectsRepository', () => {
it('throws when internalBulkResolve result is an error', async () => {
const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error('Oh no!'));
- const expectedResult: InternalBulkResolveError = { type: 'obj-type', id: 'obj-id', error };
+ const expectedResult: BulkResolveError = { type: 'obj-type', id: 'obj-id', error };
mockInternalBulkResolve.mockResolvedValue({ resolved_objects: [expectedResult] });
await expect(repository.resolve('foo', '2')).rejects.toEqual(error);
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts
index 1c36ad1edc8b8..2e2525d3d3b8b 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts
@@ -17,7 +17,7 @@ import {
isSupportedEsServer,
isNotFoundFromUnsupportedServer,
} from '@kbn/core-elasticsearch-server-internal';
-import type { SavedObject } from '@kbn/core-saved-objects-server';
+import type { BulkResolveError } from '@kbn/core-saved-objects-server';
import type {
SavedObjectsBaseOptions,
SavedObjectsIncrementCounterOptions,
@@ -69,15 +69,15 @@ import {
type ISavedObjectsEncryptionExtension,
type ISavedObjectsSecurityExtension,
type ISavedObjectsSpacesExtension,
- AuditAction,
type CheckAuthorizationResult,
type AuthorizationTypeMap,
+ AuthorizeCreateObject,
+ AuthorizeUpdateObject,
+ type AuthorizeBulkGetObject,
+ type SavedObject,
} from '@kbn/core-saved-objects-server';
-import {
- DEFAULT_NAMESPACE_STRING,
- SavedObjectsErrorHelpers,
- type DecoratedError,
-} from '@kbn/core-saved-objects-utils-server';
+import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
+import { SavedObjectsErrorHelpers, type DecoratedError } from '@kbn/core-saved-objects-server';
import {
ALL_NAMESPACES_STRING,
FIND_DEFAULT_PAGE,
@@ -101,11 +101,7 @@ import { PointInTimeFinder } from './point_in_time_finder';
import { createRepositoryEsClient, type RepositoryEsClient } from './repository_es_client';
import { getSearchDsl } from './search_dsl';
import { includedFields } from './included_fields';
-import {
- internalBulkResolve,
- type InternalBulkResolveError,
- isBulkResolveError,
-} from './internal_bulk_resolve';
+import { internalBulkResolve, isBulkResolveError } from './internal_bulk_resolve';
import { validateConvertFilterToKueryNode } from './filter_utils';
import { validateAndConvertAggregations } from './aggregations';
import {
@@ -351,28 +347,14 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
existingOriginId = preflightResult?.existingDocument?._source?.originId;
}
- const spacesToEnforce = new Set(initialNamespaces).add(namespaceString); // Always check/enforce authZ for the active space
- const existingNamespaces = preflightResult?.existingDocument?._source?.namespaces || [];
- const spacesToAuthorize = new Set(existingNamespaces);
- spacesToAuthorize.delete(ALL_NAMESPACES_STRING); // Don't accidentally check for global privileges when the object exists in '*'
- const authorizationResult = await this._securityExtension?.performAuthorization({
- // If a user tries to create an object with `initialNamespaces: ['*']`, they need to have 'create' privileges for the Global Resource
- // (e.g., All privileges for All Spaces).
- // Inversely, if a user tries to overwrite an object that already exists in '*', they don't need to 'create' privileges for the Global
- // Resource, so in that case we have to filter out that string from spacesToAuthorize (because `allowGlobalResource: true` is used
- // below.)
- actions: new Set(['create']),
- types: new Set([type]),
- spaces: new Set([...spacesToEnforce, ...spacesToAuthorize]), // existing namespaces are included so we can later redact if necessary
- enforceMap: new Map([[type, spacesToEnforce]]),
- auditCallback: (error) =>
- this._securityExtension!.addAuditEvent({
- action: AuditAction.CREATE,
- savedObject: { type, id },
- error,
- ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the create operation has not occurred yet
- }),
- options: { allowGlobalResource: true },
+ const authorizationResult = await this._securityExtension?.authorizeCreate({
+ namespace,
+ object: {
+ type,
+ id,
+ initialNamespaces,
+ existingNamespaces: preflightResult?.existingDocument?._source?.namespaces ?? [],
+ },
});
if (preflightResult?.error) {
@@ -524,45 +506,20 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
objects: preflightCheckObjects,
});
- const typesAndSpaces = new Map>();
- const spacesToAuthorize = new Set([namespaceString]); // Always check authZ for the active space
- for (const { value } of validObjects) {
- const { object, preflightCheckIndex: index } = value;
+ const authObjects: AuthorizeCreateObject[] = validObjects.map((element) => {
+ const { object, preflightCheckIndex: index } = element.value;
const preflightResult = index !== undefined ? preflightCheckResponse[index] : undefined;
+ return {
+ type: object.type,
+ id: object.id,
+ initialNamespaces: object.initialNamespaces,
+ existingNamespaces: preflightResult?.existingDocument?._source.namespaces ?? [],
+ };
+ });
- const spacesToEnforce = typesAndSpaces.get(object.type) ?? new Set([namespaceString]); // Always enforce authZ for the active space
- for (const space of object.initialNamespaces ?? []) {
- spacesToEnforce.add(space);
- spacesToAuthorize.add(space);
- }
- typesAndSpaces.set(object.type, spacesToEnforce);
- for (const space of preflightResult?.existingDocument?._source.namespaces ?? []) {
- if (space === ALL_NAMESPACES_STRING) continue; // Don't accidentally check for global privileges when the object exists in '*'
- spacesToAuthorize.add(space); // existing namespaces are included so we can later redact if necessary
- }
- }
-
- const authorizationResult = await this._securityExtension?.performAuthorization({
- // If a user tries to create an object with `initialNamespaces: ['*']`, they need to have 'bulk_create' privileges for the Global
- // Resource (e.g., All privileges for All Spaces).
- // Inversely, if a user tries to overwrite an object that already exists in '*', they don't need to have 'bulk_create' privileges for the Global
- // Resource, so in that case we have to filter out that string from spacesToAuthorize (because `allowGlobalResource: true` is used
- // below.)
- actions: new Set(['bulk_create']),
- types: new Set(typesAndSpaces.keys()),
- spaces: spacesToAuthorize,
- enforceMap: typesAndSpaces,
- auditCallback: (error) => {
- for (const { value } of validObjects) {
- this._securityExtension!.addAuditEvent({
- action: AuditAction.CREATE,
- savedObject: { type: value.object.type, id: value.object.id },
- error,
- ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the create operation has not occurred yet
- });
- }
- },
- options: { allowGlobalResource: true },
+ const authorizationResult = await this._securityExtension?.authorizeBulkCreate({
+ namespace,
+ objects: authObjects,
});
let bulkRequestIndexCounter = 0;
@@ -759,21 +716,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
};
});
- const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
const validObjects = expectedBulkGetResults.filter(isRight);
- const typesAndSpaces = new Map>();
- for (const { value } of validObjects) {
- typesAndSpaces.set(value.type, new Set([namespaceString])); // Always enforce authZ for the active space
- }
-
- await this._securityExtension?.performAuthorization({
- actions: new Set(['bulk_create']),
- types: new Set(typesAndSpaces.keys()),
- spaces: new Set([namespaceString]), // Always check authZ for the active space
- enforceMap: typesAndSpaces,
- // auditCallback is intentionally omitted, this function in the previous Security SOC wrapper implementation
- // did not have audit logging. This is primarily because it is only used by Kibana and is not exposed in a
- // public HTTP API
+ await this._securityExtension?.authorizeCheckConflicts({
+ namespace,
+ objects: validObjects.map((element) => ({ type: element.value.type, id: element.value.id })),
});
const bulkGetDocs = validObjects.map(({ value: { type, id } }) => ({
@@ -841,22 +787,11 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
const { refresh = DEFAULT_REFRESH_SETTING, force } = options;
- const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
- const typesAndSpaces = new Map>([[type, new Set([namespaceString])]]); // Always enforce authZ for the active space
-
- await this._securityExtension?.performAuthorization({
- actions: new Set(['delete']),
- types: new Set([type]),
- spaces: new Set([namespaceString]), // Always check authZ for the active space
- enforceMap: typesAndSpaces,
- auditCallback: (error) => {
- this._securityExtension!.addAuditEvent({
- action: AuditAction.DELETE,
- savedObject: { type, id },
- error,
- ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the delete operation has not occurred yet
- });
- },
+ // we don't need to pass existing namespaces in because we're only concerned with authorizing
+ // the current space. This saves us from performing the preflight check if we're unauthorized
+ await this._securityExtension?.authorizeDelete({
+ namespace,
+ object: { type, id },
});
const rawId = this._serializer.generateRawId(namespace, type, id);
@@ -1123,42 +1058,25 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
force,
});
- // Perform Auth Check (on both L/R, we'll deal with that later)
- const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
- const typesAndSpaces = new Map>();
- const spacesToAuthorize = new Set([namespaceString]); // Always check authZ for the active space
if (this._securityExtension) {
- for (const { value } of expectedBulkDeleteMultiNamespaceDocsResults) {
- const index = (value as { esRequestIndex: number }).esRequestIndex;
- const { type } = value;
- const preflightResult =
- index !== undefined ? multiNamespaceDocsResponse?.body.docs[index] : undefined;
-
- const spacesToEnforce = typesAndSpaces.get(type) ?? new Set([namespaceString]); // Always enforce authZ for the active space
- typesAndSpaces.set(type, spacesToEnforce);
- // @ts-expect-error MultiGetHit._source is optional
- for (const space of preflightResult?._source?.namespaces ?? []) {
- spacesToAuthorize.add(space); // existing namespaces are included
- }
- }
- }
+ // Perform Auth Check (on both L/R, we'll deal with that later)
+ const authObjects: AuthorizeUpdateObject[] = expectedBulkDeleteMultiNamespaceDocsResults.map(
+ (element) => {
+ const index = (element.value as { esRequestIndex: number }).esRequestIndex;
+ const { type, id } = element.value;
+ const preflightResult =
+ index !== undefined ? multiNamespaceDocsResponse?.body.docs[index] : undefined;
- await this._securityExtension?.performAuthorization({
- actions: new Set(['bulk_delete']),
- types: new Set(typesAndSpaces.keys()),
- spaces: spacesToAuthorize,
- enforceMap: typesAndSpaces,
- auditCallback: (error) => {
- for (const { value } of expectedBulkDeleteMultiNamespaceDocsResults) {
- this._securityExtension!.addAuditEvent({
- action: AuditAction.DELETE,
- savedObject: { type: value.type, id: value.id },
- error,
- ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the delete operation has not occurred yet
- });
+ return {
+ type,
+ id,
+ // @ts-expect-error MultiGetHit._source is optional
+ existingNamespaces: preflightResult?._source?.namespaces ?? [],
+ };
}
- },
- });
+ );
+ await this._securityExtension.authorizeBulkDelete({ namespace, objects: authObjects });
+ }
// Filter valid objects
const validObjects = expectedBulkDeleteMultiNamespaceDocsResults.filter(isRight);
@@ -1438,30 +1356,23 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
}
}
- // We have to first do a "pre-authorization" check so that we can construct the search DSL accordingly
- const spacesToPreauthorize = new Set(namespaces);
+ // We have to first perform an initial authorization check so that we can construct the search DSL accordingly
+ const spacesToAuthorize = new Set(namespaces);
+ const typesToAuthorize = new Set(types);
let typeToNamespacesMap: Map | undefined;
- let preAuthorizationResult: CheckAuthorizationResult<'find'> | undefined;
+ let authorizationResult: CheckAuthorizationResult | undefined;
if (!disableExtensions && this._securityExtension) {
- preAuthorizationResult = await this._securityExtension.performAuthorization({
- actions: new Set(['find']),
- types: new Set(types),
- spaces: spacesToPreauthorize,
+ authorizationResult = await this._securityExtension.authorizeFind({
+ namespaces: spacesToAuthorize,
+ types: typesToAuthorize,
});
- if (preAuthorizationResult.status === 'unauthorized') {
+ if (authorizationResult?.status === 'unauthorized') {
// If the user is unauthorized to find *anything* they requested, return an empty response
- this._securityExtension.addAuditEvent({
- action: AuditAction.FIND,
- error: new Error(`User is unauthorized for any requested types/spaces.`),
- // TODO: include object type(s) that were requested?
- // requestedTypes: types,
- // requestedSpaces: namespaces,
- });
return SavedObjectsUtils.createEmptyFindResponse(options);
}
- if (preAuthorizationResult.status === 'partially_authorized') {
+ if (authorizationResult?.status === 'partially_authorized') {
typeToNamespacesMap = new Map();
- for (const [objType, entry] of preAuthorizationResult.typeMap) {
+ for (const [objType, entry] of authorizationResult.typeMap) {
if (!entry.find) continue;
// This ensures that the query DSL can filter only for object types that the user is authorized to access for a given space
const { authorizedSpaces, isGloballyAuthorized } = entry.find;
@@ -1542,30 +1453,22 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
return result;
}
- const spacesToAuthorize = new Set(spacesToPreauthorize); // only for namespace redaction
- for (const { type: objType, id, namespaces: objectNamespaces = [] } of result.saved_objects) {
- for (const space of objectNamespaces) {
- spacesToAuthorize.add(space);
- }
- this._securityExtension?.addAuditEvent({
- action: AuditAction.FIND,
- savedObject: { type: objType, id },
- });
- }
- const authorizationResult =
- spacesToAuthorize.size > spacesToPreauthorize.size
- ? // If there are any namespaces in the object results that were not already checked during pre-authorization, we need *another*
- // authorization check so we can correctly redact the object namespaces below.
- await this._securityExtension?.performAuthorization({
- actions: new Set(['find']),
- types: new Set(types),
- spaces: spacesToAuthorize,
- })
- : undefined;
+ // Now that we have a full set of results with all existing namespaces for each object,
+ // we need an updated authorization type map to pass on to the redact method
+ const redactTypeMap = await this._securityExtension?.getFindRedactTypeMap({
+ previouslyCheckedNamespaces: spacesToAuthorize,
+ objects: result.saved_objects.map((obj) => {
+ return {
+ type: obj.type,
+ id: obj.id,
+ existingNamespaces: obj.namespaces ?? [],
+ };
+ }),
+ });
return this.optionallyDecryptAndRedactBulkResult(
result,
- authorizationResult?.typeMap ?? preAuthorizationResult?.typeMap // If we made a second authorization check, use that one; otherwise, fall back to the pre-authorization check
+ redactTypeMap ?? authorizationResult?.typeMap // If the redact type map is valid, use that one; otherwise, fall back to the authorization check
);
}
@@ -1682,37 +1585,38 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError();
}
- const typesAndSpaces = new Map>();
- const spacesToAuthorize = new Set([SavedObjectsUtils.namespaceIdToString(namespace)]); // Always check authZ for the active space
+ const authObjects: AuthorizeBulkGetObject[] = [];
const result = {
saved_objects: expectedBulkGetResults.map((expectedResult) => {
if (isLeft(expectedResult)) {
+ const { type, id } = expectedResult.value;
+ authObjects.push({ type, id, existingNamespaces: [], error: true });
return expectedResult.value as any;
}
const {
type,
id,
- namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)], // set to default value for `rawDocExistsInNamespaces` check below
+ // set to default namespaces value for `rawDocExistsInNamespaces` check below
+ namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)],
esRequestIndex,
} = expectedResult.value;
- const doc = bulkGetResponse?.body.docs[esRequestIndex];
- const spacesToEnforce =
- typesAndSpaces.get(type) ?? new Set([SavedObjectsUtils.namespaceIdToString(namespace)]); // Always enforce authZ for the active space
- for (const space of namespaces) {
- spacesToEnforce.add(space);
- typesAndSpaces.set(type, spacesToEnforce);
- spacesToAuthorize.add(space);
- }
+ const doc = bulkGetResponse?.body.docs[esRequestIndex];
// @ts-expect-error MultiGetHit._source is optional
- for (const space of doc?._source?.namespaces ?? []) {
- spacesToAuthorize.add(space); // existing namespaces are included so we can later redact if necessary
- }
+ const docNotFound = !doc?.found || !this.rawDocExistsInNamespaces(doc, namespaces);
- // @ts-expect-error MultiGetHit._source is optional
- if (!doc?.found || !this.rawDocExistsInNamespaces(doc, namespaces)) {
+ authObjects.push({
+ type,
+ id,
+ objectNamespaces: namespaces,
+ // @ts-expect-error MultiGetHit._source is optional
+ existingNamespaces: doc?._source?.namespaces ?? [],
+ error: docNotFound,
+ });
+
+ if (docNotFound) {
return {
id,
type,
@@ -1725,21 +1629,9 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
}),
};
- const authorizationResult = await this._securityExtension?.performAuthorization({
- actions: new Set(['bulk_get']),
- types: new Set(typesAndSpaces.keys()),
- spaces: spacesToAuthorize,
- enforceMap: typesAndSpaces,
- auditCallback: (error) => {
- for (const { type, id, error: bulkError } of result.saved_objects) {
- if (!error && !!bulkError) continue; // Only log success events for objects that were actually found (and are being returned to the user)
- this._securityExtension!.addAuditEvent({
- action: AuditAction.GET,
- savedObject: { type, id },
- error,
- });
- }
- },
+ const authorizationResult = await this._securityExtension?.authorizeBulkGet({
+ namespace,
+ objects: authObjects,
});
return this.optionallyDecryptAndRedactBulkResult(result, authorizationResult?.typeMap);
@@ -1768,7 +1660,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
const resolvedObjects = bulkResults.map>((result) => {
// extract payloads from saved object errors
if (isBulkResolveError(result)) {
- const errorResult = result as InternalBulkResolveError;
+ const errorResult = result as BulkResolveError;
const { type, id, error } = errorResult;
return {
saved_object: { type, id, error: errorContent(error) } as unknown as SavedObject,
@@ -1806,39 +1698,23 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id);
}
- const spacesToEnforce = new Set([SavedObjectsUtils.namespaceIdToString(namespace)]); // Always check/enforce authZ for the active space
- const existingNamespaces = body?._source?.namespaces || [];
+ const objectNotFound =
+ !isFoundGetResponse(body) || indexNotFound || !this.rawDocExistsInNamespace(body, namespace);
- const authorizationResult = await this._securityExtension?.performAuthorization({
- actions: new Set(['get']),
- types: new Set([type]),
- spaces: new Set([...spacesToEnforce, ...existingNamespaces]), // existing namespaces are included so we can later redact if necessary
- enforceMap: new Map([[type, spacesToEnforce]]),
- auditCallback: (error) => {
- if (error) {
- this._securityExtension!.addAuditEvent({
- action: AuditAction.GET,
- savedObject: { type, id },
- error,
- });
- }
- // Audit event for success case is added separately below
+ const authorizationResult = await this._securityExtension?.authorizeGet({
+ namespace,
+ object: {
+ type,
+ id,
+ existingNamespaces: body?._source?.namespaces ?? [],
},
+ objectNotFound,
});
- if (
- !isFoundGetResponse(body) ||
- indexNotFound ||
- !this.rawDocExistsInNamespace(body, namespace)
- ) {
+ if (objectNotFound) {
// see "404s from missing index" above
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
- // Only log a success event if the object was actually found (and is being returned to the user)
- this._securityExtension?.addAuditEvent({
- action: AuditAction.GET,
- savedObject: { type, id },
- });
const result = getSavedObjectFromSource(this._registry, type, id, body);
@@ -1908,21 +1784,11 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
});
}
- const spacesToEnforce = new Set([SavedObjectsUtils.namespaceIdToString(namespace)]); // Always check/enforce authZ for the active space
- const existingNamespaces = preflightResult?.savedObjectNamespaces || [];
-
- const authorizationResult = await this._securityExtension?.performAuthorization({
- actions: new Set(['update']),
- types: new Set([type]),
- spaces: new Set([...spacesToEnforce, ...existingNamespaces]), // existing namespaces are included so we can later redact if necessary
- enforceMap: new Map([[type, spacesToEnforce]]),
- auditCallback: (error) =>
- this._securityExtension!.addAuditEvent({
- action: AuditAction.UPDATE,
- savedObject: { type, id },
- error,
- ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the update/upsert operation has not occurred yet
- }),
+ const existingNamespaces = preflightResult?.savedObjectNamespaces ?? [];
+
+ const authorizationResult = await this._securityExtension?.authorizeUpdate({
+ namespace,
+ object: { type, id, existingNamespaces },
});
if (
@@ -2106,9 +1972,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
}
}
- // `objectNamespace` is a namespace string, while `namespace` is a namespace ID.
- // The object namespace string, if defined, will supersede the operation's namespace ID.
-
if (error) {
return {
tag: 'Left',
@@ -2148,6 +2011,8 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
};
}
+ // `objectNamespace` is a namespace string, while `namespace` is a namespace ID.
+ // The object namespace string, if defined, will supersede the operation's namespace ID.
const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
const getNamespaceId = (objectNamespace?: string) =>
objectNamespace !== undefined
@@ -2175,38 +2040,21 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError();
}
- const typesAndSpaces = new Map>();
- const spacesToAuthorize = new Set