Skip to content

Commit

Permalink
Change saved objects client find to allow partial authorization (#7…
Browse files Browse the repository at this point in the history
  • Loading branch information
jportner authored Sep 22, 2020
1 parent 7544a33 commit d666038
Show file tree
Hide file tree
Showing 50 changed files with 1,067 additions and 525 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ export interface SavedObjectsFindOptions
| [sortField](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | <code>string</code> | |
| [sortOrder](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | <code>string</code> | |
| [type](./kibana-plugin-core-public.savedobjectsfindoptions.type.md) | <code>string &#124; string[]</code> | |
| [typeToNamespacesMap](./kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md) | <code>Map&lt;string, string[] &#124; undefined&gt;</code> | This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the <code>type</code> and <code>namespaces</code> fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. |

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) &gt; [typeToNamespacesMap](./kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md)

## SavedObjectsFindOptions.typeToNamespacesMap property

This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the `type` and `namespaces` fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace.

<b>Signature:</b>

```typescript
typeToNamespacesMap?: Map<string, string[] | undefined>;
```
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ export interface SavedObjectsFindOptions
| [sortField](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | <code>string</code> | |
| [sortOrder](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | <code>string</code> | |
| [type](./kibana-plugin-core-server.savedobjectsfindoptions.type.md) | <code>string &#124; string[]</code> | |
| [typeToNamespacesMap](./kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md) | <code>Map&lt;string, string[] &#124; undefined&gt;</code> | This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the <code>type</code> and <code>namespaces</code> fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. |

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) &gt; [typeToNamespacesMap](./kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md)

## SavedObjectsFindOptions.typeToNamespacesMap property

This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the `type` and `namespaces` fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace.

<b>Signature:</b>

```typescript
typeToNamespacesMap?: Map<string, string[] | undefined>;
```
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
<b>Signature:</b>

```typescript
find<T = unknown>({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| { search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, } | <code>SavedObjectsFindOptions</code> | |
| options | <code>SavedObjectsFindOptions</code> | |
<b>Returns:</b>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export declare class SavedObjectsRepository
| [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object |
| [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. |
| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[<code>addToNamespaces</code>\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. |
| [find({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | |
| [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | |
| [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object |
| [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. |
| [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) &gt; [createEmptyFindResponse](./kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md)

## SavedObjectsUtils.createEmptyFindResponse property

Creates an empty response for a find operation. This is only intended to be used by saved objects client wrappers.

<b>Signature:</b>

```typescript
static createEmptyFindResponse: <T>({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse<T>;
```
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export declare class SavedObjectsUtils

| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [createEmptyFindResponse](./kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md) | <code>static</code> | <code>&lt;T&gt;({ page, perPage, }: SavedObjectsFindOptions) =&gt; SavedObjectsFindResponse&lt;T&gt;</code> | Creates an empty response for a find operation. This is only intended to be used by saved objects client wrappers. |
| [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) | <code>static</code> | <code>(namespace?: string &#124; undefined) =&gt; string</code> | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the <code>undefined</code> namespace ID (which has a namespace string of <code>'default'</code>). |
| [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) | <code>static</code> | <code>(namespace: string) =&gt; string &#124; undefined</code> | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the <code>'default'</code> namespace string (which has a namespace ID of <code>undefined</code>). |

1 change: 1 addition & 0 deletions src/core/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,7 @@ export interface SavedObjectsFindOptions {
sortOrder?: string;
// (undocumented)
type: string | string[];
typeToNamespacesMap?: Map<string, string[] | undefined>;
}

// @public
Expand Down
2 changes: 1 addition & 1 deletion src/core/public/saved_objects/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { HttpFetchOptions, HttpSetup } from '../http';

type SavedObjectsFindOptions = Omit<
SavedObjectFindOptionsServer,
'namespace' | 'sortOrder' | 'rootSearchFields'
'sortOrder' | 'rootSearchFields' | 'typeToNamespacesMap'
>;

type PromiseType<T extends Promise<any>> = T extends Promise<infer U> ? U : never;
Expand Down
93 changes: 76 additions & 17 deletions src/core/server/saved_objects/service/lib/repository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2477,6 +2477,33 @@ describe('SavedObjectsRepository', () => {
expect(client.search).not.toHaveBeenCalled();
});

it(`throws when namespaces is an empty array`, async () => {
await expect(
savedObjectsRepository.find({ type: 'foo', namespaces: [] })
).rejects.toThrowError('options.namespaces cannot be an empty array');
expect(client.search).not.toHaveBeenCalled();
});

it(`throws when type is not falsy and typeToNamespacesMap is defined`, async () => {
await expect(
savedObjectsRepository.find({ type: 'foo', typeToNamespacesMap: new Map() })
).rejects.toThrowError(
'options.type must be an empty string when options.typeToNamespacesMap is used'
);
expect(client.search).not.toHaveBeenCalled();
});

it(`throws when type is not an empty array and typeToNamespacesMap is defined`, async () => {
const test = async (args) => {
await expect(savedObjectsRepository.find(args)).rejects.toThrowError(
'options.namespaces must be an empty array when options.typeToNamespacesMap is used'
);
expect(client.search).not.toHaveBeenCalled();
};
await test({ type: '', typeToNamespacesMap: new Map() });
await test({ type: '', namespaces: ['some-ns'], typeToNamespacesMap: new Map() });
});

it(`throws when searchFields is defined but not an array`, async () => {
await expect(
savedObjectsRepository.find({ type, searchFields: 'string' })
Expand All @@ -2493,7 +2520,7 @@ describe('SavedObjectsRepository', () => {

it(`throws when KQL filter syntax is invalid`, async () => {
const findOpts = {
namespace,
namespaces: [namespace],
search: 'foo*',
searchFields: ['foo'],
type: ['dashboard'],
Expand Down Expand Up @@ -2577,38 +2604,70 @@ describe('SavedObjectsRepository', () => {
const test = async (types) => {
const result = await savedObjectsRepository.find({ type: types });
expect(result).toEqual(expect.objectContaining({ saved_objects: [] }));
expect(client.search).not.toHaveBeenCalled();
};

await test('unknownType');
await test(HIDDEN_TYPE);
await test(['unknownType', HIDDEN_TYPE]);
});

it(`should return empty results when attempting to find only invalid or hidden types using typeToNamespacesMap`, async () => {
const test = async (types) => {
const result = await savedObjectsRepository.find({
typeToNamespacesMap: new Map(types.map((x) => [x, undefined])),
type: '',
namespaces: [],
});
expect(result).toEqual(expect.objectContaining({ saved_objects: [] }));
expect(client.search).not.toHaveBeenCalled();
};

await test(['unknownType']);
await test([HIDDEN_TYPE]);
await test(['unknownType', HIDDEN_TYPE]);
});
});

describe('search dsl', () => {
it(`passes mappings, registry, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl`, async () => {
const commonOptions = {
type: [type], // cannot be used when `typeToNamespacesMap` is present
namespaces: [namespace], // cannot be used when `typeToNamespacesMap` is present
search: 'foo*',
searchFields: ['foo'],
sortField: 'name',
sortOrder: 'desc',
defaultSearchOperator: 'AND',
hasReference: {
type: 'foo',
id: '1',
},
kueryNode: undefined,
};

it(`passes mappings, registry, and search options to getSearchDsl`, async () => {
await findSuccess(commonOptions, namespace);
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, commonOptions);
});

it(`accepts typeToNamespacesMap`, async () => {
const relevantOpts = {
namespaces: [namespace],
search: 'foo*',
searchFields: ['foo'],
type: [type],
sortField: 'name',
sortOrder: 'desc',
defaultSearchOperator: 'AND',
hasReference: {
type: 'foo',
id: '1',
},
kueryNode: undefined,
...commonOptions,
type: '',
namespaces: [],
typeToNamespacesMap: new Map([[type, [namespace]]]), // can only be used when `type` is falsy and `namespaces` is an empty array
};

await findSuccess(relevantOpts, namespace);
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, relevantOpts);
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, {
...relevantOpts,
type: [type],
});
});

it(`accepts KQL expression filter and passes KueryNode to getSearchDsl`, async () => {
const findOpts = {
namespace,
namespaces: [namespace],
search: 'foo*',
searchFields: ['foo'],
type: ['dashboard'],
Expand Down Expand Up @@ -2649,7 +2708,7 @@ describe('SavedObjectsRepository', () => {

it(`accepts KQL KueryNode filter and passes KueryNode to getSearchDsl`, async () => {
const findOpts = {
namespace,
namespaces: [namespace],
search: 'foo*',
searchFields: ['foo'],
type: ['dashboard'],
Expand Down
65 changes: 40 additions & 25 deletions src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import {
} from '../../types';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { validateConvertFilterToKueryNode } from './filter_utils';
import { SavedObjectsUtils } from './utils';
import { FIND_DEFAULT_PAGE, FIND_DEFAULT_PER_PAGE, SavedObjectsUtils } from './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.
Expand Down Expand Up @@ -693,37 +693,51 @@ export class SavedObjectsRepository {
* @property {string} [options.preference]
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page }
*/
async find<T = unknown>({
search,
defaultSearchOperator = 'OR',
searchFields,
rootSearchFields,
hasReference,
page = 1,
perPage = 20,
sortField,
sortOrder,
fields,
namespaces,
type,
filter,
preference,
}: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>> {
if (!type) {
async find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>> {
const {
search,
defaultSearchOperator = 'OR',
searchFields,
rootSearchFields,
hasReference,
page = FIND_DEFAULT_PAGE,
perPage = FIND_DEFAULT_PER_PAGE,
sortField,
sortOrder,
fields,
namespaces,
type,
typeToNamespacesMap,
filter,
preference,
} = options;

if (!type && !typeToNamespacesMap) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'options.type must be a string or an array of strings'
);
} else if (namespaces?.length === 0 && !typeToNamespacesMap) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'options.namespaces cannot be an empty array'
);
} else if (type && typeToNamespacesMap) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'options.type must be an empty string when options.typeToNamespacesMap is used'
);
} else if ((!namespaces || namespaces?.length) && typeToNamespacesMap) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'options.namespaces must be an empty array when options.typeToNamespacesMap is used'
);
}

const types = Array.isArray(type) ? type : [type];
const types = type
? Array.isArray(type)
? type
: [type]
: Array.from(typeToNamespacesMap!.keys());
const allowedTypes = types.filter((t) => this._allowedTypes.includes(t));
if (allowedTypes.length === 0) {
return {
page,
per_page: perPage,
total: 0,
saved_objects: [],
};
return SavedObjectsUtils.createEmptyFindResponse<T>(options);
}

if (searchFields && !Array.isArray(searchFields)) {
Expand Down Expand Up @@ -766,6 +780,7 @@ export class SavedObjectsRepository {
sortField,
sortOrder,
namespaces,
typeToNamespacesMap,
hasReference,
kueryNode,
}),
Expand Down
Loading

0 comments on commit d666038

Please sign in to comment.