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`.
**/