Change response for Saved Objects share/unshare operations (#74995)

This commit is contained in:
Joe Portner 2020-08-20 08:35:33 -04:00 committed by GitHub
parent cd36188c40
commit 4dd5d63484
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 156 additions and 37 deletions

View file

@ -153,6 +153,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions.<!-- -->For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. |
| [SavedObjectReference](./kibana-plugin-core-server.savedobjectreference.md) | A reference to another saved object. |
| [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) | |
| [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) | |
| [SavedObjectsBaseOptions](./kibana-plugin-core-server.savedobjectsbaseoptions.md) | |
| [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) | |
| [SavedObjectsBulkGetObject](./kibana-plugin-core-server.savedobjectsbulkgetobject.md) | |
@ -167,6 +168,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | |
| [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | |
| [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | |
| [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) | |
| [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | |
| [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) | Options controlling the export operation. |
| [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry |

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md)
## SavedObjectsAddToNamespacesResponse interface
<b>Signature:</b>
```typescript
export interface SavedObjectsAddToNamespacesResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [namespaces](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md) | <code>string[]</code> | The namespaces the object exists in after this operation is complete. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) &gt; [namespaces](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md)
## SavedObjectsAddToNamespacesResponse.namespaces property
The namespaces the object exists in after this operation is complete.
<b>Signature:</b>
```typescript
namespaces: string[];
```

View file

@ -9,7 +9,7 @@ Adds namespaces to a SavedObject
<b>Signature:</b>
```typescript
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>;
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<SavedObjectsAddToNamespacesResponse>;
```
## Parameters
@ -23,5 +23,5 @@ addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedO
<b>Returns:</b>
`Promise<{}>`
`Promise<SavedObjectsAddToNamespacesResponse>`

View file

@ -9,7 +9,7 @@ Removes namespaces from a SavedObject
<b>Signature:</b>
```typescript
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<SavedObjectsDeleteFromNamespacesResponse>;
```
## Parameters
@ -23,5 +23,5 @@ deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: S
<b>Returns:</b>
`Promise<{}>`
`Promise<SavedObjectsDeleteFromNamespacesResponse>`

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md)
## SavedObjectsDeleteFromNamespacesResponse interface
<b>Signature:</b>
```typescript
export interface SavedObjectsDeleteFromNamespacesResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [namespaces](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md) | <code>string[]</code> | The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) &gt; [namespaces](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md)
## SavedObjectsDeleteFromNamespacesResponse.namespaces property
The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted.
<b>Signature:</b>
```typescript
namespaces: string[];
```

View file

@ -9,7 +9,7 @@ Adds one or more namespaces to a given multi-namespace saved object. This method
<b>Signature:</b>
```typescript
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>;
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<SavedObjectsAddToNamespacesResponse>;
```
## Parameters
@ -23,5 +23,5 @@ addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedO
<b>Returns:</b>
`Promise<{}>`
`Promise<SavedObjectsAddToNamespacesResponse>`

View file

@ -9,7 +9,7 @@ Removes one or more namespaces from a given multi-namespace saved object. If no
<b>Signature:</b>
```typescript
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<SavedObjectsDeleteFromNamespacesResponse>;
```
## Parameters
@ -23,5 +23,5 @@ deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: S
<b>Returns:</b>
`Promise<{}>`
`Promise<SavedObjectsDeleteFromNamespacesResponse>`

View file

@ -274,7 +274,9 @@ export {
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
SavedObjectsAddToNamespacesOptions,
SavedObjectsAddToNamespacesResponse,
SavedObjectsDeleteFromNamespacesOptions,
SavedObjectsDeleteFromNamespacesResponse,
SavedObjectsServiceStart,
SavedObjectsServiceSetup,
SavedObjectStatusMeta,

View file

@ -393,14 +393,14 @@ describe('SavedObjectsRepository', () => {
});
describe('returns', () => {
it(`returns an empty object on success`, async () => {
it(`returns all existing and new namespaces on success`, async () => {
const result = await addToNamespacesSuccess(type, id, [newNs1, newNs2]);
expect(result).toEqual({});
expect(result).toEqual({ namespaces: [currentNs1, currentNs2, newNs1, newNs2] });
});
it(`succeeds when adding existing namespaces`, async () => {
const result = await addToNamespacesSuccess(type, id, [currentNs1]);
expect(result).toEqual({});
expect(result).toEqual({ namespaces: [currentNs1, currentNs2] });
});
});
});
@ -3102,17 +3102,17 @@ describe('SavedObjectsRepository', () => {
});
describe('returns', () => {
it(`returns an empty object on success (delete)`, async () => {
it(`returns an empty namespaces array on success (delete)`, async () => {
const test = async (namespaces) => {
const result = await deleteFromNamespacesSuccess(type, id, namespaces, namespaces);
expect(result).toEqual({});
expect(result).toEqual({ namespaces: [] });
client.delete.mockClear();
};
await test([namespace1]);
await test([namespace1, namespace2]);
});
it(`returns an empty object on success (update)`, async () => {
it(`returns remaining namespaces on success (update)`, async () => {
const test = async (remaining) => {
const currentNamespaces = [namespace1].concat(remaining);
const result = await deleteFromNamespacesSuccess(
@ -3121,7 +3121,7 @@ describe('SavedObjectsRepository', () => {
[namespace1],
currentNamespaces
);
expect(result).toEqual({});
expect(result).toEqual({ namespaces: remaining });
client.delete.mockClear();
};
await test([namespace2]);
@ -3132,7 +3132,7 @@ describe('SavedObjectsRepository', () => {
const namespaces = [namespace2];
const currentNamespaces = [namespace1];
const result = await deleteFromNamespacesSuccess(type, id, namespaces, currentNamespaces);
expect(result).toEqual({});
expect(result).toEqual({ namespaces: currentNamespaces });
});
});
});

View file

@ -52,7 +52,9 @@ import {
SavedObjectsBulkUpdateOptions,
SavedObjectsDeleteOptions,
SavedObjectsAddToNamespacesOptions,
SavedObjectsAddToNamespacesResponse,
SavedObjectsDeleteFromNamespacesOptions,
SavedObjectsDeleteFromNamespacesResponse,
} from '../saved_objects_client';
import {
SavedObject,
@ -947,7 +949,7 @@ export class SavedObjectsRepository {
id: string,
namespaces: string[],
options: SavedObjectsAddToNamespacesOptions = {}
): Promise<{}> {
): Promise<SavedObjectsAddToNamespacesResponse> {
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
@ -996,7 +998,7 @@ export class SavedObjectsRepository {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
return {};
return { namespaces: doc.namespaces };
}
/**
@ -1009,7 +1011,7 @@ export class SavedObjectsRepository {
id: string,
namespaces: string[],
options: SavedObjectsDeleteFromNamespacesOptions = {}
): Promise<{}> {
): Promise<SavedObjectsDeleteFromNamespacesResponse> {
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
@ -1063,7 +1065,7 @@ export class SavedObjectsRepository {
// see "404s from missing index" above
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
return {};
return { namespaces: doc.namespaces };
} else {
// if there are no namespaces remaining, delete the saved object
const { body, statusCode } = await this.client.delete<DeleteDocumentResponse>(
@ -1080,7 +1082,7 @@ export class SavedObjectsRepository {
const deleted = body.result === 'deleted';
if (deleted) {
return {};
return { namespaces: [] };
}
const deleteDocNotFound = body.result === 'not_found';

View file

@ -135,6 +135,15 @@ export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOpti
refresh?: MutatingOperationRefreshSetting;
}
/**
*
* @public
*/
export interface SavedObjectsAddToNamespacesResponse {
/** The namespaces the object exists in after this operation is complete. */
namespaces: string[];
}
/**
*
* @public
@ -144,6 +153,15 @@ export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBas
refresh?: MutatingOperationRefreshSetting;
}
/**
*
* @public
*/
export interface SavedObjectsDeleteFromNamespacesResponse {
/** The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. */
namespaces: string[];
}
/**
*
* @public
@ -320,7 +338,7 @@ export class SavedObjectsClient {
id: string,
namespaces: string[],
options: SavedObjectsAddToNamespacesOptions = {}
): Promise<{}> {
): Promise<SavedObjectsAddToNamespacesResponse> {
return await this._repository.addToNamespaces(type, id, namespaces, options);
}
@ -337,7 +355,7 @@ export class SavedObjectsClient {
id: string,
namespaces: string[],
options: SavedObjectsDeleteFromNamespacesOptions = {}
): Promise<{}> {
): Promise<SavedObjectsDeleteFromNamespacesResponse> {
return await this._repository.deleteFromNamespaces(type, id, namespaces, options);
}

View file

@ -2023,6 +2023,11 @@ export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOpti
version?: string;
}
// @public (undocumented)
export interface SavedObjectsAddToNamespacesResponse {
namespaces: string[];
}
// Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts
//
@ -2092,13 +2097,13 @@ export interface SavedObjectsBulkUpdateResponse<T = unknown> {
export class SavedObjectsClient {
// @internal
constructor(repository: ISavedObjectsRepository);
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>;
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<SavedObjectsAddToNamespacesResponse>;
bulkCreate<T = unknown>(objects: Array<SavedObjectsBulkCreateObject<T>>, options?: SavedObjectsCreateOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkGet<T = unknown>(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkUpdate<T = unknown>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBulkUpdateOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
create<T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise<SavedObject<T>>;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<SavedObjectsDeleteFromNamespacesResponse>;
// (undocumented)
static errors: typeof SavedObjectsErrorHelpers;
// (undocumented)
@ -2194,6 +2199,11 @@ export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBas
refresh?: MutatingOperationRefreshSetting;
}
// @public (undocumented)
export interface SavedObjectsDeleteFromNamespacesResponse {
namespaces: string[];
}
// @public (undocumented)
export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions {
refresh?: MutatingOperationRefreshSetting;
@ -2492,7 +2502,7 @@ export interface SavedObjectsRawDoc {
// @public (undocumented)
export class SavedObjectsRepository {
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>;
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<SavedObjectsAddToNamespacesResponse>;
bulkCreate<T = unknown>(objects: Array<SavedObjectsBulkCreateObject<T>>, options?: SavedObjectsCreateOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkGet<T = unknown>(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkUpdate<T = unknown>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBulkUpdateOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
@ -2503,7 +2513,7 @@ export class SavedObjectsRepository {
static createRepository(migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise<any>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<SavedObjectsDeleteFromNamespacesResponse>;
// (undocumented)
find<T = unknown>({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;

View file

@ -135,26 +135,36 @@ const expectPrivilegeCheck = async (fn: Function, args: Record<string, any>) =>
);
};
const expectObjectNamespaceFiltering = async (fn: Function, args: Record<string, any>) => {
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce(
getMockCheckPrivilegesSuccess // privilege check for authorization
);
const expectObjectNamespaceFiltering = async (
fn: Function,
args: Record<string, any>,
privilegeChecks = 1
) => {
for (let i = 0; i < privilegeChecks; i++) {
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce(
getMockCheckPrivilegesSuccess // privilege check for authorization
);
}
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation(
getMockCheckPrivilegesFailure // privilege check for namespace filtering
);
const authorizedNamespace = args.options.namespace || 'default';
const authorizedNamespace = args.options?.namespace || 'default';
const namespaces = ['some-other-namespace', authorizedNamespace];
const returnValue = { namespaces, foo: 'bar' };
// we don't know which base client method will be called; mock them all
clientOpts.baseClient.create.mockReturnValue(returnValue as any);
clientOpts.baseClient.get.mockReturnValue(returnValue as any);
clientOpts.baseClient.update.mockReturnValue(returnValue as any);
clientOpts.baseClient.addToNamespaces.mockReturnValue(returnValue as any);
clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(returnValue as any);
const result = await fn.bind(client)(...Object.values(args));
expect(result).toEqual(expect.objectContaining({ namespaces: [authorizedNamespace, '?'] }));
expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2);
expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(
privilegeChecks + 1
);
expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith(
'login:',
namespaces
@ -369,6 +379,11 @@ describe('#addToNamespaces', () => {
undefined // default namespace
);
});
test(`filters namespaces that the user doesn't have access to`, async () => {
// this operation is unique because it requires two privilege checks before it executes
await expectObjectNamespaceFiltering(client.addToNamespaces, { type, id, namespaces }, 2);
});
});
describe('#bulkCreate', () => {
@ -682,6 +697,10 @@ describe('#deleteFromNamespaces', () => {
namespaces
);
});
test(`filters namespaces that the user doesn't have access to`, async () => {
await expectObjectNamespaceFiltering(client.deleteFromNamespaces, { type, id, namespaces });
});
});
describe('#update', () => {

View file

@ -164,7 +164,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
// result in a 404 error.
await this.ensureAuthorized(type, 'update', namespace, args, 'addToNamespacesUpdate');
return await this.baseClient.addToNamespaces(type, id, namespaces, options);
const result = await this.baseClient.addToNamespaces(type, id, namespaces, options);
return await this.redactSavedObjectNamespaces(result);
}
public async deleteFromNamespaces(
@ -177,7 +178,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
// To un-share an object, the user must have the "delete" permission in each of the target namespaces.
await this.ensureAuthorized(type, 'delete', namespaces, args, 'deleteFromNamespaces');
return await this.baseClient.deleteFromNamespaces(type, id, namespaces, options);
const result = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options);
return await this.redactSavedObjectNamespaces(result);
}
public async bulkUpdate<T = unknown>(

View file

@ -423,7 +423,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
test(`supplements options with the current namespace`, async () => {
const { client, baseClient } = await createSpacesSavedObjectsClient();
const expectedReturnValue = createMockResponse();
const expectedReturnValue = { namespaces: ['foo', 'bar'] };
baseClient.addToNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue));
const type = Symbol();
@ -453,7 +453,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
test(`supplements options with the current namespace`, async () => {
const { client, baseClient } = await createSpacesSavedObjectsClient();
const expectedReturnValue = createMockResponse();
const expectedReturnValue = { namespaces: ['foo', 'bar'] };
baseClient.deleteFromNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue));
const type = Symbol();