From fa6ae1cd736a3de905ec68dec7b51077e45a12f1 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 19 Aug 2020 14:30:26 -0400 Subject: [PATCH 01/16] Add `namespaceStringToId` and `namespaceIdToString` methods to core Now that saved objects' `namespaces` are exposed, we should provide a method to compare these strings to namespace IDs. The Spaces plugin already provided utility functions for this; I changed them to be a facade over the new core functions. The reason for this is that other plugins (alerting, actions) depend on the Spaces plugin and will use an `undefined` namespace if the Spaces plugin is not enabled. --- .../core/server/kibana-plugin-core-server.md | 2 + ...-plugin-core-server.namespaceidtostring.md | 13 +++++ ...-plugin-core-server.namespacestringtoid.md | 13 +++++ src/core/server/index.ts | 2 + .../server/saved_objects/service/index.ts | 2 + .../server/saved_objects/service/lib/index.ts | 2 + .../service/lib/namespace.test.ts | 53 +++++++++++++++++++ .../saved_objects/service/lib/namespace.ts | 50 +++++++++++++++++ .../saved_objects/service/lib/repository.ts | 29 ++++------ .../service/lib/search_dsl/query_params.ts | 11 ++-- src/core/server/server.api.md | 6 +++ .../lib/copy_to_spaces/copy_to_spaces.test.ts | 1 + .../resolve_copy_conflicts.test.ts | 1 + .../server/lib/utils/__mocks__/index.ts | 14 +++++ .../spaces/server/lib/utils/namespace.test.ts | 46 ++++++---------- .../spaces/server/lib/utils/namespace.ts | 36 ++++++------- .../routes/api/external/copy_to_space.test.ts | 2 +- 17 files changed, 206 insertions(+), 77 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md create mode 100644 src/core/server/saved_objects/service/lib/namespace.test.ts create mode 100644 src/core/server/saved_objects/service/lib/namespace.ts create mode 100644 x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 5347f0b55e19b..5098e60ef6785 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -219,6 +219,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | Variable | Description | | --- | --- | | [kibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Set of helpers used to create KibanaResponse to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution. | +| [namespaceIdToString](./kibana-plugin-core-server.namespaceidtostring.md) | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined namespace ID (which has a namespace string of 'default'). | +| [namespaceStringToId](./kibana-plugin-core-server.namespacestringtoid.md) | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default' namespace string (which has a namespace ID of undefined). | | [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md) | The current "level" of availability of a service. | | [validBodyOutput](./kibana-plugin-core-server.validbodyoutput.md) | The set of valid body.output | diff --git a/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md b/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md new file mode 100644 index 0000000000000..5c9f093531e1c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [namespaceIdToString](./kibana-plugin-core-server.namespaceidtostring.md) + +## namespaceIdToString variable + +Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the `undefined` namespace ID (which has a namespace string of `'default'`). + +Signature: + +```typescript +namespaceIdToString: (namespace?: string | undefined) => string +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md b/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md new file mode 100644 index 0000000000000..39d6a13aa7fb0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [namespaceStringToId](./kibana-plugin-core-server.namespacestringtoid.md) + +## namespaceStringToId variable + +Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the `'default'` namespace string (which has a namespace ID of `undefined`). + +Signature: + +```typescript +namespaceStringToId: (namespace: string) => string | undefined +``` diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 382318ea86a34..de45f0a665906 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -298,6 +298,8 @@ export { exportSavedObjectsToStream, importSavedObjectsFromStream, resolveSavedObjectsImportErrors, + namespaceIdToString, + namespaceStringToId, } from './saved_objects'; export { diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 9f625b4732e26..245df328db665 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -58,6 +58,8 @@ export { SavedObjectsErrorHelpers, SavedObjectsClientFactory, SavedObjectsClientFactoryProvider, + namespaceIdToString, + namespaceStringToId, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index e103120388e35..cfae6ae9b62d5 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -30,3 +30,5 @@ export { } from './scoped_client_provider'; export { SavedObjectsErrorHelpers } from './errors'; + +export { namespaceIdToString, namespaceStringToId } from './namespace'; diff --git a/src/core/server/saved_objects/service/lib/namespace.test.ts b/src/core/server/saved_objects/service/lib/namespace.test.ts new file mode 100644 index 0000000000000..c5818deb8ef2a --- /dev/null +++ b/src/core/server/saved_objects/service/lib/namespace.test.ts @@ -0,0 +1,53 @@ +/* + * 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 { namespaceIdToString, namespaceStringToId } from './namespace'; + +describe('#namespaceIdToString', () => { + it('converts `undefined` to default namespace string', () => { + expect(namespaceIdToString(undefined)).toEqual('default'); + }); + + it('leaves other namespace IDs as-is', () => { + expect(namespaceIdToString('foo')).toEqual('foo'); + }); + + it('throws an error when a namespace ID is an empty string', () => { + expect(() => namespaceIdToString('')).toThrowError('namespace cannot be an empty string'); + }); +}); + +describe('#namespaceStringToId', () => { + it('converts default namespace string to `undefined`', () => { + expect(namespaceStringToId('default')).toBeUndefined(); + }); + + it('leaves other namespace strings as-is', () => { + expect(namespaceStringToId('foo')).toEqual('foo'); + }); + + it('throws an error when a namespace string is falsy', () => { + const test = (arg: any) => + expect(() => namespaceStringToId(arg)).toThrowError('namespace must be a non-empty string'); + + test(undefined); + test(null); + test(''); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/namespace.ts b/src/core/server/saved_objects/service/lib/namespace.ts new file mode 100644 index 0000000000000..385725ca6e948 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/namespace.ts @@ -0,0 +1,50 @@ +/* + * 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. + */ + +export const DEFAULT_NAMESPACE_STRING = 'default'; + +/** + * @public + * Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with + * the exception of the `undefined` namespace ID (which has a namespace string of `'default'`). + * + * @param namespace The namespace ID, which must be either a non-empty string or `undefined`. + */ +export const namespaceIdToString = (namespace?: string) => { + if (namespace === '') { + throw new TypeError('namespace cannot be an empty string'); + } + + return namespace ?? DEFAULT_NAMESPACE_STRING; +}; + +/** + * @public + * Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with + * the exception of the `'default'` namespace string (which has a namespace ID of `undefined`). + * + * @param namespace The namespace string, which must be non-empty. + */ +export const namespaceStringToId = (namespace: string) => { + if (!namespace) { + throw new TypeError('namespace must be a non-empty string'); + } + + return namespace !== DEFAULT_NAMESPACE_STRING ? namespace : undefined; +}; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 9f6db446ea195..941488d2264e6 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -63,6 +63,7 @@ import { } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; +import { namespaceIdToString } from './namespace'; // 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. @@ -470,7 +471,7 @@ export class SavedObjectsRepository { preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); const remainingNamespaces = existingNamespaces?.filter( - (x) => x !== getNamespaceString(namespace) + (x) => x !== namespaceIdToString(namespace) ); if (remainingNamespaces?.length) { @@ -568,7 +569,7 @@ export class SavedObjectsRepository { } `, lang: 'painless', - params: { namespace: getNamespaceString(namespace) }, + params: { namespace: namespaceIdToString(namespace) }, }, conflicts: 'proceed', ...getSearchDsl(this._mappings, this._registry, { @@ -793,7 +794,7 @@ export class SavedObjectsRepository { let namespaces = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = doc._source.namespaces ?? [getNamespaceString(doc._source.namespace)]; + namespaces = doc._source.namespaces ?? [namespaceIdToString(doc._source.namespace)]; } return { @@ -849,7 +850,7 @@ export class SavedObjectsRepository { let namespaces: string[] = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = body._source.namespaces ?? [getNamespaceString(body._source.namespace)]; + namespaces = body._source.namespaces ?? [namespaceIdToString(body._source.namespace)]; } return { @@ -922,7 +923,7 @@ export class SavedObjectsRepository { let namespaces = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = body.get._source.namespaces ?? [getNamespaceString(body.get._source.namespace)]; + namespaces = body.get._source.namespaces ?? [namespaceIdToString(body.get._source.namespace)]; } return { @@ -1199,12 +1200,12 @@ export class SavedObjectsRepository { }; } namespaces = actualResult._source.namespaces ?? [ - getNamespaceString(actualResult._source.namespace), + namespaceIdToString(actualResult._source.namespace), ]; versionProperties = getExpectedVersionProperties(version, actualResult); } else { if (this._registry.isSingleNamespace(type)) { - namespaces = [getNamespaceString(namespace)]; + namespaces = [namespaceIdToString(namespace)]; } versionProperties = getExpectedVersionProperties(version); } @@ -1391,7 +1392,7 @@ export class SavedObjectsRepository { const savedObject = this._serializer.rawToSavedObject(raw); const { namespace, type } = savedObject; if (this._registry.isSingleNamespace(type)) { - savedObject.namespaces = [getNamespaceString(namespace)]; + savedObject.namespaces = [namespaceIdToString(namespace)]; } return omit(savedObject, 'namespace') as SavedObject; } @@ -1414,7 +1415,7 @@ export class SavedObjectsRepository { } const namespaces = raw._source.namespaces; - return namespaces?.includes(getNamespaceString(namespace)) ?? false; + return namespaces?.includes(namespaceIdToString(namespace)) ?? false; } /** @@ -1519,14 +1520,6 @@ function getExpectedVersionProperties(version?: string, document?: SavedObjectsR return {}; } -/** - * Returns the string representation of a namespace. - * The default namespace is undefined, and is represented by the string 'default'. - */ -function getNamespaceString(namespace?: string) { - return namespace ?? 'default'; -} - /** * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal @@ -1542,7 +1535,7 @@ function getSavedObjectNamespaces( if (document) { return document._source?.namespaces; } - return [getNamespaceString(namespace)]; + return [namespaceIdToString(namespace)]; } const unique = (array: string[]) => [...new Set(array)]; 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 164756f9796a5..f945fc7925ac6 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 @@ -21,6 +21,7 @@ import { esKuery, KueryNode } from '../../../../../../plugins/data/server'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; +import { DEFAULT_NAMESPACE_STRING } from '../namespace'; /** * Gets the types based on the type. Uses mappings to support @@ -63,7 +64,7 @@ function getFieldsForTypes(types: string[], searchFields?: string[]) { */ function getClauseForType( registry: ISavedObjectTypeRegistry, - namespaces: string[] = ['default'], + namespaces: string[] = [DEFAULT_NAMESPACE_STRING], type: string ) { if (namespaces.length === 0) { @@ -78,11 +79,11 @@ function getClauseForType( }; } else if (registry.isSingleNamespace(type)) { const should: Array> = []; - const eligibleNamespaces = namespaces.filter((namespace) => namespace !== 'default'); + const eligibleNamespaces = namespaces.filter((x) => x !== DEFAULT_NAMESPACE_STRING); if (eligibleNamespaces.length > 0) { should.push({ terms: { namespace: eligibleNamespaces } }); } - if (namespaces.includes('default')) { + if (namespaces.includes(DEFAULT_NAMESPACE_STRING)) { should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } }); } if (should.length === 0) { @@ -150,9 +151,7 @@ export function getQueryParams({ // would result in no results being returned, as the wildcard is treated as a literal, and not _actually_ as a wildcard. // We had a good discussion around the tradeoffs here: https://github.com/elastic/kibana/pull/67644#discussion_r441055716 const normalizedNamespaces = namespaces - ? Array.from( - new Set(namespaces.map((namespace) => (namespace === '*' ? 'default' : namespace))) - ) + ? Array.from(new Set(namespaces.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x)))) : undefined; const bool: any = { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 01558c1460f1b..da28ca3c3d53f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1597,6 +1597,12 @@ export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulPart // @public export type MutatingOperationRefreshSetting = boolean | 'wait_for'; +// @public +export const namespaceIdToString: (namespace?: string | undefined) => string; + +// @public +export const namespaceStringToId: (namespace: string) => string | undefined; + // Warning: (ae-missing-release-tag) "NodesVersionCompatibility" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 9679dd8c52523..6f30dade29e65 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -14,6 +14,7 @@ import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core jest.mock('../../../../../../src/core/server', () => { return { + ...(jest.requireActual('../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), importSavedObjectsFromStream: jest.fn(), }; diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 7bb4c61ed51a0..139ca4c45ffb4 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -14,6 +14,7 @@ import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_ jest.mock('../../../../../../src/core/server', () => { return { + ...(jest.requireActual('../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), resolveSavedObjectsImportErrors: jest.fn(), }; diff --git a/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts b/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts new file mode 100644 index 0000000000000..2bb23d0304858 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const mockNamespaceIdToString = jest.fn(); +const mockNamespaceStringToId = jest.fn(); +jest.mock('../../../../../../../src/core/server', () => ({ + namespaceIdToString: mockNamespaceIdToString, + namespaceStringToId: mockNamespaceStringToId, +})); + +export { mockNamespaceIdToString, mockNamespaceStringToId }; diff --git a/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts b/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts index a81a5f3cee187..79d3dda301045 100644 --- a/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts +++ b/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts @@ -4,45 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { mockNamespaceIdToString, mockNamespaceStringToId } from './__mocks__'; import { spaceIdToNamespace, namespaceToSpaceId } from './namespace'; -describe('#spaceIdToNamespace', () => { - it('converts the default space to undefined', () => { - expect(spaceIdToNamespace(DEFAULT_SPACE_ID)).toBeUndefined(); - }); - - it('returns non-default spaces as-is', () => { - expect(spaceIdToNamespace('foo')).toEqual('foo'); - }); - - it('throws an error when a spaceId is not provided', () => { - // @ts-ignore ts knows this isn't right - expect(() => spaceIdToNamespace()).toThrowErrorMatchingInlineSnapshot(`"spaceId is required"`); +beforeEach(() => { + jest.clearAllMocks(); +}); - // @ts-ignore ts knows this isn't right - expect(() => spaceIdToNamespace(null)).toThrowErrorMatchingInlineSnapshot( - `"spaceId is required"` - ); +describe('#spaceIdToNamespace', () => { + it('returns result of namespaceStringToId', () => { + mockNamespaceStringToId.mockReturnValue('bar'); - expect(() => spaceIdToNamespace('')).toThrowErrorMatchingInlineSnapshot( - `"spaceId is required"` - ); + const result = spaceIdToNamespace('foo'); + expect(mockNamespaceStringToId).toHaveBeenCalledWith('foo'); + expect(result).toEqual('bar'); }); }); describe('#namespaceToSpaceId', () => { - it('returns the default space id for undefined namespaces', () => { - expect(namespaceToSpaceId(undefined)).toEqual(DEFAULT_SPACE_ID); - }); - - it('returns all other namespaces as-is', () => { - expect(namespaceToSpaceId('foo')).toEqual('foo'); - }); + it('returns result of namespaceIdToString', () => { + mockNamespaceIdToString.mockReturnValue('bar'); - it('throws an error when an empty string is provided', () => { - expect(() => namespaceToSpaceId('')).toThrowErrorMatchingInlineSnapshot( - `"namespace cannot be an empty string"` - ); + const result = namespaceToSpaceId('foo'); + expect(mockNamespaceIdToString).toHaveBeenCalledWith('foo'); + expect(result).toEqual('bar'); }); }); diff --git a/x-pack/plugins/spaces/server/lib/utils/namespace.ts b/x-pack/plugins/spaces/server/lib/utils/namespace.ts index 8c7ed2ea1797d..6861d0141ea02 100644 --- a/x-pack/plugins/spaces/server/lib/utils/namespace.ts +++ b/x-pack/plugins/spaces/server/lib/utils/namespace.ts @@ -4,28 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { namespaceStringToId, namespaceIdToString } from '../../../../../../src/core/server'; -export function spaceIdToNamespace(spaceId: string): string | undefined { - if (!spaceId) { - throw new TypeError('spaceId is required'); - } - - if (spaceId === DEFAULT_SPACE_ID) { - return undefined; - } - - return spaceId; +/** + * Converts a Space ID string to its namespace ID representation. Note that a Space ID string is equivalent to a namespace string. + * + * See also: {@link namespaceStringToId}. + */ +export function spaceIdToNamespace(spaceId: string) { + return namespaceStringToId(spaceId); } -export function namespaceToSpaceId(namespace: string | undefined): string { - if (namespace === '') { - throw new TypeError('namespace cannot be an empty string'); - } - - if (!namespace) { - return DEFAULT_SPACE_ID; - } - - return namespace; +/** + * Converts a namespace ID to its Space ID string representation. Note that a Space ID string is equivalent to a namespace string. + * + * See also: {@link namespaceIdToString}. + */ +export function namespaceToSpaceId(namespace?: string) { + return namespaceIdToString(namespace); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index b604554cbc59a..180507c22ffd5 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -30,10 +30,10 @@ import { securityMock } from '../../../../../security/server/mocks'; import { ObjectType } from '@kbn/config-schema'; jest.mock('../../../../../../../src/core/server', () => { return { + ...(jest.requireActual('../../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), importSavedObjectsFromStream: jest.fn(), resolveSavedObjectsImportErrors: jest.fn(), - kibanaResponseFactory: jest.requireActual('src/core/server').kibanaResponseFactory, }; }); import { From 341052ad338fa84b0f416e1f81d348f65f1b35d4 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 20 Aug 2020 10:22:36 -0400 Subject: [PATCH 02/16] Change `bulkUpdate` method to allow per-object namespaces Includes docs changes and integration tests. --- ...ore-server.savedobjectsbulkupdateobject.md | 1 + ....savedobjectsbulkupdateobject.namespace.md | 15 +++ .../saved_objects/routes/bulk_update.ts | 1 + .../service/lib/repository.test.js | 113 +++++++++++------- .../saved_objects/service/lib/repository.ts | 35 ++++-- .../service/saved_objects_client.ts | 7 ++ src/core/server/server.api.md | 1 + ...ecure_saved_objects_client_wrapper.test.ts | 21 +++- .../secure_saved_objects_client_wrapper.ts | 15 ++- .../common/suites/bulk_update.ts | 14 ++- .../security_and_spaces/apis/bulk_update.ts | 58 ++++++--- .../security_only/apis/bulk_update.ts | 33 ++++- .../spaces_only/apis/bulk_update.ts | 51 +++++--- 13 files changed, 266 insertions(+), 99 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md index e079e0fa51aac..3ae529270bf32 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md @@ -17,5 +17,6 @@ export interface SavedObjectsBulkUpdateObject extends PickPartial<T> | The data for a Saved Object is stored as an object in the attributes property. | | [id](./kibana-plugin-core-server.savedobjectsbulkupdateobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | +| [namespace](./kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md) | string | Optional namespace string to use for this document. If this is defined, it will supersede the namespace ID that is in [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md).Note: the default namespace's string representation is 'default', and its ID representation is undefined. | | [type](./kibana-plugin-core-server.savedobjectsbulkupdateobject.type.md) | string | The type of this Saved Object. Each plugin can define it's own custom Saved Object types. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md new file mode 100644 index 0000000000000..5796e70eda980 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-core-server.savedobjectsbulkupdateobject.md) > [namespace](./kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md) + +## SavedObjectsBulkUpdateObject.namespace property + +Optional namespace string to use for this document. If this is defined, it will supersede the namespace ID that is in [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md). + +Note: the default namespace's string representation is `'default'`, and its ID representation is `undefined`. + +Signature: + +```typescript +namespace?: string; +``` diff --git a/src/core/server/saved_objects/routes/bulk_update.ts b/src/core/server/saved_objects/routes/bulk_update.ts index c112833b29f3f..882213644146a 100644 --- a/src/core/server/saved_objects/routes/bulk_update.ts +++ b/src/core/server/saved_objects/routes/bulk_update.ts @@ -40,6 +40,7 @@ export const registerBulkUpdateRoute = (router: IRouter) => { }) ) ), + namespace: schema.maybe(schema.string({ minLength: 1 })), }) ), }, 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 941c4091a66a7..87337d643ff1f 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -154,26 +154,35 @@ describe('SavedObjectsRepository', () => { validateDoc: jest.fn(), }); - const getMockGetResponse = ({ type, id, references, namespace }) => ({ - // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these - found: true, - _id: `${registry.isSingleNamespace(type) && namespace ? `${namespace}:` : ''}${type}:${id}`, - ...mockVersionProps, - _source: { - ...(registry.isSingleNamespace(type) && { namespace }), - ...(registry.isMultiNamespace(type) && { namespaces: [namespace ?? 'default'] }), - type, - [type]: { title: 'Testing' }, - references, - specialProperty: 'specialValue', - ...mockTimestampFields, - }, - }); + const getMockGetResponse = ({ type, id, references, namespace: objectNamespace }, namespace) => { + const namespaceId = + // eslint-disable-next-line no-nested-ternary + objectNamespace !== undefined + ? objectNamespace !== 'default' + ? objectNamespace + : undefined + : namespace; + return { + // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these + found: true, + _id: `${ + registry.isSingleNamespace(type) && namespaceId ? `${namespaceId}:` : '' + }${type}:${id}`, + ...mockVersionProps, + _source: { + ...(registry.isSingleNamespace(type) && { namespace: namespaceId }), + ...(registry.isMultiNamespace(type) && { namespaces: [namespaceId ?? 'default'] }), + type, + [type]: { title: 'Testing' }, + references, + specialProperty: 'specialValue', + ...mockTimestampFields, + }, + }; + }; const getMockMgetResponse = (objects, namespace) => ({ - docs: objects.map((obj) => - obj.found === false ? obj : getMockGetResponse({ ...obj, namespace }) - ), + docs: objects.map((obj) => (obj.found === false ? obj : getMockGetResponse(obj, namespace))), }); expect.extend({ @@ -1311,29 +1320,57 @@ describe('SavedObjectsRepository', () => { const getId = (type, id) => `${namespace}:${type}:${id}`; await bulkUpdateSuccess([obj1, obj2], { namespace }); expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + + jest.clearAllMocks(); + // test again with object namespace string that supersedes the operation's namespace ID + await bulkUpdateSuccess([ + { ...obj1, namespace }, + { ...obj2, namespace }, + ]); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { const getId = (type, id) => `${type}:${id}`; await bulkUpdateSuccess([obj1, obj2]); expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + + jest.clearAllMocks(); + // test again with object namespace string that supersedes the operation's namespace ID + await bulkUpdateSuccess( + [ + { ...obj1, namespace: 'default' }, + { ...obj2, namespace: 'default' }, + ], + { namespace } + ); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { const getId = (type, id) => `${type}:${id}`; - const objects1 = [{ ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }]; - await bulkUpdateSuccess(objects1, { namespace }); - expectClientCallArgsAction(objects1, { method: 'update', getId }); - client.bulk.mockClear(); const overrides = { // bulkUpdate uses a preflight `get` request for multi-namespace saved objects, and specifies that version on `update` // we aren't testing for this here, but we need to include Jest assertions so this test doesn't fail if_primary_term: expect.any(Number), if_seq_no: expect.any(Number), }; - const objects2 = [{ ...obj2, type: MULTI_NAMESPACE_TYPE }]; - await bulkUpdateSuccess(objects2, { namespace }); - expectClientCallArgsAction(objects2, { method: 'update', getId, overrides }, 2); + const _obj1 = { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }; + const _obj2 = { ...obj2, type: MULTI_NAMESPACE_TYPE }; + + await bulkUpdateSuccess([_obj1], { namespace }); + expectClientCallArgsAction([_obj1], { method: 'update', getId }); + client.bulk.mockClear(); + await bulkUpdateSuccess([_obj2], { namespace }); + expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }, 2); + + jest.clearAllMocks(); + // test again with object namespace string that supersedes the operation's namespace ID + await bulkUpdateSuccess([{ ..._obj1, namespace }]); + expectClientCallArgsAction([_obj1], { method: 'update', getId }); + client.bulk.mockClear(); + await bulkUpdateSuccess([{ ..._obj2, namespace }]); + expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }, 2); }); }); @@ -1684,11 +1721,7 @@ describe('SavedObjectsRepository', () => { }); it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { - const response = getMockGetResponse({ - type: MULTI_NAMESPACE_TYPE, - id, - namespace: 'bar-namespace', - }); + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }, 'bar-namespace'); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -1785,7 +1818,7 @@ describe('SavedObjectsRepository', () => { const deleteSuccess = async (type, id, options) => { if (registry.isMultiNamespace(type)) { - const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); + const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) ); @@ -1911,7 +1944,7 @@ describe('SavedObjectsRepository', () => { }); it(`throws when the type is multi-namespace and the document exists, but not in this namespace`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }, namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -2440,7 +2473,7 @@ describe('SavedObjectsRepository', () => { const namespace = 'foo-namespace'; const getSuccess = async (type, id, options) => { - const response = getMockGetResponse({ type, id, namespace: options?.namespace }); + const response = getMockGetResponse({ type, id }, options?.namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -2529,7 +2562,7 @@ describe('SavedObjectsRepository', () => { }); it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }, namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -2579,7 +2612,7 @@ describe('SavedObjectsRepository', () => { const incrementCounterSuccess = async (type, id, field, options) => { const isMultiNamespace = registry.isMultiNamespace(type); if (isMultiNamespace) { - const response = getMockGetResponse({ type, id, namespace: options?.namespace }); + const response = getMockGetResponse({ type, id }, options?.namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -2716,11 +2749,7 @@ describe('SavedObjectsRepository', () => { }); it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { - const response = getMockGetResponse({ - type: MULTI_NAMESPACE_TYPE, - id, - namespace: 'bar-namespace', - }); + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }, 'bar-namespace'); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -3152,7 +3181,7 @@ describe('SavedObjectsRepository', () => { const updateSuccess = async (type, id, attributes, options) => { if (registry.isMultiNamespace(type)) { - const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); + const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) ); @@ -3349,7 +3378,7 @@ describe('SavedObjectsRepository', () => { }); it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }, namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 941488d2264e6..0027c04395f20 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -63,7 +63,7 @@ import { } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; -import { namespaceIdToString } from './namespace'; +import { namespaceIdToString, namespaceStringToId } from './namespace'; // 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. @@ -1131,7 +1131,9 @@ export class SavedObjectsRepository { }; } - const { attributes, references, version } = object; + const { attributes, references, version, namespace: objectNamespace } = object; + // `objectNamespace` is a namespace string, while `namespace` is a namespace ID. + // The object namespace string, if defined, will supersede the operation's namespace ID. const documentToSave = { [type]: attributes, @@ -1148,16 +1150,22 @@ export class SavedObjectsRepository { id, version, documentToSave, + objectNamespace, ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), }, }; }); + const getNamespaceId = (objectNamespace?: string) => + objectNamespace !== undefined ? namespaceStringToId(objectNamespace) : namespace; + const getNamespaceString = (objectNamespace?: string) => + objectNamespace ?? namespaceIdToString(namespace); + const bulkGetDocs = expectedBulkGetResults .filter(isRight) .filter(({ value }) => value.esRequestIndex !== undefined) - .map(({ value: { type, id } }) => ({ - _id: this._serializer.generateRawId(namespace, type, id), + .map(({ value: { type, id, objectNamespace } }) => ({ + _id: this._serializer.generateRawId(getNamespaceId(objectNamespace), type, id), _index: this.getIndexForType(type), _source: ['type', 'namespaces'], })); @@ -1182,14 +1190,25 @@ export class SavedObjectsRepository { return expectedBulkGetResult; } - const { esRequestIndex, id, type, version, documentToSave } = expectedBulkGetResult.value; + const { + esRequestIndex, + id, + type, + version, + documentToSave, + objectNamespace, + } = expectedBulkGetResult.value; + let namespaces; let versionProperties; if (esRequestIndex !== undefined) { const indexFound = bulkGetResponse?.statusCode !== 404; const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined; const docFound = indexFound && actualResult.found === true; - if (!docFound || !this.rawDocExistsInNamespace(actualResult, namespace)) { + if ( + !docFound || + !this.rawDocExistsInNamespace(actualResult, getNamespaceId(objectNamespace)) + ) { return { tag: 'Left' as 'Left', error: { @@ -1205,7 +1224,7 @@ export class SavedObjectsRepository { versionProperties = getExpectedVersionProperties(version, actualResult); } else { if (this._registry.isSingleNamespace(type)) { - namespaces = [namespaceIdToString(namespace)]; + namespaces = [getNamespaceString(objectNamespace)]; } versionProperties = getExpectedVersionProperties(version); } @@ -1221,7 +1240,7 @@ export class SavedObjectsRepository { bulkUpdateParams.push( { update: { - _id: this._serializer.generateRawId(namespace, type, id), + _id: this._serializer.generateRawId(getNamespaceId(objectNamespace), type, id), _index: this.getIndexForType(type), ...versionProperties, }, diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 6a9f4f5143e84..9b273bc61f61e 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -75,6 +75,13 @@ export interface SavedObjectsBulkUpdateObject type: string; /** {@inheritdoc SavedObjectAttributes} */ attributes: Partial; + /** + * Optional namespace string to use for this document. If this is defined, it will supersede the namespace ID that is in + * {@link SavedObjectsUpdateOptions}. + * + * Note: the default namespace's string representation is `'default'`, and its ID representation is `undefined`. + **/ + namespace?: string; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index da28ca3c3d53f..8e043e225daca 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2079,6 +2079,7 @@ export interface SavedObjectsBulkResponse { export interface SavedObjectsBulkUpdateObject extends Pick { attributes: Partial; id: string; + namespace?: string; type: string; } diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 1cf879adc5415..605b804e65168 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -117,7 +117,11 @@ const expectSuccess = async (fn: Function, args: Record) => { return result; }; -const expectPrivilegeCheck = async (fn: Function, args: Record) => { +const expectPrivilegeCheck = async ( + fn: Function, + args: Record, + namespacesOverride?: string[] +) => { clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( getMockCheckPrivilegesFailure ); @@ -131,7 +135,7 @@ const expectPrivilegeCheck = async (fn: Function, args: Record) => expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(1); expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( actions, - args.options?.namespace ?? args.options?.namespaces + namespacesOverride ?? args.options?.namespace ?? args.options?.namespaces ); }; @@ -468,7 +472,18 @@ describe('#bulkUpdate', () => { test(`checks privileges for user, actions, and namespace`, async () => { const objects = [obj1, obj2]; - await expectPrivilegeCheck(client.bulkUpdate, { objects, options }); + const namespacesOverride = [options.namespace]; // the bulkCreate function checks privileges as an array + await expectPrivilegeCheck(client.bulkUpdate, { objects, options }, namespacesOverride); + }); + + test(`checks privileges for object namespaces if present`, async () => { + const objects = [ + { ...obj1, namespace: 'foo-ns' }, + { ...obj2, namespace: 'bar-ns' }, + ]; + const namespacesOverride = ['default', 'foo-ns', 'bar-ns']; + // use the default namespace for the options + await expectPrivilegeCheck(client.bulkUpdate, { objects, options: {} }, namespacesOverride); }); test(`filters namespaces that the user doesn't have access to`, async () => { diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 621299a0f025e..ab82c4234e49d 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -15,6 +15,7 @@ import { SavedObjectsUpdateOptions, SavedObjectsAddToNamespacesOptions, SavedObjectsDeleteFromNamespacesOptions, + namespaceIdToString, } from '../../../../../src/core/server'; import { SecurityAuditLogger } from '../audit'; import { Actions, CheckSavedObjectsPrivileges } from '../authorization'; @@ -184,12 +185,14 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: Array> = [], options: SavedObjectsBaseOptions = {} ) { - await this.ensureAuthorized( - this.getUniqueObjectTypes(objects), - 'bulk_update', - options && options.namespace, - { objects, options } - ); + const objectNamespaces = objects + .filter(({ namespace }) => namespace !== undefined) + .map(({ namespace }) => namespace!); + const namespaces = uniq([namespaceIdToString(options?.namespace), ...objectNamespaces]); + await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { + objects, + options, + }); const response = await this.baseClient.bulkUpdate(objects, options); return await this.redactSavedObjectsNamespaces(response); diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts index 0b5656004492a..2e3c55f029d29 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts @@ -8,12 +8,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; -import { - createRequest, - expectResponses, - getUrlPrefix, - getTestTitle, -} from '../lib/saved_object_test_utils'; +import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils'; import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; export interface BulkUpdateTestDefinition extends TestDefinition { @@ -21,6 +16,7 @@ export interface BulkUpdateTestDefinition extends TestDefinition { } export type BulkUpdateTestSuite = TestSuite; export interface BulkUpdateTestCase extends TestCase { + namespace?: string; // used to define individual "object namespace" strings, e.g., bulkUpdate across multiple namespaces failure?: 404; // only used for permitted response case } @@ -30,6 +26,12 @@ const NEW_ATTRIBUTE_VAL = `Updated attribute value ${Date.now()}`; const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +const createRequest = ({ type, id, namespace }: BulkUpdateTestCase) => ({ + type, + id, + ...(namespace && { namespace }), // individual "object namespace" string +}); + export function bulkUpdateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbidden = expectResponses.forbiddenTypes('bulk_update'); const expectResponseBody = ( diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts index 90f72e0b34449..1e11d1fc61110 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts @@ -39,7 +39,18 @@ const createTestCases = (spaceId: string) => { ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; const allTypes = normalTypes.concat(hiddenType); - return { normalTypes, hiddenType, allTypes }; + // an "object namespace" string can be specified for individual objects (to bulkUpdate across namespaces) + const withObjectNamespaces = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespace: DEFAULT_SPACE_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespace: SPACE_1_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, namespace: SPACE_1_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespace: DEFAULT_SPACE_ID }, // SPACE_1_ID would also work + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespace: SPACE_2_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, namespace: SPACE_2_ID }, + CASES.NAMESPACE_AGNOSTIC, // any namespace would work and would make no difference + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + return { normalTypes, hiddenType, allTypes, withObjectNamespaces }; }; export default function ({ getService }: FtrProviderContext) { @@ -51,26 +62,42 @@ export default function ({ getService }: FtrProviderContext) { supertest ); const createTests = (spaceId: string) => { - const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + const { normalTypes, hiddenType, allTypes, withObjectNamespaces } = createTestCases(spaceId); // use singleRequest to reduce execution time and/or test combined cases + const authorizedCommon = [ + createTestDefinitions(normalTypes, false, { singleRequest: true }), + createTestDefinitions(hiddenType, true), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(); return { - unauthorized: createTestDefinitions(allTypes, true), - authorized: [ - createTestDefinitions(normalTypes, false, { singleRequest: true }), - createTestDefinitions(hiddenType, true), - createTestDefinitions(allTypes, true, { - singleRequest: true, - responseBodyOverride: expectForbidden(['hiddentype']), - }), + unauthorized: [ + createTestDefinitions(allTypes, true), + createTestDefinitions(withObjectNamespaces, true, { singleRequest: true }), + ].flat(), + authorizedAtSpace: [ + authorizedCommon, + createTestDefinitions(withObjectNamespaces, true, { singleRequest: true }), + ].flat(), + authorizedAllSpaces: [ + authorizedCommon, + createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), + ].flat(), + superuser: [ + createTestDefinitions(allTypes, false, { singleRequest: true }), + createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), ].flat(), - superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), }; }; describe('_bulk_update', () => { getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { const suffix = ` within the ${spaceId} space`; - const { unauthorized, authorized, superuser } = createTests(spaceId); + const { unauthorized, authorizedAtSpace, authorizedAllSpaces, superuser } = createTests( + spaceId + ); const _addTests = (user: TestUser, tests: BulkUpdateTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, spaceId, tests }); }; @@ -85,8 +112,11 @@ export default function ({ getService }: FtrProviderContext) { ].forEach((user) => { _addTests(user, unauthorized); }); - [users.dualAll, users.allGlobally, users.allAtSpace].forEach((user) => { - _addTests(user, authorized); + [users.allAtSpace].forEach((user) => { + _addTests(user, authorizedAtSpace); + }); + [users.dualAll, users.allGlobally].forEach((user) => { + _addTests(user, authorizedAllSpaces); }); _addTests(users.superuser, superuser); }); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts index d42eb25b81cf5..39ceb5a70d1b2 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SPACES } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -13,6 +14,11 @@ import { BulkUpdateTestDefinition, } from '../../common/suites/bulk_update'; +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; const { fail404 } = testCaseFailures; const createTestCases = () => { @@ -30,7 +36,19 @@ const createTestCases = () => { ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; const allTypes = normalTypes.concat(hiddenType); - return { normalTypes, hiddenType, allTypes }; + // an "object namespace" string can be specified for individual objects (to bulkUpdate across namespaces) + // even if the Spaces plugin is disabled, this should work, as `namespace` is handled by the Core API + const withObjectNamespaces = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespace: DEFAULT_SPACE_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespace: SPACE_1_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, namespace: SPACE_1_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespace: DEFAULT_SPACE_ID }, // SPACE_1_ID would also work + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespace: SPACE_2_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, namespace: SPACE_2_ID }, + CASES.NAMESPACE_AGNOSTIC, // any namespace would work and would make no difference + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + return { normalTypes, hiddenType, allTypes, withObjectNamespaces }; }; export default function ({ getService }: FtrProviderContext) { @@ -42,10 +60,13 @@ export default function ({ getService }: FtrProviderContext) { supertest ); const createTests = () => { - const { normalTypes, hiddenType, allTypes } = createTestCases(); + const { normalTypes, hiddenType, allTypes, withObjectNamespaces } = createTestCases(); // use singleRequest to reduce execution time and/or test combined cases return { - unauthorized: createTestDefinitions(allTypes, true), + unauthorized: [ + createTestDefinitions(allTypes, true), + createTestDefinitions(withObjectNamespaces, true, { singleRequest: true }), + ].flat(), authorized: [ createTestDefinitions(normalTypes, false, { singleRequest: true }), createTestDefinitions(hiddenType, true), @@ -53,8 +74,12 @@ export default function ({ getService }: FtrProviderContext) { singleRequest: true, responseBodyOverride: expectForbidden(['hiddentype']), }), + createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), + ].flat(), + superuser: [ + createTestDefinitions(allTypes, false, { singleRequest: true }), + createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), ].flat(), - superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), }; }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts index 93e44e357918a..dac85736f1fc5 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts @@ -16,22 +16,38 @@ const { } = SPACES; const { fail404 } = testCaseFailures; -const createTestCases = (spaceId: string) => [ +const createTestCases = (spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, - { - ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), - }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, - CASES.NAMESPACE_AGNOSTIC, - { ...CASES.HIDDEN, ...fail404() }, - { ...CASES.DOES_NOT_EXIST, ...fail404() }, -]; + const normal = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + + // an "object namespace" string can be specified for individual objects (to bulkUpdate across namespaces) + // even if the Spaces plugin is disabled, this should work, as `namespace` is handled by the Core API + const withObjectNamespaces = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespace: DEFAULT_SPACE_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespace: SPACE_1_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, namespace: SPACE_1_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespace: DEFAULT_SPACE_ID }, // SPACE_1_ID would also work + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespace: SPACE_2_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, namespace: SPACE_2_ID }, + CASES.NAMESPACE_AGNOSTIC, // any namespace would work and would make no difference + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + return { normal, withObjectNamespaces }; +}; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -39,8 +55,11 @@ export default function ({ getService }: FtrProviderContext) { const { addTests, createTestDefinitions } = bulkUpdateTestSuiteFactory(esArchiver, supertest); const createTests = (spaceId: string) => { - const testCases = createTestCases(spaceId); - return createTestDefinitions(testCases, false, { singleRequest: true }); + const { normal, withObjectNamespaces } = createTestCases(spaceId); + return [ + createTestDefinitions(normal, false, { singleRequest: true }), + createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), + ].flat(); }; describe('_bulk_update', () => { From 2096fc376c9864af6f428699645f4ca3e963eb25 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 31 Aug 2020 13:34:42 -0400 Subject: [PATCH 03/16] Add comment per review feedback --- src/core/server/saved_objects/service/lib/repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 0a329d595aa21..bf32a3ead0582 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1319,6 +1319,7 @@ export class SavedObjectsRepository { versionProperties = getExpectedVersionProperties(version, actualResult); } else { if (this._registry.isSingleNamespace(type)) { + // if `objectNamespace` is undefined, fall back to `options.namespace` namespaces = [getNamespaceString(objectNamespace)]; } versionProperties = getExpectedVersionProperties(version); From 440d987726fc9b774cb74ef1b5588fccacd2701a Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 31 Aug 2020 13:59:49 -0400 Subject: [PATCH 04/16] Tweak secure saved objects client wrapper authZ checks --- .../check_saved_objects_privileges.test.ts | 31 +++++++++++++++++-- .../check_saved_objects_privileges.ts | 13 ++++++-- ...ecure_saved_objects_client_wrapper.test.ts | 4 +-- .../secure_saved_objects_client_wrapper.ts | 7 ++--- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts index 5e38045b88c74..b393c4a9e1a04 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts @@ -31,7 +31,9 @@ beforeEach(() => { mockSpacesService = { getSpaceId: jest.fn(), - namespaceToSpaceId: jest.fn().mockImplementation((namespace: string) => `${namespace}-id`), + namespaceToSpaceId: jest + .fn() + .mockImplementation((namespace: string = 'default') => `${namespace}-id`), }; }); @@ -41,8 +43,6 @@ describe('#checkSavedObjectsPrivileges', () => { const namespace2 = 'qux'; describe('when checking multiple namespaces', () => { - const namespaces = [namespace1, namespace2]; - test(`throws an error when using an empty namespaces array`, async () => { const checkSavedObjectsPrivileges = createFactory(); @@ -58,6 +58,7 @@ describe('#checkSavedObjectsPrivileges', () => { mockCheckPrivileges.atSpaces.mockReturnValue(expectedResult as any); const checkSavedObjectsPrivileges = createFactory(); + const namespaces = [namespace1, namespace2]; const result = await checkSavedObjectsPrivileges(actions, namespaces); expect(result).toBe(expectedResult); @@ -70,6 +71,30 @@ describe('#checkSavedObjectsPrivileges', () => { const spaceIds = mockSpacesService!.namespaceToSpaceId.mock.results.map((x) => x.value); expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, actions); }); + + test(`de-duplicates namespaces`, async () => { + const expectedResult = Symbol(); + mockCheckPrivileges.atSpaces.mockReturnValue(expectedResult as any); + const checkSavedObjectsPrivileges = createFactory(); + + const namespaces = [undefined, 'default', namespace1, namespace1]; + const result = await checkSavedObjectsPrivileges(actions, namespaces); + + expect(result).toBe(expectedResult); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledTimes(4); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(1, undefined); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(2, 'default'); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(3, namespace1); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(4, namespace1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledTimes(1); + const spaceIds = [ + mockSpacesService!.namespaceToSpaceId(undefined), // deduplicated with 'default' + mockSpacesService!.namespaceToSpaceId(namespace1), // deduplicated with namespace1 + ]; + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, actions); + }); }); describe('when checking a single namespace', () => { diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts index 0c2260542bf72..6d2f724dae948 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts @@ -14,9 +14,13 @@ export type CheckSavedObjectsPrivilegesWithRequest = ( export type CheckSavedObjectsPrivileges = ( actions: string | string[], - namespaceOrNamespaces?: string | string[] + namespaceOrNamespaces?: string | Array ) => Promise; +function uniq(arr: T[]): T[] { + return Array.from(new Set(arr)); +} + export const checkSavedObjectsPrivilegesWithRequestFactory = ( checkPrivilegesWithRequest: CheckPrivilegesWithRequest, getSpacesService: () => SpacesService | undefined @@ -26,7 +30,7 @@ export const checkSavedObjectsPrivilegesWithRequestFactory = ( ): CheckSavedObjectsPrivileges { return async function checkSavedObjectsPrivileges( actions: string | string[], - namespaceOrNamespaces?: string | string[] + namespaceOrNamespaces?: string | Array ) { const spacesService = getSpacesService(); if (!spacesService) { @@ -37,7 +41,10 @@ export const checkSavedObjectsPrivilegesWithRequestFactory = ( if (!namespaceOrNamespaces.length) { throw new Error(`Can't check saved object privileges for 0 namespaces`); } - const spaceIds = namespaceOrNamespaces.map((x) => spacesService.namespaceToSpaceId(x)); + const spaceIds = uniq( + namespaceOrNamespaces.map((x) => spacesService.namespaceToSpaceId(x)) + ); + return await checkPrivilegesWithRequest(request).atSpaces(spaceIds, actions); } else { // Spaces enabled, authorizing against a single space diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index a4cb13957916d..2cf072453a3c7 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -120,7 +120,7 @@ const expectSuccess = async (fn: Function, args: Record, action?: s const expectPrivilegeCheck = async ( fn: Function, args: Record, - namespacesOverride?: string[] + namespacesOverride?: Array ) => { clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( getMockCheckPrivilegesFailure @@ -496,7 +496,7 @@ describe('#bulkUpdate', () => { { ...obj1, namespace: 'foo-ns' }, { ...obj2, namespace: 'bar-ns' }, ]; - const namespacesOverride = ['default', 'foo-ns', 'bar-ns']; + const namespacesOverride = [undefined, 'foo-ns', 'bar-ns']; // use the default namespace for the options await expectPrivilegeCheck(client.bulkUpdate, { objects, options: {} }, namespacesOverride); }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index ed9086401fc5e..1c294b7cd1775 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -16,7 +16,6 @@ import { SavedObjectsUpdateOptions, SavedObjectsAddToNamespacesOptions, SavedObjectsDeleteFromNamespacesOptions, - namespaceIdToString, } from '../../../../../src/core/server'; import { SecurityAuditLogger } from '../audit'; import { Actions, CheckSavedObjectsPrivileges } from '../authorization'; @@ -203,7 +202,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const objectNamespaces = objects .filter(({ namespace }) => namespace !== undefined) .map(({ namespace }) => namespace!); - const namespaces = uniq([namespaceIdToString(options?.namespace), ...objectNamespaces]); + const namespaces = [options?.namespace, ...objectNamespaces]; await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { objects, options, @@ -215,7 +214,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra private async checkPrivileges( actions: string | string[], - namespaceOrNamespaces?: string | string[] + namespaceOrNamespaces?: string | Array ) { try { return await this.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespaceOrNamespaces); @@ -227,7 +226,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra private async ensureAuthorized( typeOrTypes: string | string[], action: string, - namespaceOrNamespaces?: string | string[], + namespaceOrNamespaces?: string | Array, args?: Record, auditAction: string = action, requiresAll = true From 0b2f62cbc89018e705adc9c09f7c917cf79518f6 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 31 Aug 2020 14:52:58 -0400 Subject: [PATCH 05/16] Don't expose core namespace conversion methods in the public contract This partially reverts commit fa6ae1cd736a3de905ec68dec7b51077e45a12f1. --- .../core/server/kibana-plugin-core-server.md | 2 - ...-plugin-core-server.namespaceidtostring.md | 13 ------ ...-plugin-core-server.namespacestringtoid.md | 13 ------ src/core/server/index.ts | 2 - .../server/saved_objects/service/index.ts | 2 - .../server/saved_objects/service/lib/index.ts | 2 - src/core/server/server.api.md | 6 --- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 1 - .../resolve_copy_conflicts.test.ts | 1 - .../server/lib/utils/__mocks__/index.ts | 14 ------ .../spaces/server/lib/utils/namespace.test.ts | 46 +++++++++++++------ .../spaces/server/lib/utils/namespace.ts | 36 +++++++++------ .../routes/api/external/copy_to_space.test.ts | 2 +- 13 files changed, 53 insertions(+), 87 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md delete mode 100644 x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index a29d550e831ae..89330d2a86f76 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -224,8 +224,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | Variable | Description | | --- | --- | | [kibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Set of helpers used to create KibanaResponse to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution. | -| [namespaceIdToString](./kibana-plugin-core-server.namespaceidtostring.md) | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined namespace ID (which has a namespace string of 'default'). | -| [namespaceStringToId](./kibana-plugin-core-server.namespacestringtoid.md) | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default' namespace string (which has a namespace ID of undefined). | | [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md) | The current "level" of availability of a service. | | [validBodyOutput](./kibana-plugin-core-server.validbodyoutput.md) | The set of valid body.output | diff --git a/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md b/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md deleted file mode 100644 index 5c9f093531e1c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [namespaceIdToString](./kibana-plugin-core-server.namespaceidtostring.md) - -## namespaceIdToString variable - -Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the `undefined` namespace ID (which has a namespace string of `'default'`). - -Signature: - -```typescript -namespaceIdToString: (namespace?: string | undefined) => string -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md b/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md deleted file mode 100644 index 39d6a13aa7fb0..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [namespaceStringToId](./kibana-plugin-core-server.namespacestringtoid.md) - -## namespaceStringToId variable - -Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the `'default'` namespace string (which has a namespace ID of `undefined`). - -Signature: - -```typescript -namespaceStringToId: (namespace: string) => string | undefined -``` diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 9753f725c7c96..5422cbc2180ef 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -303,8 +303,6 @@ export { exportSavedObjectsToStream, importSavedObjectsFromStream, resolveSavedObjectsImportErrors, - namespaceIdToString, - namespaceStringToId, } from './saved_objects'; export { diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 245df328db665..9f625b4732e26 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -58,8 +58,6 @@ export { SavedObjectsErrorHelpers, SavedObjectsClientFactory, SavedObjectsClientFactoryProvider, - namespaceIdToString, - namespaceStringToId, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index cfae6ae9b62d5..e103120388e35 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -30,5 +30,3 @@ export { } from './scoped_client_provider'; export { SavedObjectsErrorHelpers } from './errors'; - -export { namespaceIdToString, namespaceStringToId } from './namespace'; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4924685769011..dd6fa64278290 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1596,12 +1596,6 @@ export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulPart // @public export type MutatingOperationRefreshSetting = boolean | 'wait_for'; -// @public -export const namespaceIdToString: (namespace?: string | undefined) => string; - -// @public -export const namespaceStringToId: (namespace: string) => string | undefined; - // Warning: (ae-missing-release-tag) "NodesVersionCompatibility" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 1cec7b769fa26..d49dfa2015dc6 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -20,7 +20,6 @@ import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; jest.mock('../../../../../../src/core/server', () => { return { - ...(jest.requireActual('../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), importSavedObjectsFromStream: jest.fn(), }; diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 37181c9d81649..6a77bf7397cb5 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -20,7 +20,6 @@ import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_ jest.mock('../../../../../../src/core/server', () => { return { - ...(jest.requireActual('../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), resolveSavedObjectsImportErrors: jest.fn(), }; diff --git a/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts b/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts deleted file mode 100644 index 2bb23d0304858..0000000000000 --- a/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -const mockNamespaceIdToString = jest.fn(); -const mockNamespaceStringToId = jest.fn(); -jest.mock('../../../../../../../src/core/server', () => ({ - namespaceIdToString: mockNamespaceIdToString, - namespaceStringToId: mockNamespaceStringToId, -})); - -export { mockNamespaceIdToString, mockNamespaceStringToId }; diff --git a/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts b/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts index 79d3dda301045..a81a5f3cee187 100644 --- a/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts +++ b/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts @@ -4,29 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockNamespaceIdToString, mockNamespaceStringToId } from './__mocks__'; +import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { spaceIdToNamespace, namespaceToSpaceId } from './namespace'; -beforeEach(() => { - jest.clearAllMocks(); -}); - describe('#spaceIdToNamespace', () => { - it('returns result of namespaceStringToId', () => { - mockNamespaceStringToId.mockReturnValue('bar'); + it('converts the default space to undefined', () => { + expect(spaceIdToNamespace(DEFAULT_SPACE_ID)).toBeUndefined(); + }); + + it('returns non-default spaces as-is', () => { + expect(spaceIdToNamespace('foo')).toEqual('foo'); + }); - const result = spaceIdToNamespace('foo'); - expect(mockNamespaceStringToId).toHaveBeenCalledWith('foo'); - expect(result).toEqual('bar'); + it('throws an error when a spaceId is not provided', () => { + // @ts-ignore ts knows this isn't right + expect(() => spaceIdToNamespace()).toThrowErrorMatchingInlineSnapshot(`"spaceId is required"`); + + // @ts-ignore ts knows this isn't right + expect(() => spaceIdToNamespace(null)).toThrowErrorMatchingInlineSnapshot( + `"spaceId is required"` + ); + + expect(() => spaceIdToNamespace('')).toThrowErrorMatchingInlineSnapshot( + `"spaceId is required"` + ); }); }); describe('#namespaceToSpaceId', () => { - it('returns result of namespaceIdToString', () => { - mockNamespaceIdToString.mockReturnValue('bar'); + it('returns the default space id for undefined namespaces', () => { + expect(namespaceToSpaceId(undefined)).toEqual(DEFAULT_SPACE_ID); + }); + + it('returns all other namespaces as-is', () => { + expect(namespaceToSpaceId('foo')).toEqual('foo'); + }); - const result = namespaceToSpaceId('foo'); - expect(mockNamespaceIdToString).toHaveBeenCalledWith('foo'); - expect(result).toEqual('bar'); + it('throws an error when an empty string is provided', () => { + expect(() => namespaceToSpaceId('')).toThrowErrorMatchingInlineSnapshot( + `"namespace cannot be an empty string"` + ); }); }); diff --git a/x-pack/plugins/spaces/server/lib/utils/namespace.ts b/x-pack/plugins/spaces/server/lib/utils/namespace.ts index 6861d0141ea02..8c7ed2ea1797d 100644 --- a/x-pack/plugins/spaces/server/lib/utils/namespace.ts +++ b/x-pack/plugins/spaces/server/lib/utils/namespace.ts @@ -4,22 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { namespaceStringToId, namespaceIdToString } from '../../../../../../src/core/server'; +import { DEFAULT_SPACE_ID } from '../../../common/constants'; -/** - * Converts a Space ID string to its namespace ID representation. Note that a Space ID string is equivalent to a namespace string. - * - * See also: {@link namespaceStringToId}. - */ -export function spaceIdToNamespace(spaceId: string) { - return namespaceStringToId(spaceId); +export function spaceIdToNamespace(spaceId: string): string | undefined { + if (!spaceId) { + throw new TypeError('spaceId is required'); + } + + if (spaceId === DEFAULT_SPACE_ID) { + return undefined; + } + + return spaceId; } -/** - * Converts a namespace ID to its Space ID string representation. Note that a Space ID string is equivalent to a namespace string. - * - * See also: {@link namespaceIdToString}. - */ -export function namespaceToSpaceId(namespace?: string) { - return namespaceIdToString(namespace); +export function namespaceToSpaceId(namespace: string | undefined): string { + if (namespace === '') { + throw new TypeError('namespace cannot be an empty string'); + } + + if (!namespace) { + return DEFAULT_SPACE_ID; + } + + return namespace; } diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index dce6de908cfcb..bec3a5dcb0b71 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -30,10 +30,10 @@ import { securityMock } from '../../../../../security/server/mocks'; import { ObjectType } from '@kbn/config-schema'; jest.mock('../../../../../../../src/core/server', () => { return { - ...(jest.requireActual('../../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), importSavedObjectsFromStream: jest.fn(), resolveSavedObjectsImportErrors: jest.fn(), + kibanaResponseFactory: jest.requireActual('src/core/server').kibanaResponseFactory, }; }); import { From 1228d61414e506bbb06943f326abb961464266ff Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 1 Sep 2020 09:16:51 -0400 Subject: [PATCH 06/16] Remove @public tag from jsdocs that are no longer exported from core --- src/core/server/saved_objects/service/lib/namespace.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/server/saved_objects/service/lib/namespace.ts b/src/core/server/saved_objects/service/lib/namespace.ts index 385725ca6e948..bc0691c0bb1db 100644 --- a/src/core/server/saved_objects/service/lib/namespace.ts +++ b/src/core/server/saved_objects/service/lib/namespace.ts @@ -20,7 +20,6 @@ export const DEFAULT_NAMESPACE_STRING = 'default'; /** - * @public * Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with * the exception of the `undefined` namespace ID (which has a namespace string of `'default'`). * @@ -35,7 +34,6 @@ export const namespaceIdToString = (namespace?: string) => { }; /** - * @public * Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with * the exception of the `'default'` namespace string (which has a namespace ID of `undefined`). * From 46bc77d41e7cef1188cb0eceaa3187d1aa98dda1 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 1 Sep 2020 09:58:36 -0400 Subject: [PATCH 07/16] Normalize namespace strings for saved object CRUD operations Now, options.namespace can use `'default'` interchangeably with `undefined`. --- .../service/lib/repository.test.js | 76 ++++++++++++++++++- .../saved_objects/service/lib/repository.ts | 33 +++++--- 2 files changed, 98 insertions(+), 11 deletions(-) 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 30a3df20a2dc4..5a00bdc197464 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -597,6 +597,16 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await bulkCreateSuccess([obj1, obj2], { namespace: 'default' }); + const expected = expect.not.objectContaining({ namespace: 'default' }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => { const objects = [ { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, @@ -994,6 +1004,12 @@ describe('SavedObjectsRepository', () => { _expectClientCallArgs([obj1, obj2], { getId }); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + const getId = (type, id) => `${type}:${id}`; + await bulkGetSuccess([obj1, obj2], { namespace: 'default' }); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { const getId = (type, id) => `${type}:${id}`; let objects = [obj1, obj2].map((obj) => ({ ...obj, type: NAMESPACE_AGNOSTIC_TYPE })); @@ -1368,6 +1384,12 @@ describe('SavedObjectsRepository', () => { expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + const getId = (type, id) => `${type}:${id}`; + await bulkUpdateSuccess([obj1, obj2], { namespace: 'default' }); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { const getId = (type, id) => `${type}:${id}`; const overrides = { @@ -1631,6 +1653,12 @@ describe('SavedObjectsRepository', () => { _expectClientCallArgs([obj1, obj2], { getId }); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + const getId = (type, id) => `${type}:${id}`; + await checkConflictsSuccess([obj1, obj2], { namespace: 'default' }); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { const getId = (type, id) => `${type}:${id}`; // obj3 is multi-namespace, and obj6 is namespace-agnostic @@ -1855,6 +1883,16 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await createSuccess(type, attributes, { id, namespace: 'default' }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); expect(client.create).toHaveBeenCalledWith( @@ -2070,6 +2108,14 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await deleteSuccess(type, id, { namespace: 'default' }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${type}:${id}` }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await deleteSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); expect(client.delete).toHaveBeenCalledWith( @@ -2698,6 +2744,16 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await getSuccess(type, id, { namespace: 'default' }); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await getSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); expect(client.get).toHaveBeenCalledWith( @@ -2879,6 +2935,16 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await incrementCounterSuccess(type, id, field, { namespace: 'default' }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, field, { namespace }); expect(client.update).toHaveBeenCalledWith( @@ -3238,7 +3304,7 @@ describe('SavedObjectsRepository', () => { expect(client.update).not.toHaveBeenCalled(); }); - it(`throws when type is not namespace-agnostic`, async () => { + it(`throws when type is not multi-namespace`, async () => { const test = async (type) => { const message = `${type} doesn't support multiple namespaces`; await expectBadRequestError(type, id, [namespace1, namespace2], message); @@ -3511,6 +3577,14 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await updateSuccess(type, id, attributes, { references, namespace: 'default' }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await updateSuccess(NAMESPACE_AGNOSTIC_TYPE, id, attributes, { namespace }); expect(client.update).toHaveBeenCalledWith( diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index bf32a3ead0582..c73dec7797801 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -221,13 +221,13 @@ export class SavedObjectsRepository { const { id, migrationVersion, - namespace, overwrite = false, references = [], refresh = DEFAULT_REFRESH_SETTING, originId, version, } = options; + const namespace = normalizeNamespace(options.namespace); if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); @@ -294,7 +294,8 @@ export class SavedObjectsRepository { objects: Array>, options: SavedObjectsCreateOptions = {} ): Promise> { - const { namespace, overwrite = false, refresh = DEFAULT_REFRESH_SETTING } = options; + const { overwrite = false, refresh = DEFAULT_REFRESH_SETTING } = options; + const namespace = normalizeNamespace(options.namespace); const time = this._getCurrentTime(); let bulkGetRequestIndexCounter = 0; @@ -469,7 +470,7 @@ export class SavedObjectsRepository { return { errors: [] }; } - const { namespace } = options; + const namespace = normalizeNamespace(options.namespace); let bulkGetRequestIndexCounter = 0; const expectedBulkGetResults: Either[] = objects.map((object) => { @@ -552,7 +553,8 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + const { refresh = DEFAULT_REFRESH_SETTING } = options; + const namespace = normalizeNamespace(options.namespace); const rawId = this._serializer.generateRawId(namespace, type, id); let preflightResult: SavedObjectsRawDoc | undefined; @@ -659,7 +661,7 @@ export class SavedObjectsRepository { } `, lang: 'painless', - params: { namespace: namespaceIdToString(namespace) }, + params: { namespace }, }, conflicts: 'proceed', ...getSearchDsl(this._mappings, this._registry, { @@ -815,7 +817,7 @@ export class SavedObjectsRepository { objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ): Promise> { - const { namespace } = options; + const namespace = normalizeNamespace(options.namespace); if (objects.length === 0) { return { saved_objects: [] }; @@ -921,7 +923,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { namespace } = options; + const namespace = normalizeNamespace(options.namespace); const { body, statusCode } = await this.client.get>( { @@ -979,7 +981,8 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { version, namespace, references, refresh = DEFAULT_REFRESH_SETTING } = options; + const { version, references, refresh = DEFAULT_REFRESH_SETTING } = options; + const namespace = normalizeNamespace(options.namespace); let preflightResult: SavedObjectsRawDoc | undefined; if (this._registry.isMultiNamespace(type)) { @@ -1061,6 +1064,7 @@ export class SavedObjectsRepository { } const { version, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + // we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used const rawId = this._serializer.generateRawId(undefined, type, id); const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); @@ -1123,6 +1127,7 @@ export class SavedObjectsRepository { } const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + // we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used const rawId = this._serializer.generateRawId(undefined, type, id); const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); @@ -1209,7 +1214,7 @@ export class SavedObjectsRepository { options: SavedObjectsBulkUpdateOptions = {} ): Promise> { const time = this._getCurrentTime(); - const { namespace } = options; + const namespace = normalizeNamespace(options.namespace); let bulkGetRequestIndexCounter = 0; const expectedBulkGetResults: Either[] = objects.map((object) => { @@ -1422,7 +1427,8 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } - const { migrationVersion, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING } = options; + const namespace = normalizeNamespace(options.namespace); const time = this._getCurrentTime(); let savedObjectNamespace; @@ -1662,6 +1668,13 @@ function getSavedObjectNamespaces( return [namespaceIdToString(namespace)]; } +/** + * Ensure that a namespace is always in its namespace ID representation. + * This allows `'default'` to be used interchangeably with `undefined`. + */ +const normalizeNamespace = (namespace?: string) => + namespace === undefined ? namespace : namespaceStringToId(namespace); + /** * Extracts the contents of a decorated error to return the attributes for bulk operations. */ From ea2235f5cfd1978328776d8f4420993d67de9044 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 2 Sep 2020 14:38:30 -0400 Subject: [PATCH 08/16] Change `bulkUpdate` in ESO plugin to use object namespace --- ...ypted_saved_objects_client_wrapper.test.ts | 42 +++++++++++++++---- .../encrypted_saved_objects_client_wrapper.ts | 4 +- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index f8d66b8ecac27..cd7e2ebb8aaa1 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -555,7 +555,12 @@ describe('#bulkUpdate', () => { }); describe('namespace', () => { - const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => { + const doTest = async ( + optionsNamespace: string | undefined, + expectOptionsNamespaceInDescriptor: boolean, + objectNamespace: string | undefined, + expectObjectNamespaceInDescriptor: boolean + ) => { const docs = [ { id: 'some-id', @@ -566,12 +571,13 @@ describe('#bulkUpdate', () => { attrThree: 'three', }, version: 'some-version', + namespace: objectNamespace, }, ]; - const options = { namespace }; + const options = { namespace: optionsNamespace }; mockBaseClient.bulkUpdate.mockResolvedValue({ - saved_objects: docs.map((doc) => ({ ...doc, references: undefined })), + saved_objects: docs.map(({ namespace, ...doc }) => ({ ...doc, references: undefined })), }); await expect(wrapper.bulkUpdate(docs, options)).resolves.toEqual({ @@ -594,7 +600,11 @@ describe('#bulkUpdate', () => { { type: 'known-type', id: 'some-id', - namespace: expectNamespaceInDescriptor ? namespace : undefined, + namespace: expectObjectNamespaceInDescriptor + ? objectNamespace + : expectOptionsNamespaceInDescriptor + ? optionsNamespace + : undefined, }, { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }, { user: mockAuthenticatedUser() } @@ -612,6 +622,7 @@ describe('#bulkUpdate', () => { attrThree: 'three', }, version: 'some-version', + namespace: objectNamespace, references: undefined, }, @@ -620,13 +631,26 @@ describe('#bulkUpdate', () => { ); }; - it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => { - await doTest('some-namespace', true); + it('does not use options `namespace` or object `namespace` to encrypt attributes if neither are specified', async () => { + await doTest(undefined, false, undefined, false); }); - it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => { - mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); - await doTest('some-namespace', false); + describe('with a single-namespace type', () => { + it('uses options `namespace` to encrypt attributes if it is specified and object `namespace` is not', async () => { + await doTest('some-namespace', true, undefined, false); + }); + + it('uses object `namespace` to encrypt attributes if it is specified', async () => { + // object namespace supersedes options namespace + await doTest('some-namespace', false, 'another-namespace', true); + }); + }); + + describe('with a non-single-namespace type', () => { + it('does not use object `namespace` or options `namespace` to encrypt attributes if it is specified', async () => { + mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); + await doTest('some-namespace', false, 'another-namespace', false); + }); }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index a2725cbc6a274..0eeb9943b5be9 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -150,14 +150,14 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon // sequential processing. const encryptedObjects = await Promise.all( objects.map(async (object) => { - const { type, id, attributes } = object; + const { type, id, attributes, namespace: objectNamespace } = object; if (!this.options.service.isRegistered(type)) { return object; } const namespace = getDescriptorNamespace( this.options.baseTypeRegistry, type, - options?.namespace + objectNamespace ?? options?.namespace ); return { ...object, From 17985c3f17d9b7f2927e5b2d9d26331f235c0d71 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 3 Sep 2020 15:32:48 -0400 Subject: [PATCH 09/16] Updated non-dev usages of node-forge We have a direct dependency on node-forge `0.9.1`, and a also a transitive dependency via: @elastic/request-crypto@1.1.4 > node-jose@1.1.0 > node-forge@0.7.6 This commit updates both of these to `0.10.0`. --- package.json | 5 +++-- yarn.lock | 21 ++++++++------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index cbf8fd6bc3bd1..ff487510f7a32 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "**/istanbul-instrumenter-loader/schema-utils": "1.0.0", "**/image-diff/gm/debug": "^2.6.9", "**/load-grunt-config/lodash": "^4.17.20", + "**/node-jose/node-forge": "^0.10.0", "**/react-dom": "^16.12.0", "**/react": "^16.12.0", "**/react-test-renderer": "^16.12.0", @@ -191,7 +192,7 @@ "moment-timezone": "^0.5.27", "mustache": "2.3.2", "node-fetch": "1.7.3", - "node-forge": "^0.9.1", + "node-forge": "^0.10.0", "opn": "^5.5.0", "oppsy": "^2.0.0", "p-map": "^4.0.0", @@ -305,7 +306,7 @@ "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", "@types/node": ">=10.17.17 <10.20.0", - "@types/node-forge": "^0.9.0", + "@types/node-forge": "^0.9.5", "@types/normalize-path": "^3.0.0", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", diff --git a/yarn.lock b/yarn.lock index 57a2a9d8bc506..b518bf62734ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4268,10 +4268,10 @@ dependencies: "@types/node" "*" -"@types/node-forge@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.0.tgz#e9f678ec09283f9f35cb8de6c01f86be9278ac08" - integrity sha512-J00+BIHJOfagO1Qs67Jp5CZO3VkFxY8YKMt44oBhXr+3ZYNnl8wv/vtcJyPjuH0QZ+q7+5nnc6o/YH91ZJy2pQ== +"@types/node-forge@^0.9.5": + version "0.9.5" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.5.tgz#648231d79da197216290429020698d4e767365a0" + integrity sha512-rrN3xfA/oZIzwOnO3d2wRQz7UdeVkmMMPjWUCfpPTPuKFVb3D6G10LuiVHYYmvrivBBLMx4m0P/FICoDbNZUMA== dependencies: "@types/node" "*" @@ -20591,15 +20591,10 @@ node-forge@0.9.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== -node-forge@^0.7.6: - version "0.7.6" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" - integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== - -node-forge@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" - integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== +node-forge@^0.10.0, node-forge@^0.7.6: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-gyp@^3.8.0: version "3.8.0" From ba8c72d5678f7ca1877f38d6d74b521d8a855456 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 4 Sep 2020 11:23:57 -0400 Subject: [PATCH 10/16] Revert "Don't expose core namespace conversion methods in the public contract" This reverts commit 0b2f62cbc89018e705adc9c09f7c917cf79518f6. --- .../core/server/kibana-plugin-core-server.md | 2 + ...-plugin-core-server.namespaceidtostring.md | 13 ++++++ ...-plugin-core-server.namespacestringtoid.md | 13 ++++++ src/core/server/index.ts | 2 + .../server/saved_objects/service/index.ts | 2 + .../server/saved_objects/service/lib/index.ts | 2 + src/core/server/server.api.md | 6 +++ .../lib/copy_to_spaces/copy_to_spaces.test.ts | 1 + .../resolve_copy_conflicts.test.ts | 1 + .../server/lib/utils/__mocks__/index.ts | 14 ++++++ .../spaces/server/lib/utils/namespace.test.ts | 46 ++++++------------- .../spaces/server/lib/utils/namespace.ts | 36 ++++++--------- .../routes/api/external/copy_to_space.test.ts | 2 +- 13 files changed, 87 insertions(+), 53 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md create mode 100644 x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 89330d2a86f76..a29d550e831ae 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -224,6 +224,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | Variable | Description | | --- | --- | | [kibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Set of helpers used to create KibanaResponse to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution. | +| [namespaceIdToString](./kibana-plugin-core-server.namespaceidtostring.md) | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined namespace ID (which has a namespace string of 'default'). | +| [namespaceStringToId](./kibana-plugin-core-server.namespacestringtoid.md) | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default' namespace string (which has a namespace ID of undefined). | | [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md) | The current "level" of availability of a service. | | [validBodyOutput](./kibana-plugin-core-server.validbodyoutput.md) | The set of valid body.output | diff --git a/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md b/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md new file mode 100644 index 0000000000000..5c9f093531e1c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [namespaceIdToString](./kibana-plugin-core-server.namespaceidtostring.md) + +## namespaceIdToString variable + +Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the `undefined` namespace ID (which has a namespace string of `'default'`). + +Signature: + +```typescript +namespaceIdToString: (namespace?: string | undefined) => string +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md b/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md new file mode 100644 index 0000000000000..39d6a13aa7fb0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [namespaceStringToId](./kibana-plugin-core-server.namespacestringtoid.md) + +## namespaceStringToId variable + +Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the `'default'` namespace string (which has a namespace ID of `undefined`). + +Signature: + +```typescript +namespaceStringToId: (namespace: string) => string | undefined +``` diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 5422cbc2180ef..9753f725c7c96 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -303,6 +303,8 @@ export { exportSavedObjectsToStream, importSavedObjectsFromStream, resolveSavedObjectsImportErrors, + namespaceIdToString, + namespaceStringToId, } from './saved_objects'; export { diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 9f625b4732e26..245df328db665 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -58,6 +58,8 @@ export { SavedObjectsErrorHelpers, SavedObjectsClientFactory, SavedObjectsClientFactoryProvider, + namespaceIdToString, + namespaceStringToId, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index e103120388e35..cfae6ae9b62d5 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -30,3 +30,5 @@ export { } from './scoped_client_provider'; export { SavedObjectsErrorHelpers } from './errors'; + +export { namespaceIdToString, namespaceStringToId } from './namespace'; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 6f526109b96ce..337ccf336741f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1599,6 +1599,12 @@ export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulPart // @public export type MutatingOperationRefreshSetting = boolean | 'wait_for'; +// @public +export const namespaceIdToString: (namespace?: string | undefined) => string; + +// @public +export const namespaceStringToId: (namespace: string) => string | undefined; + // Warning: (ae-missing-release-tag) "NodesVersionCompatibility" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index d49dfa2015dc6..1cec7b769fa26 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -20,6 +20,7 @@ import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; jest.mock('../../../../../../src/core/server', () => { return { + ...(jest.requireActual('../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), importSavedObjectsFromStream: jest.fn(), }; diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 6a77bf7397cb5..37181c9d81649 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -20,6 +20,7 @@ import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_ jest.mock('../../../../../../src/core/server', () => { return { + ...(jest.requireActual('../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), resolveSavedObjectsImportErrors: jest.fn(), }; diff --git a/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts b/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts new file mode 100644 index 0000000000000..2bb23d0304858 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const mockNamespaceIdToString = jest.fn(); +const mockNamespaceStringToId = jest.fn(); +jest.mock('../../../../../../../src/core/server', () => ({ + namespaceIdToString: mockNamespaceIdToString, + namespaceStringToId: mockNamespaceStringToId, +})); + +export { mockNamespaceIdToString, mockNamespaceStringToId }; diff --git a/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts b/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts index a81a5f3cee187..79d3dda301045 100644 --- a/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts +++ b/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts @@ -4,45 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { mockNamespaceIdToString, mockNamespaceStringToId } from './__mocks__'; import { spaceIdToNamespace, namespaceToSpaceId } from './namespace'; -describe('#spaceIdToNamespace', () => { - it('converts the default space to undefined', () => { - expect(spaceIdToNamespace(DEFAULT_SPACE_ID)).toBeUndefined(); - }); - - it('returns non-default spaces as-is', () => { - expect(spaceIdToNamespace('foo')).toEqual('foo'); - }); - - it('throws an error when a spaceId is not provided', () => { - // @ts-ignore ts knows this isn't right - expect(() => spaceIdToNamespace()).toThrowErrorMatchingInlineSnapshot(`"spaceId is required"`); +beforeEach(() => { + jest.clearAllMocks(); +}); - // @ts-ignore ts knows this isn't right - expect(() => spaceIdToNamespace(null)).toThrowErrorMatchingInlineSnapshot( - `"spaceId is required"` - ); +describe('#spaceIdToNamespace', () => { + it('returns result of namespaceStringToId', () => { + mockNamespaceStringToId.mockReturnValue('bar'); - expect(() => spaceIdToNamespace('')).toThrowErrorMatchingInlineSnapshot( - `"spaceId is required"` - ); + const result = spaceIdToNamespace('foo'); + expect(mockNamespaceStringToId).toHaveBeenCalledWith('foo'); + expect(result).toEqual('bar'); }); }); describe('#namespaceToSpaceId', () => { - it('returns the default space id for undefined namespaces', () => { - expect(namespaceToSpaceId(undefined)).toEqual(DEFAULT_SPACE_ID); - }); - - it('returns all other namespaces as-is', () => { - expect(namespaceToSpaceId('foo')).toEqual('foo'); - }); + it('returns result of namespaceIdToString', () => { + mockNamespaceIdToString.mockReturnValue('bar'); - it('throws an error when an empty string is provided', () => { - expect(() => namespaceToSpaceId('')).toThrowErrorMatchingInlineSnapshot( - `"namespace cannot be an empty string"` - ); + const result = namespaceToSpaceId('foo'); + expect(mockNamespaceIdToString).toHaveBeenCalledWith('foo'); + expect(result).toEqual('bar'); }); }); diff --git a/x-pack/plugins/spaces/server/lib/utils/namespace.ts b/x-pack/plugins/spaces/server/lib/utils/namespace.ts index 8c7ed2ea1797d..6861d0141ea02 100644 --- a/x-pack/plugins/spaces/server/lib/utils/namespace.ts +++ b/x-pack/plugins/spaces/server/lib/utils/namespace.ts @@ -4,28 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { namespaceStringToId, namespaceIdToString } from '../../../../../../src/core/server'; -export function spaceIdToNamespace(spaceId: string): string | undefined { - if (!spaceId) { - throw new TypeError('spaceId is required'); - } - - if (spaceId === DEFAULT_SPACE_ID) { - return undefined; - } - - return spaceId; +/** + * Converts a Space ID string to its namespace ID representation. Note that a Space ID string is equivalent to a namespace string. + * + * See also: {@link namespaceStringToId}. + */ +export function spaceIdToNamespace(spaceId: string) { + return namespaceStringToId(spaceId); } -export function namespaceToSpaceId(namespace: string | undefined): string { - if (namespace === '') { - throw new TypeError('namespace cannot be an empty string'); - } - - if (!namespace) { - return DEFAULT_SPACE_ID; - } - - return namespace; +/** + * Converts a namespace ID to its Space ID string representation. Note that a Space ID string is equivalent to a namespace string. + * + * See also: {@link namespaceIdToString}. + */ +export function namespaceToSpaceId(namespace?: string) { + return namespaceIdToString(namespace); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index bec3a5dcb0b71..dce6de908cfcb 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -30,10 +30,10 @@ import { securityMock } from '../../../../../security/server/mocks'; import { ObjectType } from '@kbn/config-schema'; jest.mock('../../../../../../../src/core/server', () => { return { + ...(jest.requireActual('../../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), importSavedObjectsFromStream: jest.fn(), resolveSavedObjectsImportErrors: jest.fn(), - kibanaResponseFactory: jest.requireActual('src/core/server').kibanaResponseFactory, }; }); import { From 2c36a3137657ac2224936ce9c35777325eba07c4 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 4 Sep 2020 12:02:27 -0400 Subject: [PATCH 11/16] Wrap namespace utility methods in `SavedObjectsUtils` class --- .../core/server/kibana-plugin-core-server.md | 3 +- ...na-plugin-core-server.savedobjectsutils.md | 20 +++++++ ....savedobjectsutils.namespaceidtostring.md} | 6 +- ....savedobjectsutils.namespacestringtoid.md} | 6 +- src/core/server/index.ts | 3 +- .../server/saved_objects/service/index.ts | 3 +- .../server/saved_objects/service/lib/index.ts | 2 +- .../service/lib/namespace.test.ts | 53 ----------------- .../saved_objects/service/lib/namespace.ts | 48 ---------------- .../saved_objects/service/lib/repository.ts | 32 +++++++---- .../service/lib/search_dsl/query_params.ts | 2 +- .../saved_objects/service/lib/utils.test.ts | 57 +++++++++++++++++++ .../server/saved_objects/service/lib/utils.ts | 53 +++++++++++++++++ src/core/server/server.api.md | 12 ++-- .../server/lib/utils/__mocks__/index.ts | 6 +- .../spaces/server/lib/utils/namespace.ts | 6 +- 16 files changed, 174 insertions(+), 138 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md rename docs/development/core/server/{kibana-plugin-core-server.namespaceidtostring.md => kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md} (57%) rename docs/development/core/server/{kibana-plugin-core-server.namespacestringtoid.md => kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md} (57%) delete mode 100644 src/core/server/saved_objects/service/lib/namespace.test.ts delete mode 100644 src/core/server/saved_objects/service/lib/namespace.ts create mode 100644 src/core/server/saved_objects/service/lib/utils.test.ts create mode 100644 src/core/server/saved_objects/service/lib/utils.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index a29d550e831ae..2adeb6c8dc685 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -28,6 +28,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | | [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) | | | [SavedObjectsSerializer](./kibana-plugin-core-server.savedobjectsserializer.md) | A serializer that can be used to manually convert [raw](./kibana-plugin-core-server.savedobjectsrawdoc.md) or [sanitized](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) documents to the other kind. | +| [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) | | | [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) | Registry holding information about all the registered [saved object types](./kibana-plugin-core-server.savedobjectstype.md). | ## Enumerations @@ -224,8 +225,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | Variable | Description | | --- | --- | | [kibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Set of helpers used to create KibanaResponse to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution. | -| [namespaceIdToString](./kibana-plugin-core-server.namespaceidtostring.md) | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined namespace ID (which has a namespace string of 'default'). | -| [namespaceStringToId](./kibana-plugin-core-server.namespacestringtoid.md) | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default' namespace string (which has a namespace ID of undefined). | | [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md) | The current "level" of availability of a service. | | [validBodyOutput](./kibana-plugin-core-server.validbodyoutput.md) | The set of valid body.output | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md new file mode 100644 index 0000000000000..e365dfbcb5142 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) + +## SavedObjectsUtils class + + +Signature: + +```typescript +export declare class SavedObjectsUtils +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) | static | (namespace?: string | undefined) => string | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined namespace ID (which has a namespace string of 'default'). | +| [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) | static | (namespace: string) => string | undefined | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default' namespace string (which has a namespace ID of undefined). | + diff --git a/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md similarity index 57% rename from docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md index 5c9f093531e1c..591505892e64f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.namespaceidtostring.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [namespaceIdToString](./kibana-plugin-core-server.namespaceidtostring.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) > [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) -## namespaceIdToString variable +## SavedObjectsUtils.namespaceIdToString property Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the `undefined` namespace ID (which has a namespace string of `'default'`). Signature: ```typescript -namespaceIdToString: (namespace?: string | undefined) => string +static namespaceIdToString: (namespace?: string | undefined) => string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md similarity index 57% rename from docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md index 39d6a13aa7fb0..e052fe493b5ea 100644 --- a/docs/development/core/server/kibana-plugin-core-server.namespacestringtoid.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [namespaceStringToId](./kibana-plugin-core-server.namespacestringtoid.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) > [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) -## namespaceStringToId variable +## SavedObjectsUtils.namespaceStringToId property Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the `'default'` namespace string (which has a namespace ID of `undefined`). Signature: ```typescript -namespaceStringToId: (namespace: string) => string | undefined +static namespaceStringToId: (namespace: string) => string | undefined; ``` diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 9753f725c7c96..257ae3710478e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -300,11 +300,10 @@ export { SavedObjectsTypeManagementDefinition, SavedObjectMigrationMap, SavedObjectMigrationFn, + SavedObjectsUtils, exportSavedObjectsToStream, importSavedObjectsFromStream, resolveSavedObjectsImportErrors, - namespaceIdToString, - namespaceStringToId, } from './saved_objects'; export { diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 245df328db665..19ba7396911ad 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -58,8 +58,7 @@ export { SavedObjectsErrorHelpers, SavedObjectsClientFactory, SavedObjectsClientFactoryProvider, - namespaceIdToString, - namespaceStringToId, + SavedObjectsUtils, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index cfae6ae9b62d5..eae8c5ef2e10c 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -31,4 +31,4 @@ export { export { SavedObjectsErrorHelpers } from './errors'; -export { namespaceIdToString, namespaceStringToId } from './namespace'; +export { SavedObjectsUtils } from './utils'; diff --git a/src/core/server/saved_objects/service/lib/namespace.test.ts b/src/core/server/saved_objects/service/lib/namespace.test.ts deleted file mode 100644 index c5818deb8ef2a..0000000000000 --- a/src/core/server/saved_objects/service/lib/namespace.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { namespaceIdToString, namespaceStringToId } from './namespace'; - -describe('#namespaceIdToString', () => { - it('converts `undefined` to default namespace string', () => { - expect(namespaceIdToString(undefined)).toEqual('default'); - }); - - it('leaves other namespace IDs as-is', () => { - expect(namespaceIdToString('foo')).toEqual('foo'); - }); - - it('throws an error when a namespace ID is an empty string', () => { - expect(() => namespaceIdToString('')).toThrowError('namespace cannot be an empty string'); - }); -}); - -describe('#namespaceStringToId', () => { - it('converts default namespace string to `undefined`', () => { - expect(namespaceStringToId('default')).toBeUndefined(); - }); - - it('leaves other namespace strings as-is', () => { - expect(namespaceStringToId('foo')).toEqual('foo'); - }); - - it('throws an error when a namespace string is falsy', () => { - const test = (arg: any) => - expect(() => namespaceStringToId(arg)).toThrowError('namespace must be a non-empty string'); - - test(undefined); - test(null); - test(''); - }); -}); diff --git a/src/core/server/saved_objects/service/lib/namespace.ts b/src/core/server/saved_objects/service/lib/namespace.ts deleted file mode 100644 index bc0691c0bb1db..0000000000000 --- a/src/core/server/saved_objects/service/lib/namespace.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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. - */ - -export const DEFAULT_NAMESPACE_STRING = 'default'; - -/** - * Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with - * the exception of the `undefined` namespace ID (which has a namespace string of `'default'`). - * - * @param namespace The namespace ID, which must be either a non-empty string or `undefined`. - */ -export const namespaceIdToString = (namespace?: string) => { - if (namespace === '') { - throw new TypeError('namespace cannot be an empty string'); - } - - return namespace ?? DEFAULT_NAMESPACE_STRING; -}; - -/** - * Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with - * the exception of the `'default'` namespace string (which has a namespace ID of `undefined`). - * - * @param namespace The namespace string, which must be non-empty. - */ -export const namespaceStringToId = (namespace: string) => { - if (!namespace) { - throw new TypeError('namespace must be a non-empty string'); - } - - return namespace !== DEFAULT_NAMESPACE_STRING ? namespace : undefined; -}; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index c73dec7797801..9cb0d415e25a9 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -67,7 +67,7 @@ import { } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; -import { namespaceIdToString, namespaceStringToId } from './namespace'; +import { 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. @@ -563,7 +563,7 @@ export class SavedObjectsRepository { preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); const remainingNamespaces = existingNamespaces?.filter( - (x) => x !== namespaceIdToString(namespace) + (x) => x !== SavedObjectsUtils.namespaceIdToString(namespace) ); if (remainingNamespaces?.length) { @@ -887,7 +887,9 @@ export class SavedObjectsRepository { const { originId, updated_at: updatedAt } = doc._source; let namespaces = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = doc._source.namespaces ?? [namespaceIdToString(doc._source.namespace)]; + namespaces = doc._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(doc._source.namespace), + ]; } return { @@ -944,7 +946,9 @@ export class SavedObjectsRepository { let namespaces: string[] = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = body._source.namespaces ?? [namespaceIdToString(body._source.namespace)]; + namespaces = body._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(body._source.namespace), + ]; } return { @@ -1020,7 +1024,9 @@ export class SavedObjectsRepository { const { originId } = body.get._source; let namespaces = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = body.get._source.namespaces ?? [namespaceIdToString(body.get._source.namespace)]; + namespaces = body.get._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(body.get._source.namespace), + ]; } return { @@ -1257,9 +1263,11 @@ export class SavedObjectsRepository { }); const getNamespaceId = (objectNamespace?: string) => - objectNamespace !== undefined ? namespaceStringToId(objectNamespace) : namespace; + objectNamespace !== undefined + ? SavedObjectsUtils.namespaceStringToId(objectNamespace) + : namespace; const getNamespaceString = (objectNamespace?: string) => - objectNamespace ?? namespaceIdToString(namespace); + objectNamespace ?? SavedObjectsUtils.namespaceIdToString(namespace); const bulkGetDocs = expectedBulkGetResults .filter(isRight) @@ -1319,7 +1327,7 @@ export class SavedObjectsRepository { }; } namespaces = actualResult._source.namespaces ?? [ - namespaceIdToString(actualResult._source.namespace), + SavedObjectsUtils.namespaceIdToString(actualResult._source.namespace), ]; versionProperties = getExpectedVersionProperties(version, actualResult); } else { @@ -1522,7 +1530,7 @@ export class SavedObjectsRepository { const savedObject = this._serializer.rawToSavedObject(raw); const { namespace, type } = savedObject; if (this._registry.isSingleNamespace(type)) { - savedObject.namespaces = [namespaceIdToString(namespace)]; + savedObject.namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)]; } return omit(savedObject, 'namespace') as SavedObject; } @@ -1545,7 +1553,7 @@ export class SavedObjectsRepository { } const namespaces = raw._source.namespaces; - return namespaces?.includes(namespaceIdToString(namespace)) ?? false; + return namespaces?.includes(SavedObjectsUtils.namespaceIdToString(namespace)) ?? false; } /** @@ -1665,7 +1673,7 @@ function getSavedObjectNamespaces( if (document) { return document._source?.namespaces; } - return [namespaceIdToString(namespace)]; + return [SavedObjectsUtils.namespaceIdToString(namespace)]; } /** @@ -1673,7 +1681,7 @@ function getSavedObjectNamespaces( * This allows `'default'` to be used interchangeably with `undefined`. */ const normalizeNamespace = (namespace?: string) => - namespace === undefined ? namespace : namespaceStringToId(namespace); + namespace === undefined ? namespace : SavedObjectsUtils.namespaceStringToId(namespace); /** * Extracts the contents of a decorated error to return the attributes for bulk operations. 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 42fcc0b351fbc..3ff72a86c2f89 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 @@ -21,7 +21,7 @@ import { esKuery, KueryNode } from '../../../../../../plugins/data/server'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; -import { DEFAULT_NAMESPACE_STRING } from '../namespace'; +import { DEFAULT_NAMESPACE_STRING } from '../utils'; /** * Gets the types based on the type. Uses mappings to support diff --git a/src/core/server/saved_objects/service/lib/utils.test.ts b/src/core/server/saved_objects/service/lib/utils.test.ts new file mode 100644 index 0000000000000..ecb0c695f0883 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/utils.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { SavedObjectsUtils } from './utils'; + +describe('#SavedObjectsUtils', () => { + const { namespaceIdToString, namespaceStringToId } = SavedObjectsUtils; + + describe('#namespaceIdToString', () => { + it('converts `undefined` to default namespace string', () => { + expect(namespaceIdToString(undefined)).toEqual('default'); + }); + + it('leaves other namespace IDs as-is', () => { + expect(namespaceIdToString('foo')).toEqual('foo'); + }); + + it('throws an error when a namespace ID is an empty string', () => { + expect(() => namespaceIdToString('')).toThrowError('namespace cannot be an empty string'); + }); + }); + + describe('#namespaceStringToId', () => { + it('converts default namespace string to `undefined`', () => { + expect(namespaceStringToId('default')).toBeUndefined(); + }); + + it('leaves other namespace strings as-is', () => { + expect(namespaceStringToId('foo')).toEqual('foo'); + }); + + it('throws an error when a namespace string is falsy', () => { + const test = (arg: any) => + expect(() => namespaceStringToId(arg)).toThrowError('namespace must be a non-empty string'); + + test(undefined); + test(null); + test(''); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/utils.ts b/src/core/server/saved_objects/service/lib/utils.ts new file mode 100644 index 0000000000000..6101ad57cc401 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/utils.ts @@ -0,0 +1,53 @@ +/* + * 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. + */ + +export const DEFAULT_NAMESPACE_STRING = 'default'; + +/** + * @public + */ +export class SavedObjectsUtils { + /** + * Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with + * the exception of the `undefined` namespace ID (which has a namespace string of `'default'`). + * + * @param namespace The namespace ID, which must be either a non-empty string or `undefined`. + */ + public static namespaceIdToString = (namespace?: string) => { + if (namespace === '') { + throw new TypeError('namespace cannot be an empty string'); + } + + return namespace ?? DEFAULT_NAMESPACE_STRING; + }; + + /** + * Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with + * the exception of the `'default'` namespace string (which has a namespace ID of `undefined`). + * + * @param namespace The namespace string, which must be non-empty. + */ + public static namespaceStringToId = (namespace: string) => { + if (!namespace) { + throw new TypeError('namespace must be a non-empty string'); + } + + return namespace !== DEFAULT_NAMESPACE_STRING ? namespace : undefined; + }; +} diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 337ccf336741f..8d651e3245771 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1599,12 +1599,6 @@ export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulPart // @public export type MutatingOperationRefreshSetting = boolean | 'wait_for'; -// @public -export const namespaceIdToString: (namespace?: string | undefined) => string; - -// @public -export const namespaceStringToId: (namespace: string) => string | undefined; - // Warning: (ae-missing-release-tag) "NodesVersionCompatibility" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2717,6 +2711,12 @@ export interface SavedObjectsUpdateResponse extends Omit string; + static namespaceStringToId: (namespace: string) => string | undefined; +} + // @public export class SavedObjectTypeRegistry { getAllTypes(): SavedObjectsType[]; diff --git a/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts b/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts index 2bb23d0304858..2b93e6d87a7af 100644 --- a/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts +++ b/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts @@ -7,8 +7,10 @@ const mockNamespaceIdToString = jest.fn(); const mockNamespaceStringToId = jest.fn(); jest.mock('../../../../../../../src/core/server', () => ({ - namespaceIdToString: mockNamespaceIdToString, - namespaceStringToId: mockNamespaceStringToId, + SavedObjectsUtils: { + namespaceIdToString: mockNamespaceIdToString, + namespaceStringToId: mockNamespaceStringToId, + }, })); export { mockNamespaceIdToString, mockNamespaceStringToId }; diff --git a/x-pack/plugins/spaces/server/lib/utils/namespace.ts b/x-pack/plugins/spaces/server/lib/utils/namespace.ts index 6861d0141ea02..344da18846f3b 100644 --- a/x-pack/plugins/spaces/server/lib/utils/namespace.ts +++ b/x-pack/plugins/spaces/server/lib/utils/namespace.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { namespaceStringToId, namespaceIdToString } from '../../../../../../src/core/server'; +import { SavedObjectsUtils } from '../../../../../../src/core/server'; /** * Converts a Space ID string to its namespace ID representation. Note that a Space ID string is equivalent to a namespace string. @@ -12,7 +12,7 @@ import { namespaceStringToId, namespaceIdToString } from '../../../../../../src/ * See also: {@link namespaceStringToId}. */ export function spaceIdToNamespace(spaceId: string) { - return namespaceStringToId(spaceId); + return SavedObjectsUtils.namespaceStringToId(spaceId); } /** @@ -21,5 +21,5 @@ export function spaceIdToNamespace(spaceId: string) { * See also: {@link namespaceIdToString}. */ export function namespaceToSpaceId(namespace?: string) { - return namespaceIdToString(namespace); + return SavedObjectsUtils.namespaceIdToString(namespace); } From 714a14e34046daa4f4b3b87dc8062bcf3aa82e3b Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 4 Sep 2020 16:59:35 -0400 Subject: [PATCH 12/16] Change ESO plugin to use SavedObjectsUtils for namespaces --- .../server/saved_objects/get_descriptor_namespace.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/get_descriptor_namespace.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/get_descriptor_namespace.ts index b2842df909a1d..7201f13fb930b 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/get_descriptor_namespace.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/get_descriptor_namespace.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ISavedObjectTypeRegistry } from 'kibana/server'; +import { ISavedObjectTypeRegistry, SavedObjectsUtils } from '../../../../../src/core/server'; export const getDescriptorNamespace = ( typeRegistry: ISavedObjectTypeRegistry, @@ -12,5 +12,12 @@ export const getDescriptorNamespace = ( namespace?: string ) => { const descriptorNamespace = typeRegistry.isSingleNamespace(type) ? namespace : undefined; - return descriptorNamespace === 'default' ? undefined : descriptorNamespace; + return normalizeNamespace(descriptorNamespace); }; + +/** + * Ensure that a namespace is always in its namespace ID representation. + * This allows `'default'` to be used interchangeably with `undefined`. + */ +const normalizeNamespace = (namespace?: string) => + namespace === undefined ? namespace : SavedObjectsUtils.namespaceStringToId(namespace); From c2a207a0851c46eea69aa3901b2bf3f1b00582fe Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 4 Sep 2020 17:03:12 -0400 Subject: [PATCH 13/16] Remove unnecessary comment --- .../saved_object_api_integration/spaces_only/apis/bulk_update.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts index dac85736f1fc5..b51ec303fadf3 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts @@ -35,7 +35,6 @@ const createTestCases = (spaceId: string) => { ]; // an "object namespace" string can be specified for individual objects (to bulkUpdate across namespaces) - // even if the Spaces plugin is disabled, this should work, as `namespace` is handled by the Core API const withObjectNamespaces = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespace: DEFAULT_SPACE_ID }, { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespace: SPACE_1_ID }, From cf0a0a00641f0101bd4a644a9f8c7fb07960ea61 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 9 Sep 2020 17:58:02 -0400 Subject: [PATCH 14/16] Address PR review feedback --- .../service/lib/repository.test.js | 36 ++++++++----------- .../saved_objects/service/lib/utils.test.ts | 2 +- 2 files changed, 16 insertions(+), 22 deletions(-) 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 54a3dbce8f44a..7d30875b90796 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -159,13 +159,7 @@ describe('SavedObjectsRepository', () => { { type, id, references, namespace: objectNamespace, originId }, namespace ) => { - const namespaceId = - // eslint-disable-next-line no-nested-ternary - objectNamespace !== undefined - ? objectNamespace !== 'default' - ? objectNamespace - : undefined - : namespace; + const namespaceId = objectNamespace === 'default' ? undefined : objectNamespace ?? namespace; return { // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these found: true, @@ -675,19 +669,19 @@ describe('SavedObjectsRepository', () => { }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; + const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) await bulkCreateSuccess([obj1, obj2], { namespace }); expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) await bulkCreateSuccess([obj1, obj2]); expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) const objects = [ { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, { ...obj2, type: MULTI_NAMESPACE_TYPE }, @@ -994,25 +988,25 @@ describe('SavedObjectsRepository', () => { describe('client calls', () => { it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; + const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) await bulkGetSuccess([obj1, obj2], { namespace }); _expectClientCallArgs([obj1, obj2], { getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) await bulkGetSuccess([obj1, obj2]); _expectClientCallArgs([obj1, obj2], { getId }); }); it(`normalizes options.namespace from 'default' to undefined`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) await bulkGetSuccess([obj1, obj2], { namespace: 'default' }); _expectClientCallArgs([obj1, obj2], { getId }); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) let objects = [obj1, obj2].map((obj) => ({ ...obj, type: NAMESPACE_AGNOSTIC_TYPE })); await bulkGetSuccess(objects, { namespace }); _expectClientCallArgs(objects, { getId }); @@ -1355,7 +1349,7 @@ describe('SavedObjectsRepository', () => { }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; + const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) await bulkUpdateSuccess([obj1, obj2], { namespace }); expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); @@ -1369,7 +1363,7 @@ describe('SavedObjectsRepository', () => { }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) await bulkUpdateSuccess([obj1, obj2]); expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); @@ -1392,7 +1386,7 @@ describe('SavedObjectsRepository', () => { }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) const overrides = { // bulkUpdate uses a preflight `get` request for multi-namespace saved objects, and specifies that version on `update` // we aren't testing for this here, but we need to include Jest assertions so this test doesn't fail @@ -1643,25 +1637,25 @@ describe('SavedObjectsRepository', () => { }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; + const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) await checkConflictsSuccess([obj1, obj2], { namespace }); _expectClientCallArgs([obj1, obj2], { getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) await checkConflictsSuccess([obj1, obj2]); _expectClientCallArgs([obj1, obj2], { getId }); }); it(`normalizes options.namespace from 'default' to undefined`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) await checkConflictsSuccess([obj1, obj2], { namespace: 'default' }); _expectClientCallArgs([obj1, obj2], { getId }); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) // obj3 is multi-namespace, and obj6 is namespace-agnostic await checkConflictsSuccess([obj3, obj6], { namespace }); _expectClientCallArgs([obj3, obj6], { getId }); diff --git a/src/core/server/saved_objects/service/lib/utils.test.ts b/src/core/server/saved_objects/service/lib/utils.test.ts index ecb0c695f0883..ea4fa68242bea 100644 --- a/src/core/server/saved_objects/service/lib/utils.test.ts +++ b/src/core/server/saved_objects/service/lib/utils.test.ts @@ -19,7 +19,7 @@ import { SavedObjectsUtils } from './utils'; -describe('#SavedObjectsUtils', () => { +describe('SavedObjectsUtils', () => { const { namespaceIdToString, namespaceStringToId } = SavedObjectsUtils; describe('#namespaceIdToString', () => { From 70793b0dd26f6706059a9cf6124417cfabef8ef6 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 10 Sep 2020 14:00:22 -0400 Subject: [PATCH 15/16] Address more PR review feedback --- ...ypted_saved_objects_client_wrapper.test.ts | 47 ++++++++++++++----- .../secure_saved_objects_client_wrapper.ts | 2 + 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index cd7e2ebb8aaa1..18834f55af0a5 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -555,12 +555,18 @@ describe('#bulkUpdate', () => { }); describe('namespace', () => { - const doTest = async ( - optionsNamespace: string | undefined, - expectOptionsNamespaceInDescriptor: boolean, - objectNamespace: string | undefined, - expectObjectNamespaceInDescriptor: boolean - ) => { + interface TestParams { + optionsNamespace: string | undefined; + objectNamespace: string | undefined; + expectOptionsNamespaceInDescriptor: boolean; + expectObjectNamespaceInDescriptor: boolean; + } + const doTest = async ({ + optionsNamespace, + objectNamespace, + expectOptionsNamespaceInDescriptor, + expectObjectNamespaceInDescriptor, + }: TestParams) => { const docs = [ { id: 'some-id', @@ -623,7 +629,6 @@ describe('#bulkUpdate', () => { }, version: 'some-version', namespace: objectNamespace, - references: undefined, }, ], @@ -632,24 +637,44 @@ describe('#bulkUpdate', () => { }; it('does not use options `namespace` or object `namespace` to encrypt attributes if neither are specified', async () => { - await doTest(undefined, false, undefined, false); + await doTest({ + optionsNamespace: undefined, + objectNamespace: undefined, + expectOptionsNamespaceInDescriptor: false, + expectObjectNamespaceInDescriptor: false, + }); }); describe('with a single-namespace type', () => { it('uses options `namespace` to encrypt attributes if it is specified and object `namespace` is not', async () => { - await doTest('some-namespace', true, undefined, false); + await doTest({ + optionsNamespace: 'some-namespace', + objectNamespace: undefined, + expectOptionsNamespaceInDescriptor: true, + expectObjectNamespaceInDescriptor: false, + }); }); it('uses object `namespace` to encrypt attributes if it is specified', async () => { // object namespace supersedes options namespace - await doTest('some-namespace', false, 'another-namespace', true); + await doTest({ + optionsNamespace: 'some-namespace', + objectNamespace: 'another-namespace', + expectOptionsNamespaceInDescriptor: false, + expectObjectNamespaceInDescriptor: true, + }); }); }); describe('with a non-single-namespace type', () => { it('does not use object `namespace` or options `namespace` to encrypt attributes if it is specified', async () => { mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); - await doTest('some-namespace', false, 'another-namespace', false); + await doTest({ + optionsNamespace: 'some-namespace', + objectNamespace: 'another-namespace', + expectOptionsNamespaceInDescriptor: false, + expectObjectNamespaceInDescriptor: false, + }); }); }); }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 1c294b7cd1775..bfa08a0116644 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -200,6 +200,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra options: SavedObjectsBaseOptions = {} ) { const objectNamespaces = objects + // The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace; + // in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so. .filter(({ namespace }) => namespace !== undefined) .map(({ namespace }) => namespace!); const namespaces = [options?.namespace, ...objectNamespaces]; From f3458913835a1a0c9e8f614473a4b3ed0395bd5a Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 10 Sep 2020 15:57:21 -0400 Subject: [PATCH 16/16] Update JSdocs There was a typo, and I took the opportunity to reword it a bit too --- .../kibana-plugin-core-server.savedobjectsbulkupdateobject.md | 2 +- ...ugin-core-server.savedobjectsbulkupdateobject.namespace.md | 2 +- src/core/server/saved_objects/service/saved_objects_client.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md index 3ae529270bf32..d71eda6009284 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md @@ -17,6 +17,6 @@ export interface SavedObjectsBulkUpdateObject extends PickPartial<T> | The data for a Saved Object is stored as an object in the attributes property. | | [id](./kibana-plugin-core-server.savedobjectsbulkupdateobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | -| [namespace](./kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md) | string | Optional namespace string to use for this document. If this is defined, it will supersede the namespace ID that is in [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md).Note: the default namespace's string representation is 'default', and its ID representation is undefined. | +| [namespace](./kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md) | string | Optional namespace string to use when searching for this object. If this is defined, it will supersede the namespace ID that is in [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-server.savedobjectsbulkupdateoptions.md).Note: the default namespace's string representation is 'default', and its ID representation is undefined. | | [type](./kibana-plugin-core-server.savedobjectsbulkupdateobject.type.md) | string | The type of this Saved Object. Each plugin can define it's own custom Saved Object types. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md index 5796e70eda980..544efcd3be909 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md @@ -4,7 +4,7 @@ ## SavedObjectsBulkUpdateObject.namespace property -Optional namespace string to use for this document. If this is defined, it will supersede the namespace ID that is in [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md). +Optional namespace string to use when searching for this object. If this is defined, it will supersede the namespace ID that is in [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-server.savedobjectsbulkupdateoptions.md). Note: the default namespace's string representation is `'default'`, and its ID representation is `undefined`. diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 00aae283ef3b3..8c96116de49cb 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -81,8 +81,8 @@ export interface SavedObjectsBulkUpdateObject /** {@inheritdoc SavedObjectAttributes} */ attributes: Partial; /** - * Optional namespace string to use for this document. If this is defined, it will supersede the namespace ID that is in - * {@link SavedObjectsUpdateOptions}. + * Optional namespace string to use when searching for this object. If this is defined, it will supersede the namespace ID that is in + * {@link SavedObjectsBulkUpdateOptions}. * * Note: the default namespace's string representation is `'default'`, and its ID representation is `undefined`. **/