From d95c47f776fdcc11c431a2534bd5143cbb92d62d Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Wed, 2 Oct 2019 18:23:44 -0400
Subject: [PATCH] Add KQL functionality in the find function of the saved
objects (#41136)
* Add KQL functionality in the find function of the saved objects
wip
rename variable from KQL to filter, fix unit test + add new ones
miss security pluggins
review I
fix api changes
refactor after reviewing with Rudolf
fix type
review III
review IV
for security put back allowed logic back to return empty results
remove StaticIndexPattern
review V
fix core_api_changes
fix type
* validate filter to match requirement type.attributes.key or type.savedObjectKey
* Fix types
* fix a bug + add more api integration test
* fix types in test until we create package @kbn/types
* fix type issue
* fix api integration test
* export nodeTypes from packages @kbn/es-query instead of the function buildNodeKuery
* throw 400- bad request when validation error in find
* fix type issue
* accept api change
* renove _ to represent private
* fix unit test + add doc
* add comment to explain why we removed the private
---
docs/api/saved-objects/find.asciidoc | 5 +
...a-plugin-public.savedobjectsclient.find.md | 2 +-
...kibana-plugin-public.savedobjectsclient.md | 2 +-
...n-public.savedobjectsfindoptions.filter.md | 11 +
...a-plugin-public.savedobjectsfindoptions.md | 1 +
...n-server.savedobjectsfindoptions.filter.md | 11 +
...a-plugin-server.savedobjectsfindoptions.md | 1 +
packages/kbn-es-query/src/kuery/ast/ast.d.ts | 11 +-
.../kbn-es-query/src/kuery/functions/is.js | 3 +-
packages/kbn-es-query/src/kuery/index.d.ts | 10 +
packages/kbn-es-query/src/kuery/index.js | 2 +-
.../src/kuery/node_types/index.d.ts | 76 ++
.../notifications/notifications_service.ts | 2 +-
src/core/public/public.api.md | 4 +-
.../saved_objects/saved_objects_client.ts | 1 +
.../server/saved_objects/service/index.ts | 1 +
.../service/lib/cache_index_patterns.test.ts | 108 +++
.../service/lib/cache_index_patterns.ts | 82 ++
.../service/lib/filter_utils.test.ts | 457 ++++++++++++
.../saved_objects/service/lib/filter_utils.ts | 190 +++++
.../server/saved_objects/service/lib/index.ts | 2 +
.../service/lib/repository.test.js | 18 +-
.../saved_objects/service/lib/repository.ts | 40 +-
.../lib/search_dsl/query_params.test.ts | 698 +++++++++++++++---
.../service/lib/search_dsl/query_params.ts | 46 +-
.../service/lib/search_dsl/search_dsl.test.ts | 20 +-
.../service/lib/search_dsl/search_dsl.ts | 14 +-
src/core/server/saved_objects/types.ts | 1 +
src/core/server/server.api.md | 3 +
.../core_plugins/elasticsearch/index.d.ts | 2 +-
...create_saved_objects_stream_from_ndjson.ts | 2 +-
.../server/saved_objects/routes/find.ts | 5 +
.../saved_objects/saved_objects_mixin.js | 9 +
.../saved_objects/saved_objects_mixin.test.js | 5 +
.../data/common/field_formats/field_format.ts | 16 +-
.../apis/saved_objects/find.js | 87 +++
test/tsconfig.json | 4 +-
test/typings/index.d.ts | 6 +
.../common/suites/find.ts | 75 ++
.../security_and_spaces/apis/find.ts | 251 +++++++
.../security_only/apis/find.ts | 276 +++++++
.../spaces_only/apis/find.ts | 51 ++
42 files changed, 2459 insertions(+), 152 deletions(-)
create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.filter.md
create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.filter.md
create mode 100644 packages/kbn-es-query/src/kuery/node_types/index.d.ts
create mode 100644 src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts
create mode 100644 src/core/server/saved_objects/service/lib/cache_index_patterns.ts
create mode 100644 src/core/server/saved_objects/service/lib/filter_utils.test.ts
create mode 100644 src/core/server/saved_objects/service/lib/filter_utils.ts
diff --git a/docs/api/saved-objects/find.asciidoc b/docs/api/saved-objects/find.asciidoc
index fd80951b1c9f2..f20ded78e0743 100644
--- a/docs/api/saved-objects/find.asciidoc
+++ b/docs/api/saved-objects/find.asciidoc
@@ -41,6 +41,11 @@ experimental[] Retrieve a paginated set of {kib} saved objects by various condit
`has_reference`::
(Optional, object) Filters to objects that have a relationship with the type and ID combination.
+`filter`::
+ (Optional, string) The filter is a KQL string with the caveat that if you filter with an attribute from your type saved object.
+ It should look like that savedObjectType.attributes.title: "myTitle". However, If you used a direct attribute of a saved object like `updatedAt`,
+ you will have to define your filter like that savedObjectType.updatedAt > 2018-12-22.
+
NOTE: As objects change in {kib}, the results on each page of the response also
change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data.
diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md
index 80ddb1aea18d1..a4fa3f17d0d94 100644
--- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md
+++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md
@@ -9,5 +9,5 @@ Search for objects
Signature:
```typescript
-find: (options: Pick) => Promise>;
+find: (options: Pick) => Promise>;
```
diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md
index 2ad9591426ab2..00a71d25cea38 100644
--- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md
+++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md
@@ -20,7 +20,7 @@ export declare class SavedObjectsClient
| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>>
| Returns an array of objects by id |
| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>>
| Persists an object |
| [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}>
| Deletes an object |
-| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "type" | "defaultSearchOperator" | "searchFields" | "sortField" | "hasReference" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>>
| Search for objects |
+| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>>
| Search for objects |
| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>>
| Fetches a single object |
## Methods
diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.filter.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.filter.md
new file mode 100644
index 0000000000000..82237134e0b22
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.filter.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [filter](./kibana-plugin-public.savedobjectsfindoptions.filter.md)
+
+## SavedObjectsFindOptions.filter property
+
+Signature:
+
+```typescript
+filter?: string;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md
index f90c60ebdd0dc..4c916431d4333 100644
--- a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md
+++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md
@@ -17,6 +17,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions
| --- | --- | --- |
| [defaultSearchOperator](./kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR'
| |
| [fields](./kibana-plugin-public.savedobjectsfindoptions.fields.md) | string[]
| An array of fields to include in the results |
+| [filter](./kibana-plugin-public.savedobjectsfindoptions.filter.md) | string
| |
| [hasReference](./kibana-plugin-public.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
}
| |
| [page](./kibana-plugin-public.savedobjectsfindoptions.page.md) | number
| |
| [perPage](./kibana-plugin-public.savedobjectsfindoptions.perpage.md) | number
| |
diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.filter.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.filter.md
new file mode 100644
index 0000000000000..308bebbeaf60b
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.filter.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [filter](./kibana-plugin-server.savedobjectsfindoptions.filter.md)
+
+## SavedObjectsFindOptions.filter property
+
+Signature:
+
+```typescript
+filter?: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md
index ad81c439d902c..dfd51d480db92 100644
--- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md
+++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md
@@ -17,6 +17,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions
| --- | --- | --- |
| [defaultSearchOperator](./kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR'
| |
| [fields](./kibana-plugin-server.savedobjectsfindoptions.fields.md) | string[]
| An array of fields to include in the results |
+| [filter](./kibana-plugin-server.savedobjectsfindoptions.filter.md) | string
| |
| [hasReference](./kibana-plugin-server.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
}
| |
| [page](./kibana-plugin-server.savedobjectsfindoptions.page.md) | number
| |
| [perPage](./kibana-plugin-server.savedobjectsfindoptions.perpage.md) | number
| |
diff --git a/packages/kbn-es-query/src/kuery/ast/ast.d.ts b/packages/kbn-es-query/src/kuery/ast/ast.d.ts
index 915c024f2ab48..448ef0e9cca75 100644
--- a/packages/kbn-es-query/src/kuery/ast/ast.d.ts
+++ b/packages/kbn-es-query/src/kuery/ast/ast.d.ts
@@ -17,6 +17,8 @@
* under the License.
*/
+import { JsonObject } from '..';
+
/**
* WARNING: these typings are incomplete
*/
@@ -30,15 +32,6 @@ export interface KueryParseOptions {
startRule: string;
}
-type JsonValue = null | boolean | number | string | JsonObject | JsonArray;
-
-interface JsonObject {
- [key: string]: JsonValue;
-}
-
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-interface JsonArray extends Array {}
-
export function fromKueryExpression(
expression: string,
parseOptions?: KueryParseOptions
diff --git a/packages/kbn-es-query/src/kuery/functions/is.js b/packages/kbn-es-query/src/kuery/functions/is.js
index 0338671e9b3fe..690f98b08ba82 100644
--- a/packages/kbn-es-query/src/kuery/functions/is.js
+++ b/packages/kbn-es-query/src/kuery/functions/is.js
@@ -32,7 +32,6 @@ export function buildNodeParams(fieldName, value, isPhrase = false) {
if (_.isUndefined(value)) {
throw new Error('value is a required argument');
}
-
const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName);
const valueNode = typeof value === 'string' ? ast.fromLiteralExpression(value) : literal.buildNode(value);
const isPhraseNode = literal.buildNode(isPhrase);
@@ -42,7 +41,7 @@ export function buildNodeParams(fieldName, value, isPhrase = false) {
}
export function toElasticsearchQuery(node, indexPattern = null, config = {}) {
- const { arguments: [ fieldNameArg, valueArg, isPhraseArg ] } = node;
+ const { arguments: [fieldNameArg, valueArg, isPhraseArg] } = node;
const fieldName = ast.toElasticsearchQuery(fieldNameArg);
const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg;
const type = isPhraseArg.value ? 'phrase' : 'best_fields';
diff --git a/packages/kbn-es-query/src/kuery/index.d.ts b/packages/kbn-es-query/src/kuery/index.d.ts
index 9d797406420d4..b01a8914f68ef 100644
--- a/packages/kbn-es-query/src/kuery/index.d.ts
+++ b/packages/kbn-es-query/src/kuery/index.d.ts
@@ -18,3 +18,13 @@
*/
export * from './ast';
+export { nodeTypes } from './node_types';
+
+export type JsonValue = null | boolean | number | string | JsonObject | JsonArray;
+
+export interface JsonObject {
+ [key: string]: JsonValue;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface JsonArray extends Array {}
diff --git a/packages/kbn-es-query/src/kuery/index.js b/packages/kbn-es-query/src/kuery/index.js
index 84c6a205b42ce..08fa9829d4a56 100644
--- a/packages/kbn-es-query/src/kuery/index.js
+++ b/packages/kbn-es-query/src/kuery/index.js
@@ -19,5 +19,5 @@
export * from './ast';
export * from './filter_migration';
-export * from './node_types';
+export { nodeTypes } from './node_types';
export * from './errors';
diff --git a/packages/kbn-es-query/src/kuery/node_types/index.d.ts b/packages/kbn-es-query/src/kuery/node_types/index.d.ts
new file mode 100644
index 0000000000000..0d1f2c28e39f0
--- /dev/null
+++ b/packages/kbn-es-query/src/kuery/node_types/index.d.ts
@@ -0,0 +1,76 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * WARNING: these typings are incomplete
+ */
+
+import { JsonObject, JsonValue } from '..';
+
+type FunctionName =
+ | 'is'
+ | 'and'
+ | 'or'
+ | 'not'
+ | 'range'
+ | 'exists'
+ | 'geoBoundingBox'
+ | 'geoPolygon';
+
+interface FunctionTypeBuildNode {
+ type: 'function';
+ function: FunctionName;
+ // TODO -> Need to define a better type for DSL query
+ arguments: any[];
+}
+
+interface FunctionType {
+ buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode;
+ buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode;
+ toElasticsearchQuery: (node: any, indexPattern: any, config: JsonObject) => JsonValue;
+}
+
+interface LiteralType {
+ buildNode: (
+ value: null | boolean | number | string
+ ) => { type: 'literal'; value: null | boolean | number | string };
+ toElasticsearchQuery: (node: any) => null | boolean | number | string;
+}
+
+interface NamedArgType {
+ buildNode: (name: string, value: any) => { type: 'namedArg'; name: string; value: any };
+ toElasticsearchQuery: (node: any) => string;
+}
+
+interface WildcardType {
+ buildNode: (value: string) => { type: 'wildcard'; value: string };
+ test: (node: any, string: string) => boolean;
+ toElasticsearchQuery: (node: any) => string;
+ toQueryStringQuery: (node: any) => string;
+ hasLeadingWildcard: (node: any) => boolean;
+}
+
+interface NodeTypes {
+ function: FunctionType;
+ literal: LiteralType;
+ namedArg: NamedArgType;
+ wildcard: WildcardType;
+}
+
+export const nodeTypes: NodeTypes;
diff --git a/src/core/public/notifications/notifications_service.ts b/src/core/public/notifications/notifications_service.ts
index 2dc2b2ef06094..33221522fa83c 100644
--- a/src/core/public/notifications/notifications_service.ts
+++ b/src/core/public/notifications/notifications_service.ts
@@ -48,7 +48,7 @@ export class NotificationsService {
public setup({ uiSettings }: SetupDeps): NotificationsSetup {
const notificationSetup = { toasts: this.toasts.setup({ uiSettings }) };
- this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe(error => {
+ this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe((error: Error) => {
notificationSetup.toasts.addDanger({
title: i18n.translate('core.notifications.unableUpdateUISettingNotificationMessageTitle', {
defaultMessage: 'Unable to update UI setting',
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index b2d730d7fa467..102e77b564a6d 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -752,7 +752,7 @@ export class SavedObjectsClient {
}[]) => Promise>;
create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>;
delete: (type: string, id: string) => Promise<{}>;
- find: (options: Pick) => Promise>;
+ find: (options: Pick) => Promise>;
get: (type: string, id: string) => Promise>;
update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>;
}
@@ -775,6 +775,8 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
defaultSearchOperator?: 'AND' | 'OR';
fields?: string[];
// (undocumented)
+ filter?: string;
+ // (undocumented)
hasReference?: {
type: string;
id: string;
diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts
index dc13d001643a3..cf0300157aece 100644
--- a/src/core/public/saved_objects/saved_objects_client.ts
+++ b/src/core/public/saved_objects/saved_objects_client.ts
@@ -297,6 +297,7 @@ export class SavedObjectsClient {
searchFields: 'search_fields',
sortField: 'sort_field',
type: 'type',
+ filter: 'filter',
};
const renamedQuery = renameKeys(renameMap, options);
diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts
index 685ce51bc7d29..dbf35ff4e134d 100644
--- a/src/core/server/saved_objects/service/index.ts
+++ b/src/core/server/saved_objects/service/index.ts
@@ -56,6 +56,7 @@ export {
SavedObjectsClientWrapperFactory,
SavedObjectsClientWrapperOptions,
SavedObjectsErrorHelpers,
+ SavedObjectsCacheIndexPatterns,
} from './lib';
export * from './saved_objects_client';
diff --git a/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts b/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts
new file mode 100644
index 0000000000000..e3aeca42d1cf0
--- /dev/null
+++ b/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts
@@ -0,0 +1,108 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { SavedObjectsCacheIndexPatterns } from './cache_index_patterns';
+
+const mockGetFieldsForWildcard = jest.fn();
+const mockIndexPatternsService: jest.Mock = jest.fn().mockImplementation(() => ({
+ getFieldsForWildcard: mockGetFieldsForWildcard,
+ getFieldsForTimePattern: jest.fn(),
+}));
+
+describe('SavedObjectsRepository', () => {
+ let cacheIndexPatterns: SavedObjectsCacheIndexPatterns;
+
+ const fields = [
+ {
+ aggregatable: true,
+ name: 'config.type',
+ searchable: true,
+ type: 'string',
+ },
+ {
+ aggregatable: true,
+ name: 'foo.type',
+ searchable: true,
+ type: 'string',
+ },
+ {
+ aggregatable: true,
+ name: 'bar.type',
+ searchable: true,
+ type: 'string',
+ },
+ {
+ aggregatable: true,
+ name: 'baz.type',
+ searchable: true,
+ type: 'string',
+ },
+ {
+ aggregatable: true,
+ name: 'dashboard.otherField',
+ searchable: true,
+ type: 'string',
+ },
+ {
+ aggregatable: true,
+ name: 'hiddenType.someField',
+ searchable: true,
+ type: 'string',
+ },
+ ];
+
+ beforeEach(() => {
+ cacheIndexPatterns = new SavedObjectsCacheIndexPatterns();
+ jest.clearAllMocks();
+ });
+
+ it('setIndexPatterns should return an error object when indexPatternsService is undefined', async () => {
+ try {
+ await cacheIndexPatterns.setIndexPatterns('test-index');
+ } catch (error) {
+ expect(error.message).toMatch('indexPatternsService is not defined');
+ }
+ });
+
+ it('setIndexPatterns should return an error object if getFieldsForWildcard is not defined', async () => {
+ mockGetFieldsForWildcard.mockImplementation(() => {
+ throw new Error('something happen');
+ });
+ try {
+ cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService());
+ await cacheIndexPatterns.setIndexPatterns('test-index');
+ } catch (error) {
+ expect(error.message).toMatch('Index Pattern Error - something happen');
+ }
+ });
+
+ it('setIndexPatterns should return empty array when getFieldsForWildcard is returning null or undefined', async () => {
+ mockGetFieldsForWildcard.mockImplementation(() => null);
+ cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService());
+ await cacheIndexPatterns.setIndexPatterns('test-index');
+ expect(cacheIndexPatterns.getIndexPatterns()).toEqual(undefined);
+ });
+
+ it('setIndexPatterns should return index pattern when getFieldsForWildcard is returning fields', async () => {
+ mockGetFieldsForWildcard.mockImplementation(() => fields);
+ cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService());
+ await cacheIndexPatterns.setIndexPatterns('test-index');
+ expect(cacheIndexPatterns.getIndexPatterns()).toEqual({ fields, title: 'test-index' });
+ });
+});
diff --git a/src/core/server/saved_objects/service/lib/cache_index_patterns.ts b/src/core/server/saved_objects/service/lib/cache_index_patterns.ts
new file mode 100644
index 0000000000000..e96cf996f504c
--- /dev/null
+++ b/src/core/server/saved_objects/service/lib/cache_index_patterns.ts
@@ -0,0 +1,82 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FieldDescriptor } from 'src/legacy/server/index_patterns/service/index_patterns_service';
+import { IndexPatternsService } from 'src/legacy/server/index_patterns';
+
+export interface SavedObjectsIndexPatternField {
+ name: string;
+ type: string;
+ aggregatable: boolean;
+ searchable: boolean;
+}
+
+export interface SavedObjectsIndexPattern {
+ fields: SavedObjectsIndexPatternField[];
+ title: string;
+}
+
+export class SavedObjectsCacheIndexPatterns {
+ private _indexPatterns: SavedObjectsIndexPattern | undefined = undefined;
+ private _indexPatternsService: IndexPatternsService | undefined = undefined;
+
+ public setIndexPatternsService(indexPatternsService: IndexPatternsService) {
+ this._indexPatternsService = indexPatternsService;
+ }
+
+ public getIndexPatternsService() {
+ return this._indexPatternsService;
+ }
+
+ public getIndexPatterns(): SavedObjectsIndexPattern | undefined {
+ return this._indexPatterns;
+ }
+
+ public async setIndexPatterns(index: string) {
+ await this._getIndexPattern(index);
+ }
+
+ private async _getIndexPattern(index: string) {
+ try {
+ if (this._indexPatternsService == null) {
+ throw new TypeError('indexPatternsService is not defined');
+ }
+ const fieldsDescriptor: FieldDescriptor[] = await this._indexPatternsService.getFieldsForWildcard(
+ {
+ pattern: index,
+ }
+ );
+
+ this._indexPatterns =
+ fieldsDescriptor && Array.isArray(fieldsDescriptor) && fieldsDescriptor.length > 0
+ ? {
+ fields: fieldsDescriptor.map(field => ({
+ aggregatable: field.aggregatable,
+ name: field.name,
+ searchable: field.searchable,
+ type: field.type,
+ })),
+ title: index,
+ }
+ : undefined;
+ } catch (err) {
+ throw new Error(`Index Pattern Error - ${err.message}`);
+ }
+ }
+}
diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts
new file mode 100644
index 0000000000000..73a0804512ed1
--- /dev/null
+++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts
@@ -0,0 +1,457 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { fromKueryExpression } from '@kbn/es-query';
+
+import {
+ validateFilterKueryNode,
+ getSavedObjectTypeIndexPatterns,
+ validateConvertFilterToKueryNode,
+} from './filter_utils';
+import { SavedObjectsIndexPattern } from './cache_index_patterns';
+
+const mockIndexPatterns: SavedObjectsIndexPattern = {
+ fields: [
+ {
+ name: 'updatedAt',
+ type: 'date',
+ aggregatable: true,
+ searchable: true,
+ },
+ {
+ name: 'foo.title',
+ type: 'text',
+ aggregatable: true,
+ searchable: true,
+ },
+ {
+ name: 'foo.description',
+ type: 'text',
+ aggregatable: true,
+ searchable: true,
+ },
+ {
+ name: 'foo.bytes',
+ type: 'number',
+ aggregatable: true,
+ searchable: true,
+ },
+ {
+ name: 'bar.foo',
+ type: 'text',
+ aggregatable: true,
+ searchable: true,
+ },
+ {
+ name: 'bar.description',
+ type: 'text',
+ aggregatable: true,
+ searchable: true,
+ },
+ {
+ name: 'hiddentype.description',
+ type: 'text',
+ aggregatable: true,
+ searchable: true,
+ },
+ ],
+ title: 'mock',
+};
+
+describe('Filter Utils', () => {
+ describe('#validateConvertFilterToKueryNode', () => {
+ test('Validate a simple filter', () => {
+ expect(
+ validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockIndexPatterns)
+ ).toEqual(fromKueryExpression('foo.title: "best"'));
+ });
+ test('Assemble filter kuery node saved object attributes with one saved object type', () => {
+ expect(
+ validateConvertFilterToKueryNode(
+ ['foo'],
+ 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)',
+ mockIndexPatterns
+ )
+ ).toEqual(
+ fromKueryExpression(
+ '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)'
+ )
+ );
+ });
+
+ test('Assemble filter with one type kuery node saved object attributes with multiple saved object type', () => {
+ expect(
+ validateConvertFilterToKueryNode(
+ ['foo', 'bar'],
+ 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)',
+ mockIndexPatterns
+ )
+ ).toEqual(
+ fromKueryExpression(
+ '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)'
+ )
+ );
+ });
+
+ test('Assemble filter with two types kuery node saved object attributes with multiple saved object type', () => {
+ expect(
+ validateConvertFilterToKueryNode(
+ ['foo', 'bar'],
+ '(bar.updatedAt: 5678654567 OR foo.updatedAt: 5678654567) and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or bar.attributes.description :*)',
+ mockIndexPatterns
+ )
+ ).toEqual(
+ fromKueryExpression(
+ '((type: bar and updatedAt: 5678654567) or (type: foo and updatedAt: 5678654567)) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or bar.description :*)'
+ )
+ );
+ });
+
+ test('Lets make sure that we are throwing an exception if we get an error', () => {
+ expect(() => {
+ validateConvertFilterToKueryNode(
+ ['foo', 'bar'],
+ 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)',
+ mockIndexPatterns
+ );
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"This key 'updatedAt' need to be wrapped by a saved object type like foo,bar: Bad Request"`
+ );
+ });
+
+ test('Lets make sure that we are throwing an exception if we are using hiddentype with types', () => {
+ expect(() => {
+ validateConvertFilterToKueryNode([], 'hiddentype.title: "title"', mockIndexPatterns);
+ }).toThrowErrorMatchingInlineSnapshot(`"This type hiddentype is not allowed: Bad Request"`);
+ });
+ });
+
+ describe('#validateFilterKueryNode', () => {
+ test('Validate filter query through KueryNode - happy path', () => {
+ const validationObject = validateFilterKueryNode(
+ fromKueryExpression(
+ 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)'
+ ),
+ ['foo'],
+ getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns)
+ );
+
+ expect(validationObject).toEqual([
+ {
+ astPath: 'arguments.0',
+ error: null,
+ isSavedObjectAttr: true,
+ key: 'foo.updatedAt',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.bytes',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.bytes',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.title',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.description',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.description',
+ type: 'foo',
+ },
+ ]);
+ });
+
+ test('Return Error if key is not wrapper by a saved object type', () => {
+ const validationObject = validateFilterKueryNode(
+ fromKueryExpression(
+ 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)'
+ ),
+ ['foo'],
+ getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns)
+ );
+
+ expect(validationObject).toEqual([
+ {
+ astPath: 'arguments.0',
+ error: "This key 'updatedAt' need to be wrapped by a saved object type like foo",
+ isSavedObjectAttr: true,
+ key: 'updatedAt',
+ type: null,
+ },
+ {
+ astPath: 'arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.bytes',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.bytes',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.title',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.description',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.description',
+ type: 'foo',
+ },
+ ]);
+ });
+
+ test('Return Error if key of a saved object type is not wrapped with attributes', () => {
+ const validationObject = validateFilterKueryNode(
+ fromKueryExpression(
+ 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)'
+ ),
+ ['foo'],
+ getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns)
+ );
+
+ expect(validationObject).toEqual([
+ {
+ astPath: 'arguments.0',
+ error: null,
+ isSavedObjectAttr: true,
+ key: 'foo.updatedAt',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.bytes',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.0',
+ error:
+ "This key 'foo.bytes' does NOT match the filter proposition SavedObjectType.attributes.key",
+ isSavedObjectAttr: false,
+ key: 'foo.bytes',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.title',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.description',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1',
+ error:
+ "This key 'foo.description' does NOT match the filter proposition SavedObjectType.attributes.key",
+ isSavedObjectAttr: false,
+ key: 'foo.description',
+ type: 'foo',
+ },
+ ]);
+ });
+
+ test('Return Error if filter is not using an allowed type', () => {
+ const validationObject = validateFilterKueryNode(
+ fromKueryExpression(
+ 'bar.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)'
+ ),
+ ['foo'],
+ getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns)
+ );
+
+ expect(validationObject).toEqual([
+ {
+ astPath: 'arguments.0',
+ error: 'This type bar is not allowed',
+ isSavedObjectAttr: true,
+ key: 'bar.updatedAt',
+ type: 'bar',
+ },
+ {
+ astPath: 'arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.bytes',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.bytes',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.title',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.description',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.description',
+ type: 'foo',
+ },
+ ]);
+ });
+
+ test('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => {
+ const validationObject = validateFilterKueryNode(
+ fromKueryExpression(
+ 'foo.updatedAt33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)'
+ ),
+ ['foo'],
+ getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns)
+ );
+
+ expect(validationObject).toEqual([
+ {
+ astPath: 'arguments.0',
+ error: "This key 'foo.updatedAt33' does NOT exist in foo saved object index patterns",
+ isSavedObjectAttr: false,
+ key: 'foo.updatedAt33',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.bytes',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.bytes',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.0',
+ error:
+ "This key 'foo.attributes.header' does NOT exist in foo saved object index patterns",
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.header',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.description',
+ type: 'foo',
+ },
+ {
+ astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1',
+ error: null,
+ isSavedObjectAttr: false,
+ key: 'foo.attributes.description',
+ type: 'foo',
+ },
+ ]);
+ });
+ });
+
+ describe('#getSavedObjectTypeIndexPatterns', () => {
+ test('Get index patterns related to your type', () => {
+ const indexPatternsFilterByType = getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns);
+
+ expect(indexPatternsFilterByType).toEqual([
+ {
+ name: 'updatedAt',
+ type: 'date',
+ aggregatable: true,
+ searchable: true,
+ },
+ {
+ name: 'foo.title',
+ type: 'text',
+ aggregatable: true,
+ searchable: true,
+ },
+ {
+ name: 'foo.description',
+ type: 'text',
+ aggregatable: true,
+ searchable: true,
+ },
+ {
+ name: 'foo.bytes',
+ type: 'number',
+ aggregatable: true,
+ searchable: true,
+ },
+ ]);
+ });
+ });
+});
diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts
new file mode 100644
index 0000000000000..2397971e66966
--- /dev/null
+++ b/src/core/server/saved_objects/service/lib/filter_utils.ts
@@ -0,0 +1,190 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { fromKueryExpression, KueryNode, nodeTypes } from '@kbn/es-query';
+import { get, set } from 'lodash';
+
+import { SavedObjectsIndexPattern, SavedObjectsIndexPatternField } from './cache_index_patterns';
+import { SavedObjectsErrorHelpers } from './errors';
+
+export const validateConvertFilterToKueryNode = (
+ types: string[],
+ filter: string,
+ indexPattern: SavedObjectsIndexPattern | undefined
+): KueryNode => {
+ if (filter && filter.length > 0 && indexPattern) {
+ const filterKueryNode = fromKueryExpression(filter);
+
+ const typeIndexPatterns = getSavedObjectTypeIndexPatterns(types, indexPattern);
+ const validationFilterKuery = validateFilterKueryNode(
+ filterKueryNode,
+ types,
+ typeIndexPatterns,
+ filterKueryNode.type === 'function' && ['is', 'range'].includes(filterKueryNode.function)
+ );
+
+ if (validationFilterKuery.length === 0) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ 'If we have a filter options defined, we should always have validationFilterKuery defined too'
+ );
+ }
+
+ if (validationFilterKuery.some(obj => obj.error != null)) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ validationFilterKuery
+ .filter(obj => obj.error != null)
+ .map(obj => obj.error)
+ .join('\n')
+ );
+ }
+
+ validationFilterKuery.forEach(item => {
+ const path: string[] = item.astPath.length === 0 ? [] : item.astPath.split('.');
+ const existingKueryNode: KueryNode =
+ path.length === 0 ? filterKueryNode : get(filterKueryNode, path);
+ if (item.isSavedObjectAttr) {
+ existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1];
+ const itemType = types.filter(t => t === item.type);
+ if (itemType.length === 1) {
+ set(
+ filterKueryNode,
+ path,
+ nodeTypes.function.buildNode('and', [
+ nodeTypes.function.buildNode('is', 'type', itemType[0]),
+ existingKueryNode,
+ ])
+ );
+ }
+ } else {
+ existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.replace(
+ '.attributes',
+ ''
+ );
+ set(filterKueryNode, path, existingKueryNode);
+ }
+ });
+ return filterKueryNode;
+ }
+ return null;
+};
+
+export const getSavedObjectTypeIndexPatterns = (
+ types: string[],
+ indexPattern: SavedObjectsIndexPattern | undefined
+): SavedObjectsIndexPatternField[] => {
+ return indexPattern != null
+ ? indexPattern.fields.filter(
+ ip =>
+ !ip.name.includes('.') || (ip.name.includes('.') && types.includes(ip.name.split('.')[0]))
+ )
+ : [];
+};
+
+interface ValidateFilterKueryNode {
+ astPath: string;
+ error: string;
+ isSavedObjectAttr: boolean;
+ key: string;
+ type: string | null;
+}
+
+export const validateFilterKueryNode = (
+ astFilter: KueryNode,
+ types: string[],
+ typeIndexPatterns: SavedObjectsIndexPatternField[],
+ storeValue: boolean = false,
+ path: string = 'arguments'
+): ValidateFilterKueryNode[] => {
+ return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => {
+ if (ast.arguments) {
+ const myPath = `${path}.${index}`;
+ return [
+ ...kueryNode,
+ ...validateFilterKueryNode(
+ ast,
+ types,
+ typeIndexPatterns,
+ ast.type === 'function' && ['is', 'range'].includes(ast.function),
+ `${myPath}.arguments`
+ ),
+ ];
+ }
+ if (storeValue && index === 0) {
+ const splitPath = path.split('.');
+ return [
+ ...kueryNode,
+ {
+ astPath: splitPath.slice(0, splitPath.length - 1).join('.'),
+ error: hasFilterKeyError(ast.value, types, typeIndexPatterns),
+ isSavedObjectAttr: isSavedObjectAttr(ast.value, typeIndexPatterns),
+ key: ast.value,
+ type: getType(ast.value),
+ },
+ ];
+ }
+ return kueryNode;
+ }, []);
+};
+
+const getType = (key: string) => (key.includes('.') ? key.split('.')[0] : null);
+
+export const isSavedObjectAttr = (
+ key: string,
+ typeIndexPatterns: SavedObjectsIndexPatternField[]
+) => {
+ const splitKey = key.split('.');
+ if (splitKey.length === 1 && typeIndexPatterns.some(tip => tip.name === splitKey[0])) {
+ return true;
+ } else if (splitKey.length > 1 && typeIndexPatterns.some(tip => tip.name === splitKey[1])) {
+ return true;
+ }
+ return false;
+};
+
+export const hasFilterKeyError = (
+ key: string,
+ types: string[],
+ typeIndexPatterns: SavedObjectsIndexPatternField[]
+): string | null => {
+ if (!key.includes('.')) {
+ return `This key '${key}' need to be wrapped by a saved object type like ${types.join()}`;
+ } else if (key.includes('.')) {
+ const keySplit = key.split('.');
+ if (keySplit.length <= 1 || !types.includes(keySplit[0])) {
+ return `This type ${keySplit[0]} is not allowed`;
+ }
+ if (
+ (keySplit.length === 2 && typeIndexPatterns.some(tip => tip.name === key)) ||
+ (keySplit.length > 2 && types.includes(keySplit[0]) && keySplit[1] !== 'attributes')
+ ) {
+ return `This key '${key}' does NOT match the filter proposition SavedObjectType.attributes.key`;
+ }
+ if (
+ (keySplit.length === 2 && !typeIndexPatterns.some(tip => tip.name === keySplit[1])) ||
+ (keySplit.length > 2 &&
+ !typeIndexPatterns.some(
+ tip =>
+ tip.name === [...keySplit.slice(0, 1), ...keySplit.slice(2, keySplit.length)].join('.')
+ ))
+ ) {
+ return `This key '${key}' does NOT exist in ${types.join()} saved object index patterns`;
+ }
+ }
+ return null;
+};
diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts
index d987737c2ffa0..be78fdde76210 100644
--- a/src/core/server/saved_objects/service/lib/index.ts
+++ b/src/core/server/saved_objects/service/lib/index.ts
@@ -26,3 +26,5 @@ export {
} from './scoped_client_provider';
export { SavedObjectsErrorHelpers } from './errors';
+
+export { SavedObjectsCacheIndexPatterns } from './cache_index_patterns';
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index c35502b719d58..bc646c8c1d2e1 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -18,6 +18,7 @@
*/
import { delay } from 'bluebird';
+
import { SavedObjectsRepository } from './repository';
import * as getSearchDslNS from './search_dsl/search_dsl';
import { SavedObjectsErrorHelpers } from './errors';
@@ -272,6 +273,10 @@ describe('SavedObjectsRepository', () => {
savedObjectsRepository = new SavedObjectsRepository({
index: '.kibana-test',
+ cacheIndexPatterns: {
+ setIndexPatterns: jest.fn(),
+ getIndexPatterns: () => undefined,
+ },
mappings,
callCluster: callAdminCluster,
migrator,
@@ -285,7 +290,7 @@ describe('SavedObjectsRepository', () => {
getSearchDslNS.getSearchDsl.mockReset();
});
- afterEach(() => {});
+ afterEach(() => { });
describe('#create', () => {
beforeEach(() => {
@@ -993,7 +998,7 @@ describe('SavedObjectsRepository', () => {
expect(onBeforeWrite).toHaveBeenCalledTimes(1);
});
- it('should return objects in the same order regardless of type', () => {});
+ it('should return objects in the same order regardless of type', () => { });
});
describe('#delete', () => {
@@ -1154,6 +1159,13 @@ describe('SavedObjectsRepository', () => {
}
});
+ it('requires index pattern to be defined if filter is defined', async () => {
+ callAdminCluster.mockReturnValue(noNamespaceSearchResults);
+ expect(savedObjectsRepository.find({ type: 'foo', filter: 'foo.type: hello' }))
+ .rejects
+ .toThrowErrorMatchingInlineSnapshot('"options.filter is missing index pattern to work correctly: Bad Request"');
+ });
+
it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl',
async () => {
callAdminCluster.mockReturnValue(namespacedSearchResults);
@@ -1169,6 +1181,8 @@ describe('SavedObjectsRepository', () => {
type: 'foo',
id: '1',
},
+ indexPattern: undefined,
+ kueryNode: null,
};
await savedObjectsRepository.find(relevantOpts);
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index 3c2a644f003bd..aadb82486ccce 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -19,11 +19,13 @@
import { omit } from 'lodash';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
+
import { getRootPropertiesObjects, IndexMapping } from '../../mappings';
import { getSearchDsl } from './search_dsl';
import { includedFields } from './included_fields';
import { decorateEsError } from './decorate_es_error';
import { SavedObjectsErrorHelpers } from './errors';
+import { SavedObjectsCacheIndexPatterns } from './cache_index_patterns';
import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version';
import { SavedObjectsSchema } from '../../schema';
import { KibanaMigrator } from '../../migrations';
@@ -45,6 +47,7 @@ import {
SavedObjectsFindOptions,
SavedObjectsMigrationVersion,
} from '../../types';
+import { validateConvertFilterToKueryNode } from './filter_utils';
// 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.
@@ -74,6 +77,7 @@ export interface SavedObjectsRepositoryOptions {
serializer: SavedObjectsSerializer;
migrator: KibanaMigrator;
allowedTypes: string[];
+ cacheIndexPatterns: SavedObjectsCacheIndexPatterns;
onBeforeWrite?: (...args: Parameters) => Promise;
}
@@ -91,11 +95,13 @@ export class SavedObjectsRepository {
private _onBeforeWrite: (...args: Parameters) => Promise;
private _unwrappedCallCluster: CallCluster;
private _serializer: SavedObjectsSerializer;
+ private _cacheIndexPatterns: SavedObjectsCacheIndexPatterns;
constructor(options: SavedObjectsRepositoryOptions) {
const {
index,
config,
+ cacheIndexPatterns,
mappings,
callCluster,
schema,
@@ -106,7 +112,7 @@ export class SavedObjectsRepository {
} = options;
// It's important that we migrate documents / mark them as up-to-date
- // prior to writing them to the index. Otherwise, we'll cause unecessary
+ // prior to writing them to the index. Otherwise, we'll cause unnecessary
// index migrations to run at Kibana startup, and those will probably fail
// due to invalidly versioned documents in the index.
//
@@ -117,6 +123,7 @@ export class SavedObjectsRepository {
this._config = config;
this._mappings = mappings;
this._schema = schema;
+ this._cacheIndexPatterns = cacheIndexPatterns;
if (allowedTypes.length === 0) {
throw new Error('Empty or missing types for saved object repository!');
}
@@ -126,6 +133,9 @@ export class SavedObjectsRepository {
this._unwrappedCallCluster = async (...args: Parameters) => {
await migrator.runMigrations();
+ if (this._cacheIndexPatterns.getIndexPatterns() == null) {
+ await this._cacheIndexPatterns.setIndexPatterns(index);
+ }
return callCluster(...args);
};
this._schema = schema;
@@ -404,9 +414,12 @@ export class SavedObjectsRepository {
fields,
namespace,
type,
+ filter,
}: SavedObjectsFindOptions): Promise> {
if (!type) {
- throw new TypeError(`options.type must be a string or an array of strings`);
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ 'options.type must be a string or an array of strings'
+ );
}
const types = Array.isArray(type) ? type : [type];
@@ -421,13 +434,28 @@ export class SavedObjectsRepository {
}
if (searchFields && !Array.isArray(searchFields)) {
- throw new TypeError('options.searchFields must be an array');
+ throw SavedObjectsErrorHelpers.createBadRequestError('options.searchFields must be an array');
}
if (fields && !Array.isArray(fields)) {
- throw new TypeError('options.fields must be an array');
+ throw SavedObjectsErrorHelpers.createBadRequestError('options.fields must be an array');
}
+ if (filter && filter !== '' && this._cacheIndexPatterns.getIndexPatterns() == null) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ 'options.filter is missing index pattern to work correctly'
+ );
+ }
+
+ const kueryNode =
+ filter && filter !== ''
+ ? validateConvertFilterToKueryNode(
+ allowedTypes,
+ filter,
+ this._cacheIndexPatterns.getIndexPatterns()
+ )
+ : null;
+
const esOptions = {
index: this.getIndicesForTypes(allowedTypes),
size: perPage,
@@ -446,6 +474,8 @@ export class SavedObjectsRepository {
sortOrder,
namespace,
hasReference,
+ indexPattern: kueryNode != null ? this._cacheIndexPatterns.getIndexPatterns() : undefined,
+ kueryNode,
}),
},
};
@@ -769,7 +799,7 @@ export class SavedObjectsRepository {
// The internal representation of the saved object that the serializer returns
// includes the namespace, and we use this for migrating documents. However, we don't
- // want the namespcae to be returned from the repository, as the repository scopes each
+ // want the namespace to be returned from the repository, as the repository scopes each
// method transparently to the specified namespace.
private _rawToSavedObject(raw: RawDoc): SavedObject {
const savedObject = this._serializer.rawToSavedObject(raw);
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
index b13d86819716b..75b3058029227 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
@@ -18,6 +18,7 @@
*/
import { schemaMock } from '../../../schema/schema.mock';
+import { SavedObjectsIndexPattern } from '../cache_index_patterns';
import { getQueryParams } from './query_params';
const SCHEMA = schemaMock.create();
@@ -61,6 +62,41 @@ const MAPPINGS = {
},
},
};
+const INDEX_PATTERN: SavedObjectsIndexPattern = {
+ fields: [
+ {
+ aggregatable: true,
+ name: 'type',
+ searchable: true,
+ type: 'string',
+ },
+ {
+ aggregatable: true,
+ name: 'pending.title',
+ searchable: true,
+ type: 'string',
+ },
+ {
+ aggregatable: true,
+ name: 'saved.title',
+ searchable: true,
+ type: 'string',
+ },
+ {
+ aggregatable: true,
+ name: 'saved.obj.key1',
+ searchable: true,
+ type: 'string',
+ },
+ {
+ aggregatable: true,
+ name: 'global.name',
+ searchable: true,
+ type: 'string',
+ },
+ ],
+ title: 'test',
+};
// create a type clause to be used within the "should", if a namespace is specified
// the clause will ensure the namespace matches; otherwise, the clause will ensure
@@ -85,7 +121,7 @@ const createTypeClause = (type: string, namespace?: string) => {
describe('searchDsl/queryParams', () => {
describe('no parameters', () => {
it('searches for all known types without a namespace specified', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA)).toEqual({
+ expect(getQueryParams({ mappings: MAPPINGS, schema: SCHEMA })).toEqual({
query: {
bool: {
filter: [
@@ -108,7 +144,9 @@ describe('searchDsl/queryParams', () => {
describe('namespace', () => {
it('filters namespaced types for namespace, and ensures namespace agnostic types have no namespace', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace')).toEqual({
+ expect(
+ getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: 'foo-namespace' })
+ ).toEqual({
query: {
bool: {
filter: [
@@ -131,7 +169,9 @@ describe('searchDsl/queryParams', () => {
describe('type (singular, namespaced)', () => {
it('includes a terms filter for type and namespace not being specified', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, undefined, 'saved')).toEqual({
+ expect(
+ getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: undefined, type: 'saved' })
+ ).toEqual({
query: {
bool: {
filter: [
@@ -150,7 +190,9 @@ describe('searchDsl/queryParams', () => {
describe('type (singular, global)', () => {
it('includes a terms filter for type and namespace not being specified', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, undefined, 'global')).toEqual({
+ expect(
+ getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: undefined, type: 'global' })
+ ).toEqual({
query: {
bool: {
filter: [
@@ -169,7 +211,14 @@ describe('searchDsl/queryParams', () => {
describe('type (plural, namespaced and global)', () => {
it('includes term filters for types and namespace not being specified', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'])).toEqual({
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: undefined,
+ type: ['saved', 'global'],
+ })
+ ).toEqual({
query: {
bool: {
filter: [
@@ -188,7 +237,14 @@ describe('searchDsl/queryParams', () => {
describe('namespace, type (plural, namespaced and global)', () => {
it('includes a terms filter for type and namespace not being specified', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'])).toEqual({
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: ['saved', 'global'],
+ })
+ ).toEqual({
query: {
bool: {
filter: [
@@ -207,7 +263,15 @@ describe('searchDsl/queryParams', () => {
describe('search', () => {
it('includes a sqs query and all known types without a namespace specified', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'us*')).toEqual({
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: undefined,
+ type: undefined,
+ search: 'us*',
+ })
+ ).toEqual({
query: {
bool: {
filter: [
@@ -239,7 +303,15 @@ describe('searchDsl/queryParams', () => {
describe('namespace, search', () => {
it('includes a sqs query and namespaced types with the namespace and global types without a namespace', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'us*')).toEqual({
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: undefined,
+ search: 'us*',
+ })
+ ).toEqual({
query: {
bool: {
filter: [
@@ -271,7 +343,15 @@ describe('searchDsl/queryParams', () => {
describe('type (plural, namespaced and global), search', () => {
it('includes a sqs query and types without a namespace', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'us*')).toEqual({
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: undefined,
+ type: ['saved', 'global'],
+ search: 'us*',
+ })
+ ).toEqual({
query: {
bool: {
filter: [
@@ -299,40 +379,52 @@ describe('searchDsl/queryParams', () => {
describe('namespace, type (plural, namespaced and global), search', () => {
it('includes a sqs query and namespace type with a namespace and global type without a namespace', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'us*')).toEqual(
- {
- query: {
- bool: {
- filter: [
- {
- bool: {
- should: [
- createTypeClause('saved', 'foo-namespace'),
- createTypeClause('global'),
- ],
- minimum_should_match: 1,
- },
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: ['saved', 'global'],
+ search: 'us*',
+ })
+ ).toEqual({
+ query: {
+ bool: {
+ filter: [
+ {
+ bool: {
+ should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')],
+ minimum_should_match: 1,
},
- ],
- must: [
- {
- simple_query_string: {
- query: 'us*',
- lenient: true,
- fields: ['*'],
- },
+ },
+ ],
+ must: [
+ {
+ simple_query_string: {
+ query: 'us*',
+ lenient: true,
+ fields: ['*'],
},
- ],
- },
+ },
+ ],
},
- }
- );
+ },
+ });
});
});
describe('search, searchFields', () => {
it('includes all types for field', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title'])).toEqual({
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: undefined,
+ type: undefined,
+ search: 'y*',
+ searchFields: ['title'],
+ })
+ ).toEqual({
query: {
bool: {
filter: [
@@ -360,7 +452,16 @@ describe('searchDsl/queryParams', () => {
});
});
it('supports field boosting', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title^3'])).toEqual({
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: undefined,
+ type: undefined,
+ search: 'y*',
+ searchFields: ['title^3'],
+ })
+ ).toEqual({
query: {
bool: {
filter: [
@@ -389,7 +490,14 @@ describe('searchDsl/queryParams', () => {
});
it('supports field and multi-field', () => {
expect(
- getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title', 'title.raw'])
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: undefined,
+ type: undefined,
+ search: 'y*',
+ searchFields: ['title', 'title.raw'],
+ })
).toEqual({
query: {
bool: {
@@ -428,38 +536,52 @@ describe('searchDsl/queryParams', () => {
describe('namespace, search, searchFields', () => {
it('includes all types for field', () => {
- expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title'])).toEqual(
- {
- query: {
- bool: {
- filter: [
- {
- bool: {
- should: [
- createTypeClause('pending', 'foo-namespace'),
- createTypeClause('saved', 'foo-namespace'),
- createTypeClause('global'),
- ],
- minimum_should_match: 1,
- },
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: undefined,
+ search: 'y*',
+ searchFields: ['title'],
+ })
+ ).toEqual({
+ query: {
+ bool: {
+ filter: [
+ {
+ bool: {
+ should: [
+ createTypeClause('pending', 'foo-namespace'),
+ createTypeClause('saved', 'foo-namespace'),
+ createTypeClause('global'),
+ ],
+ minimum_should_match: 1,
},
- ],
- must: [
- {
- simple_query_string: {
- query: 'y*',
- fields: ['pending.title', 'saved.title', 'global.title'],
- },
+ },
+ ],
+ must: [
+ {
+ simple_query_string: {
+ query: 'y*',
+ fields: ['pending.title', 'saved.title', 'global.title'],
},
- ],
- },
+ },
+ ],
},
- }
- );
+ },
+ });
});
it('supports field boosting', () => {
expect(
- getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title^3'])
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: undefined,
+ search: 'y*',
+ searchFields: ['title^3'],
+ })
).toEqual({
query: {
bool: {
@@ -489,7 +611,14 @@ describe('searchDsl/queryParams', () => {
});
it('supports field and multi-field', () => {
expect(
- getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title', 'title.raw'])
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: undefined,
+ search: 'y*',
+ searchFields: ['title', 'title.raw'],
+ })
).toEqual({
query: {
bool: {
@@ -529,7 +658,14 @@ describe('searchDsl/queryParams', () => {
describe('type (plural, namespaced and global), search, searchFields', () => {
it('includes all types for field', () => {
expect(
- getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', ['title'])
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: undefined,
+ type: ['saved', 'global'],
+ search: 'y*',
+ searchFields: ['title'],
+ })
).toEqual({
query: {
bool: {
@@ -555,7 +691,14 @@ describe('searchDsl/queryParams', () => {
});
it('supports field boosting', () => {
expect(
- getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', ['title^3'])
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: undefined,
+ type: ['saved', 'global'],
+ search: 'y*',
+ searchFields: ['title^3'],
+ })
).toEqual({
query: {
bool: {
@@ -581,10 +724,14 @@ describe('searchDsl/queryParams', () => {
});
it('supports field and multi-field', () => {
expect(
- getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', [
- 'title',
- 'title.raw',
- ])
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: undefined,
+ type: ['saved', 'global'],
+ search: 'y*',
+ searchFields: ['title', 'title.raw'],
+ })
).toEqual({
query: {
bool: {
@@ -613,7 +760,14 @@ describe('searchDsl/queryParams', () => {
describe('namespace, type (plural, namespaced and global), search, searchFields', () => {
it('includes all types for field', () => {
expect(
- getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title'])
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: ['saved', 'global'],
+ search: 'y*',
+ searchFields: ['title'],
+ })
).toEqual({
query: {
bool: {
@@ -639,7 +793,14 @@ describe('searchDsl/queryParams', () => {
});
it('supports field boosting', () => {
expect(
- getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title^3'])
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: ['saved', 'global'],
+ search: 'y*',
+ searchFields: ['title^3'],
+ })
).toEqual({
query: {
bool: {
@@ -665,10 +826,14 @@ describe('searchDsl/queryParams', () => {
});
it('supports field and multi-field', () => {
expect(
- getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', [
- 'title',
- 'title.raw',
- ])
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: ['saved', 'global'],
+ search: 'y*',
+ searchFields: ['title', 'title.raw'],
+ })
).toEqual({
query: {
bool: {
@@ -697,15 +862,15 @@ describe('searchDsl/queryParams', () => {
describe('type (plural, namespaced and global), search, defaultSearchOperator', () => {
it('supports defaultSearchOperator', () => {
expect(
- getQueryParams(
- MAPPINGS,
- SCHEMA,
- 'foo-namespace',
- ['saved', 'global'],
- 'foo',
- undefined,
- 'AND'
- )
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: ['saved', 'global'],
+ search: 'foo',
+ searchFields: undefined,
+ defaultSearchOperator: 'AND',
+ })
).toEqual({
query: {
bool: {
@@ -771,19 +936,19 @@ describe('searchDsl/queryParams', () => {
describe('type (plural, namespaced and global), hasReference', () => {
it('supports hasReference', () => {
expect(
- getQueryParams(
- MAPPINGS,
- SCHEMA,
- 'foo-namespace',
- ['saved', 'global'],
- undefined,
- undefined,
- 'OR',
- {
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ type: ['saved', 'global'],
+ search: undefined,
+ searchFields: undefined,
+ defaultSearchOperator: 'OR',
+ hasReference: {
type: 'bar',
id: '1',
- }
- )
+ },
+ })
).toEqual({
query: {
bool: {
@@ -823,4 +988,345 @@ describe('searchDsl/queryParams', () => {
});
});
});
+
+ describe('type filter', () => {
+ it(' with namespace', () => {
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ kueryNode: {
+ type: 'function',
+ function: 'is',
+ arguments: [
+ { type: 'literal', value: 'global.name' },
+ { type: 'literal', value: 'GLOBAL' },
+ { type: 'literal', value: false },
+ ],
+ },
+ indexPattern: INDEX_PATTERN,
+ })
+ ).toEqual({
+ query: {
+ bool: {
+ filter: [
+ {
+ bool: {
+ should: [
+ {
+ match: {
+ 'global.name': 'GLOBAL',
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ should: [
+ {
+ bool: {
+ must: [
+ {
+ term: {
+ type: 'pending',
+ },
+ },
+ {
+ term: {
+ namespace: 'foo-namespace',
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ must: [
+ {
+ term: {
+ type: 'saved',
+ },
+ },
+ {
+ term: {
+ namespace: 'foo-namespace',
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ must: [
+ {
+ term: {
+ type: 'global',
+ },
+ },
+ ],
+ must_not: [
+ {
+ exists: {
+ field: 'namespace',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+ it(' with namespace and more complex filter', () => {
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ kueryNode: {
+ type: 'function',
+ function: 'and',
+ arguments: [
+ {
+ type: 'function',
+ function: 'is',
+ arguments: [
+ { type: 'literal', value: 'global.name' },
+ { type: 'literal', value: 'GLOBAL' },
+ { type: 'literal', value: false },
+ ],
+ },
+ {
+ type: 'function',
+ function: 'not',
+ arguments: [
+ {
+ type: 'function',
+ function: 'is',
+ arguments: [
+ { type: 'literal', value: 'saved.obj.key1' },
+ { type: 'literal', value: 'key' },
+ { type: 'literal', value: true },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ indexPattern: INDEX_PATTERN,
+ })
+ ).toEqual({
+ query: {
+ bool: {
+ filter: [
+ {
+ bool: {
+ filter: [
+ {
+ bool: {
+ should: [
+ {
+ match: {
+ 'global.name': 'GLOBAL',
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ must_not: {
+ bool: {
+ should: [
+ {
+ match_phrase: {
+ 'saved.obj.key1': 'key',
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ should: [
+ {
+ bool: {
+ must: [
+ {
+ term: {
+ type: 'pending',
+ },
+ },
+ {
+ term: {
+ namespace: 'foo-namespace',
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ must: [
+ {
+ term: {
+ type: 'saved',
+ },
+ },
+ {
+ term: {
+ namespace: 'foo-namespace',
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ must: [
+ {
+ term: {
+ type: 'global',
+ },
+ },
+ ],
+ must_not: [
+ {
+ exists: {
+ field: 'namespace',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+ it(' with search and searchFields', () => {
+ expect(
+ getQueryParams({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: 'foo-namespace',
+ search: 'y*',
+ searchFields: ['title'],
+ kueryNode: {
+ type: 'function',
+ function: 'is',
+ arguments: [
+ { type: 'literal', value: 'global.name' },
+ { type: 'literal', value: 'GLOBAL' },
+ { type: 'literal', value: false },
+ ],
+ },
+ indexPattern: INDEX_PATTERN,
+ })
+ ).toEqual({
+ query: {
+ bool: {
+ filter: [
+ {
+ bool: {
+ should: [
+ {
+ match: {
+ 'global.name': 'GLOBAL',
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ should: [
+ {
+ bool: {
+ must: [
+ {
+ term: {
+ type: 'pending',
+ },
+ },
+ {
+ term: {
+ namespace: 'foo-namespace',
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ must: [
+ {
+ term: {
+ type: 'saved',
+ },
+ },
+ {
+ term: {
+ namespace: 'foo-namespace',
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ must: [
+ {
+ term: {
+ type: 'global',
+ },
+ },
+ ],
+ must_not: [
+ {
+ exists: {
+ field: 'namespace',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ must: [
+ {
+ simple_query_string: {
+ query: 'y*',
+ fields: ['pending.title', 'saved.title', 'global.title'],
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+ });
});
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
index 9c145258a755f..125b0c40af9e4 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
@@ -16,9 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { toElasticsearchQuery, KueryNode } from '@kbn/es-query';
import { getRootPropertiesObjects, IndexMapping } from '../../../mappings';
import { SavedObjectsSchema } from '../../../schema';
+import { SavedObjectsIndexPattern } from '../cache_index_patterns';
/**
* Gets the types based on the type. Uses mappings to support
@@ -76,25 +78,43 @@ function getClauseForType(schema: SavedObjectsSchema, namespace: string | undefi
};
}
+interface HasReferenceQueryParams {
+ type: string;
+ id: string;
+}
+
+interface QueryParams {
+ mappings: IndexMapping;
+ schema: SavedObjectsSchema;
+ namespace?: string;
+ type?: string | string[];
+ search?: string;
+ searchFields?: string[];
+ defaultSearchOperator?: string;
+ hasReference?: HasReferenceQueryParams;
+ kueryNode?: KueryNode;
+ indexPattern?: SavedObjectsIndexPattern;
+}
+
/**
* Get the "query" related keys for the search body
*/
-export function getQueryParams(
- mappings: IndexMapping,
- schema: SavedObjectsSchema,
- namespace?: string,
- type?: string | string[],
- search?: string,
- searchFields?: string[],
- defaultSearchOperator?: string,
- hasReference?: {
- type: string;
- id: string;
- }
-) {
+export function getQueryParams({
+ mappings,
+ schema,
+ namespace,
+ type,
+ search,
+ searchFields,
+ defaultSearchOperator,
+ hasReference,
+ kueryNode,
+ indexPattern,
+}: QueryParams) {
const types = getTypes(mappings, type);
const bool: any = {
filter: [
+ ...(kueryNode != null ? [toElasticsearchQuery(kueryNode, indexPattern)] : []),
{
bool: {
must: hasReference
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
index 7bd04ca8f3494..97cab3e566d5e 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
@@ -72,16 +72,16 @@ describe('getSearchDsl', () => {
getSearchDsl(MAPPINGS, SCHEMA, opts);
expect(getQueryParams).toHaveBeenCalledTimes(1);
- expect(getQueryParams).toHaveBeenCalledWith(
- MAPPINGS,
- SCHEMA,
- opts.namespace,
- opts.type,
- opts.search,
- opts.searchFields,
- opts.defaultSearchOperator,
- opts.hasReference
- );
+ expect(getQueryParams).toHaveBeenCalledWith({
+ mappings: MAPPINGS,
+ schema: SCHEMA,
+ namespace: opts.namespace,
+ type: opts.type,
+ search: opts.search,
+ searchFields: opts.searchFields,
+ defaultSearchOperator: opts.defaultSearchOperator,
+ hasReference: opts.hasReference,
+ });
});
it('passes (mappings, type, sortField, sortOrder) to getSortingParams', () => {
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
index 1c2c87bca6ea7..68f6060702505 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
@@ -17,12 +17,14 @@
* under the License.
*/
+import { KueryNode } from '@kbn/es-query';
import Boom from 'boom';
import { IndexMapping } from '../../../mappings';
import { SavedObjectsSchema } from '../../../schema';
import { getQueryParams } from './query_params';
import { getSortingParams } from './sorting_params';
+import { SavedObjectsIndexPattern } from '../cache_index_patterns';
interface GetSearchDslOptions {
type: string | string[];
@@ -36,6 +38,8 @@ interface GetSearchDslOptions {
type: string;
id: string;
};
+ kueryNode?: KueryNode;
+ indexPattern?: SavedObjectsIndexPattern;
}
export function getSearchDsl(
@@ -52,6 +56,8 @@ export function getSearchDsl(
sortOrder,
namespace,
hasReference,
+ kueryNode,
+ indexPattern,
} = options;
if (!type) {
@@ -63,7 +69,7 @@ export function getSearchDsl(
}
return {
- ...getQueryParams(
+ ...getQueryParams({
mappings,
schema,
namespace,
@@ -71,8 +77,10 @@ export function getSearchDsl(
search,
searchFields,
defaultSearchOperator,
- hasReference
- ),
+ hasReference,
+ kueryNode,
+ indexPattern,
+ }),
...getSortingParams(mappings, type, sortField, sortOrder),
};
}
diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts
index 1cc424199b887..e7e7a4c64392a 100644
--- a/src/core/server/saved_objects/types.ts
+++ b/src/core/server/saved_objects/types.ts
@@ -123,6 +123,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
searchFields?: string[];
hasReference?: { type: string; id: string };
defaultSearchOperator?: 'AND' | 'OR';
+ filter?: string;
}
/**
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 4ae1c0c267ea9..ae839644fc2e2 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -10,6 +10,7 @@ import { ConfigOptions } from 'elasticsearch';
import { DetailedPeerCertificate } from 'tls';
import { Duration } from 'moment';
import { IncomingHttpHeaders } from 'http';
+import { IndexPatternsService } from 'src/legacy/server/index_patterns';
import { KibanaConfigType } from 'src/core/server/kibana_config';
import { Logger as Logger_2 } from 'src/core/server/logging';
import { ObjectType } from '@kbn/config-schema';
@@ -841,6 +842,8 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
defaultSearchOperator?: 'AND' | 'OR';
fields?: string[];
// (undocumented)
+ filter?: string;
+ // (undocumented)
hasReference?: {
type: string;
id: string;
diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts
index eeee5f3f4c6c7..4cbb1c82cc1e4 100644
--- a/src/legacy/core_plugins/elasticsearch/index.d.ts
+++ b/src/legacy/core_plugins/elasticsearch/index.d.ts
@@ -482,7 +482,7 @@ export interface CallCluster {
(endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType;
(endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType;
- // ingest namepsace
+ // ingest namespace
(endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType;
(endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType;
(endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType;
diff --git a/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts b/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts
index fa82e54e9fb0a..10047284f5c96 100644
--- a/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts
+++ b/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import { Readable } from 'stream';
-import { SavedObject } from 'kibana/server';
+import { SavedObject } from 'src/core/server';
import { createSplitStream, createMapStream, createFilterStream } from '../../../utils/streams';
export function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) {
diff --git a/src/legacy/server/saved_objects/routes/find.ts b/src/legacy/server/saved_objects/routes/find.ts
index bb8fb21aea29c..f8cb8c50d9684 100644
--- a/src/legacy/server/saved_objects/routes/find.ts
+++ b/src/legacy/server/saved_objects/routes/find.ts
@@ -39,6 +39,7 @@ interface FindRequest extends WithoutQueryAndParams {
id: string;
};
fields?: string[];
+ filter?: string;
};
}
@@ -79,6 +80,9 @@ export const createFindRoute = (prereqs: Prerequisites) => ({
fields: Joi.array()
.items(Joi.string())
.single(),
+ filter: Joi.string()
+ .allow('')
+ .optional(),
})
.default(),
},
@@ -94,6 +98,7 @@ export const createFindRoute = (prereqs: Prerequisites) => ({
sortField: query.sort_field,
hasReference: query.has_reference,
fields: query.fields,
+ filter: query.filter,
});
},
},
diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js
index edaa285006422..156c92ef6bdc0 100644
--- a/src/legacy/server/saved_objects/saved_objects_mixin.js
+++ b/src/legacy/server/saved_objects/saved_objects_mixin.js
@@ -26,6 +26,7 @@ import {
SavedObjectsClient,
SavedObjectsRepository,
ScopedSavedObjectsClientProvider,
+ SavedObjectsCacheIndexPatterns,
getSortedObjectsForExport,
importSavedObjects,
resolveImportErrors,
@@ -63,6 +64,7 @@ export function savedObjectsMixin(kbnServer, server) {
const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas);
const visibleTypes = allTypes.filter(type => !schema.isHiddenType(type));
const importableAndExportableTypes = getImportableAndExportableTypes({ kbnServer, visibleTypes });
+ const cacheIndexPatterns = new SavedObjectsCacheIndexPatterns();
server.decorate('server', 'kibanaMigrator', migrator);
server.decorate(
@@ -113,11 +115,18 @@ export function savedObjectsMixin(kbnServer, server) {
});
const combinedTypes = visibleTypes.concat(extraTypes);
const allowedTypes = [...new Set(combinedTypes)];
+
+ if (cacheIndexPatterns.getIndexPatternsService() == null) {
+ cacheIndexPatterns.setIndexPatternsService(
+ server.indexPatternsServiceFactory({ callCluster })
+ );
+ }
const config = server.config();
return new SavedObjectsRepository({
index: config.get('kibana.index'),
config,
+ cacheIndexPatterns,
migrator,
mappings,
schema,
diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js
index cdbc642485706..d3a40583dfe23 100644
--- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js
+++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js
@@ -84,6 +84,11 @@ describe('Saved Objects Mixin', () => {
get: stubConfig,
};
},
+ indexPatternsServiceFactory: () => {
+ return {
+ getFieldsForWildcard: jest.fn(),
+ };
+ },
plugins: {
elasticsearch: {
getCluster: () => {
diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts
index cdf82cd9eb9d1..962dc6b23d098 100644
--- a/src/plugins/data/common/field_formats/field_format.ts
+++ b/src/plugins/data/common/field_formats/field_format.ts
@@ -56,8 +56,10 @@ export abstract class FieldFormat {
/**
* @property {FieldFormatConvert}
* @private
+ * have to remove the private because of
+ * https://github.com/Microsoft/TypeScript/issues/17293
*/
- private convertObject: FieldFormatConvert | undefined;
+ convertObject: FieldFormatConvert | undefined;
/**
* @property {Function} - ref to child class
@@ -171,7 +173,11 @@ export abstract class FieldFormat {
return createCustomFieldFormat(convertFn);
}
- private static setupContentType(
+ /*
+ * have to remove the private because of
+ * https://github.com/Microsoft/TypeScript/issues/17293
+ */
+ static setupContentType(
fieldFormat: IFieldFormat,
convert: Partial | FieldFormatConvertFunction = {}
): FieldFormatConvert {
@@ -185,7 +191,11 @@ export abstract class FieldFormat {
};
}
- private static toConvertObject(convert: FieldFormatConvertFunction): Partial {
+ /*
+ * have to remove the private because of
+ * https://github.com/Microsoft/TypeScript/issues/17293
+ */
+ static toConvertObject(convert: FieldFormatConvertFunction): Partial {
if (isFieldFormatConvertFn(convert)) {
return {
[TEXT_CONTEXT_TYPE]: convert,
diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js
index fa03d46765e92..a41df24ea7a41 100644
--- a/test/api_integration/apis/saved_objects/find.js
+++ b/test/api_integration/apis/saved_objects/find.js
@@ -109,6 +109,63 @@ export default function ({ getService }) {
})
));
});
+
+ describe('with a filter', () => {
+ it('should return 200 with a valid response', async () => (
+ await supertest
+ .get('/api/saved_objects/_find?type=visualization&filter=visualization.attributes.title:"Count of requests"')
+ .expect(200)
+ .then(resp => {
+ expect(resp.body).to.eql({
+ page: 1,
+ per_page: 20,
+ total: 1,
+ saved_objects: [
+ {
+ type: 'visualization',
+ id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
+ attributes: {
+ title: 'Count of requests',
+ visState: resp.body.saved_objects[0].attributes.visState,
+ uiStateJSON: '{"spy":{"mode":{"name":null,"fill":false}}}',
+ description: '',
+ version: 1,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
+ },
+ },
+ references: [
+ {
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ type: 'index-pattern',
+ id: '91200a00-9efd-11e7-acb3-3dab96693fab',
+ }
+ ],
+ migrationVersion: {
+ visualization: '7.3.1',
+ },
+ updated_at: '2017-09-21T18:51:23.794Z',
+ version: 'WzIsMV0=',
+ },
+ ],
+ });
+ })
+ ));
+
+ it('wrong type should return 400 with Bad Request', async () => (
+ await supertest
+ .get('/api/saved_objects/_find?type=visualization&filter=dashboard.attributes.title:foo')
+ .expect(400)
+ .then(resp => {
+ console.log('body', JSON.stringify(resp.body));
+ expect(resp.body).to.eql({
+ error: 'Bad Request',
+ message: 'This type dashboard is not allowed: Bad Request',
+ statusCode: 400,
+ });
+ })
+ ));
+ });
});
describe('without kibana index', () => {
@@ -200,6 +257,36 @@ export default function ({ getService }) {
})
));
});
+
+ describe('with a filter', () => {
+ it('should return 200 with an empty response', async () => (
+ await supertest
+ .get('/api/saved_objects/_find?type=visualization&filter=visualization.attributes.title:"Count of requests"')
+ .expect(200)
+ .then(resp => {
+ expect(resp.body).to.eql({
+ page: 1,
+ per_page: 20,
+ total: 0,
+ saved_objects: []
+ });
+ })
+ ));
+
+ it('wrong type should return 400 with Bad Request', async () => (
+ await supertest
+ .get('/api/saved_objects/_find?type=visualization&filter=dashboard.attributes.title:foo')
+ .expect(400)
+ .then(resp => {
+ console.log('body', JSON.stringify(resp.body));
+ expect(resp.body).to.eql({
+ error: 'Bad Request',
+ message: 'This type dashboard is not allowed: Bad Request',
+ statusCode: 400,
+ });
+ })
+ ));
+ });
});
});
}
diff --git a/test/tsconfig.json b/test/tsconfig.json
index 276238adf5901..71c9e375a4124 100644
--- a/test/tsconfig.json
+++ b/test/tsconfig.json
@@ -14,9 +14,9 @@
"**/*.ts",
"**/*.tsx",
"../typings/lodash.topath/*.ts",
- "typings/**/*",
+ "typings/**/*"
],
"exclude": [
"plugin_functional/plugins/**/*"
]
-}
+}
\ No newline at end of file
diff --git a/test/typings/index.d.ts b/test/typings/index.d.ts
index ba43e7e7184e5..fd2500257b315 100644
--- a/test/typings/index.d.ts
+++ b/test/typings/index.d.ts
@@ -17,6 +17,12 @@
* under the License.
*/
+declare module '*.html' {
+ const template: string;
+ // eslint-disable-next-line import/no-default-export
+ export default template;
+}
+
type MethodKeysOf = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];
diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts
index 45e4c1ed2aa4e..6799f0ec63846 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/find.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts
@@ -23,6 +23,11 @@ interface FindTests {
unknownSearchField: FindTest;
hiddenType: FindTest;
noType: FindTest;
+ filterWithNotSpaceAwareType: FindTest;
+ filterWithHiddenType: FindTest;
+ filterWithUnknownType: FindTest;
+ filterWithNoType: FindTest;
+ filterWithUnAllowedType: FindTest;
}
interface FindTestDefinition {
@@ -73,6 +78,14 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest)
});
};
+ const expectFilterWrongTypeError = (resp: { [key: string]: any }) => {
+ expect(resp.body).to.eql({
+ error: 'Bad Request',
+ message: 'This type dashboard is not allowed: Bad Request',
+ statusCode: 400,
+ });
+ };
+
const expectTypeRequired = (resp: { [key: string]: any }) => {
expect(resp.body).to.eql({
error: 'Bad Request',
@@ -184,6 +197,67 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest)
.expect(tests.noType.statusCode)
.then(tests.noType.response));
});
+
+ describe('filter', () => {
+ it(`by wrong type should return ${tests.filterWithUnAllowedType.statusCode} with ${tests.filterWithUnAllowedType.description}`, async () =>
+ await supertest
+ .get(
+ `${getUrlPrefix(
+ spaceId
+ )}/api/saved_objects/_find?type=globaltype&filter=dashboard.title:'Requests'`
+ )
+ .auth(user.username, user.password)
+ .expect(tests.filterWithUnAllowedType.statusCode)
+ .then(tests.filterWithUnAllowedType.response));
+
+ it(`not space aware type should return ${tests.filterWithNotSpaceAwareType.statusCode} with ${tests.filterWithNotSpaceAwareType.description}`, async () =>
+ await supertest
+ .get(
+ `${getUrlPrefix(
+ spaceId
+ )}/api/saved_objects/_find?type=globaltype&filter=globaltype.attributes.name:*global*`
+ )
+ .auth(user.username, user.password)
+ .expect(tests.filterWithNotSpaceAwareType.statusCode)
+ .then(tests.filterWithNotSpaceAwareType.response));
+
+ it(`finding a hiddentype should return ${tests.filterWithHiddenType.statusCode} with ${tests.filterWithHiddenType.description}`, async () =>
+ await supertest
+ .get(
+ `${getUrlPrefix(
+ spaceId
+ )}/api/saved_objects/_find?type=hiddentype&fields=name&filter=hiddentype.attributes.name:'hello'`
+ )
+ .auth(user.username, user.password)
+ .expect(tests.filterWithHiddenType.statusCode)
+ .then(tests.filterWithHiddenType.response));
+
+ describe('unknown type', () => {
+ it(`should return ${tests.filterWithUnknownType.statusCode} with ${tests.filterWithUnknownType.description}`, async () =>
+ await supertest
+ .get(
+ `${getUrlPrefix(
+ spaceId
+ )}/api/saved_objects/_find?type=wigwags&filter=wigwags.attributes.title:'unknown'`
+ )
+ .auth(user.username, user.password)
+ .expect(tests.filterWithUnknownType.statusCode)
+ .then(tests.filterWithUnknownType.response));
+ });
+
+ describe('no type', () => {
+ it(`should return ${tests.filterWithNoType.statusCode} with ${tests.filterWithNoType.description}`, async () =>
+ await supertest
+ .get(
+ `${getUrlPrefix(
+ spaceId
+ )}/api/saved_objects/_find?filter=global.attributes.name:*global*`
+ )
+ .auth(user.username, user.password)
+ .expect(tests.filterWithNoType.statusCode)
+ .then(tests.filterWithNoType.response));
+ });
+ });
});
};
@@ -195,6 +269,7 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest)
createExpectEmpty,
createExpectRbacForbidden,
createExpectVisualizationResults,
+ expectFilterWrongTypeError,
expectNotSpaceAwareResults,
expectTypeRequired,
findTest,
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts
index 92e6ec850dc0e..366b8b44585cd 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts
@@ -18,6 +18,7 @@ export default function({ getService }: FtrProviderContext) {
createExpectEmpty,
createExpectRbacForbidden,
createExpectVisualizationResults,
+ expectFilterWrongTypeError,
expectNotSpaceAwareResults,
expectTypeRequired,
findTest,
@@ -94,6 +95,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'forbidden login and find globaltype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'forbidden',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
},
});
@@ -136,6 +162,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'empty result',
+ statusCode: 200,
+ response: createExpectEmpty(1, 20, 0),
+ },
+ filterWithUnknownType: {
+ description: 'empty result',
+ statusCode: 200,
+ response: createExpectEmpty(1, 20, 0),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -178,6 +229,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'forbidden login and find globaltype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'forbidden',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
},
});
@@ -220,6 +296,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -262,6 +363,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -304,6 +430,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -346,6 +497,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -388,6 +564,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -430,6 +631,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -472,6 +698,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'forbidden login and find globaltype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'forbidden',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
},
});
});
diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts
index d17dbe6e7b1ed..64d85a199e7bc 100644
--- a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts
+++ b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts
@@ -17,6 +17,7 @@ export default function({ getService }: FtrProviderContext) {
createExpectEmpty,
createExpectRbacForbidden,
createExpectVisualizationResults,
+ expectFilterWrongTypeError,
expectNotSpaceAwareResults,
expectTypeRequired,
findTest,
@@ -60,6 +61,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'forbidden login and find globaltype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
+ filterWithHiddenType: {
+ description: 'forbidden login and find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'forbidden',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
},
});
@@ -101,6 +127,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'empty result',
+ statusCode: 200,
+ response: createExpectEmpty(1, 20, 0),
+ },
+ filterWithUnknownType: {
+ description: 'empty result',
+ statusCode: 200,
+ response: createExpectEmpty(1, 20, 0),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -142,6 +193,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'forbidden login and find globaltype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
+ filterWithHiddenType: {
+ description: 'forbidden login and find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'forbidden',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
},
});
@@ -183,6 +259,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -224,6 +325,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -265,6 +391,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -306,6 +457,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -347,6 +523,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'forbidden',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
},
});
@@ -388,6 +589,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'forbidden',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
},
});
@@ -429,6 +655,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'forbidden',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
},
});
@@ -470,6 +721,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the globaltype',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
+ filterWithHiddenType: {
+ description: 'forbidden find hiddentype message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('hiddentype'),
+ },
+ filterWithUnknownType: {
+ description: 'forbidden find wigwags message',
+ statusCode: 403,
+ response: createExpectRbacForbidden('wigwags'),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'forbidden',
+ statusCode: 403,
+ response: createExpectRbacForbidden('globaltype'),
+ },
},
});
});
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts
index 69a2690c61978..a07d3edf834e9 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts
@@ -15,6 +15,7 @@ export default function({ getService }: FtrProviderContext) {
const {
createExpectEmpty,
createExpectVisualizationResults,
+ expectFilterWrongTypeError,
expectNotSpaceAwareResults,
expectTypeRequired,
findTest,
@@ -59,6 +60,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the visualization',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'empty result',
+ statusCode: 200,
+ response: createExpectEmpty(1, 20, 0),
+ },
+ filterWithUnknownType: {
+ description: 'empty result',
+ statusCode: 200,
+ response: createExpectEmpty(1, 20, 0),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
@@ -100,6 +126,31 @@ export default function({ getService }: FtrProviderContext) {
statusCode: 400,
response: expectTypeRequired,
},
+ filterWithNotSpaceAwareType: {
+ description: 'only the visualization',
+ statusCode: 200,
+ response: expectNotSpaceAwareResults,
+ },
+ filterWithHiddenType: {
+ description: 'empty result',
+ statusCode: 200,
+ response: createExpectEmpty(1, 20, 0),
+ },
+ filterWithUnknownType: {
+ description: 'empty result',
+ statusCode: 200,
+ response: createExpectEmpty(1, 20, 0),
+ },
+ filterWithNoType: {
+ description: 'bad request, type is required',
+ statusCode: 400,
+ response: expectTypeRequired,
+ },
+ filterWithUnAllowedType: {
+ description: 'Bad Request',
+ statusCode: 400,
+ response: expectFilterWrongTypeError,
+ },
},
});
});