Skip to content

Commit

Permalink
add data source validate
Browse files Browse the repository at this point in the history
Signed-off-by: yubonluo <[email protected]>
  • Loading branch information
yubonluo committed Nov 29, 2024
1 parent 1d13f30 commit cece0f3
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 367 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,65 @@ describe('WorkspaceIdConsumerWrapper', () => {
expect(wrapperClient.get('type', 'id')).rejects.toMatchInlineSnapshot(`[Error: Not Found]`);
expect(mockedClient.get).toHaveBeenCalledTimes(1);
});

it(`Should throw error when the options.workspaces has more than one workspace.`, async () => {
const savedObject = {
type: 'dashboard',
id: 'dashboard_id',
attributes: {},
references: [],
workspaces: ['bar'],
};
const options = { workspaces: ['foo', 'bar'] };
expect(
wrapperClient.get(savedObject.type, savedObject.id, options)
).rejects.toMatchInlineSnapshot(`[Error: Multiple workspace parameters: Bad Request]`);
expect(mockedClient.get).not.toBeCalled();
});

it(`Should get data source when user is data source admin`, async () => {
const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
updateWorkspaceState(mockRequest, { isDataSourceAdmin: true, requestWorkspaceId: 'foo' });
const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({
client: mockedClient,
typeRegistry: requestHandlerContext.savedObjects.typeRegistry,
request: mockRequest,
});
const savedObject = {
type: 'data-source',
id: 'data-source_id',
attributes: {},
references: [],
};
mockedClient.get.mockResolvedValueOnce(savedObject);
const result = await mockedWrapperClient.get(savedObject.type, savedObject.id);
expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {});
expect(result).toEqual(savedObject);
});

it(`Should throw error when the object is global data source`, async () => {
const savedObject = {
type: 'data-source',
id: 'data-source_id',
attributes: {},
references: [],
};
mockedClient.get.mockResolvedValueOnce(savedObject);
mockedClient.get.mockResolvedValueOnce(savedObject);
expect(wrapperClient.get(savedObject.type, savedObject.id)).rejects.toMatchInlineSnapshot(
`[Error: Saved object [data-source/data-source_id] not found]`
);
expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {});
});
});

describe('bulkGet', () => {
const payload = [
{ id: 'dashboard_id', type: 'dashboard' },
{ id: 'visualization_id', type: 'visualization' },
{ id: 'global_data_source_id', type: 'data-source' },
{ id: 'data_source_id', type: 'data-source' },
];
const savedObjects = [
{
Expand All @@ -324,6 +377,31 @@ describe('WorkspaceIdConsumerWrapper', () => {
references: [],
workspaces: ['bar'],
},
{
type: 'config',
id: 'config_id',
attributes: {},
references: [],
},
{
type: 'workspace',
id: 'workspace_id',
attributes: {},
references: [],
},
{
type: 'data-source',
id: 'global_data_source_id',
attributes: {},
references: [],
},
{
type: 'data-source',
id: 'data_source_id',
attributes: {},
references: [],
workspaces: ['foo'],
},
];
const options = { workspaces: ['foo'] };
beforeEach(() => {
Expand Down Expand Up @@ -360,6 +438,38 @@ describe('WorkspaceIdConsumerWrapper', () => {
"bar",
],
},
Object {
"attributes": Object {},
"id": "config_id",
"references": Array [],
"type": "config",
},
Object {
"attributes": Object {},
"id": "workspace_id",
"references": Array [],
"type": "workspace",
},
Object {
"attributes": Object {},
"error": Object {
"error": "Not Found",
"message": "Saved object [data-source/global_data_source_id] not found",
"statusCode": 404,
},
"id": "global_data_source_id",
"references": Array [],
"type": "data-source",
},
Object {
"attributes": Object {},
"id": "data_source_id",
"references": Array [],
"type": "data-source",
"workspaces": Array [
"foo",
],
},
],
}
`);
Expand Down Expand Up @@ -395,6 +505,38 @@ describe('WorkspaceIdConsumerWrapper', () => {
"bar",
],
},
Object {
"attributes": Object {},
"id": "config_id",
"references": Array [],
"type": "config",
},
Object {
"attributes": Object {},
"id": "workspace_id",
"references": Array [],
"type": "workspace",
},
Object {
"attributes": Object {},
"error": Object {
"error": "Not Found",
"message": "Saved object [data-source/global_data_source_id] not found",
"statusCode": 404,
},
"id": "global_data_source_id",
"references": Array [],
"type": "data-source",
},
Object {
"attributes": Object {},
"id": "data_source_id",
"references": Array [],
"type": "data-source",
"workspaces": Array [
"foo",
],
},
],
}
`);
Expand Down Expand Up @@ -422,5 +564,83 @@ describe('WorkspaceIdConsumerWrapper', () => {
expect(wrapperClient.bulkGet(payload)).rejects.toMatchInlineSnapshot(`[Error: Not Found]`);
expect(mockedClient.bulkGet).toBeCalledWith(payload, {});
});

it(`Should throw error when the options.workspaces has more than one workspace.`, async () => {
expect(
wrapperClient.bulkGet(payload, { workspaces: ['foo', 'var'] })
).rejects.toMatchInlineSnapshot(`[Error: Multiple workspace parameters: Bad Request]`);
expect(mockedClient.bulkGet).not.toBeCalled();
});

it(`Should bulkGet data source when user is data source admin`, async () => {
const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
updateWorkspaceState(mockRequest, { isDataSourceAdmin: true, requestWorkspaceId: 'foo' });
const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({
client: mockedClient,
typeRegistry: requestHandlerContext.savedObjects.typeRegistry,
request: mockRequest,
});

mockedClient.bulkGet.mockResolvedValueOnce({ saved_objects: savedObjects });
const result = await mockedWrapperClient.bulkGet(payload);
expect(mockedClient.bulkGet).toBeCalledWith(payload, {});
expect(result).toMatchInlineSnapshot(`
Object {
"saved_objects": Array [
Object {
"attributes": Object {},
"id": "dashboard_id",
"references": Array [],
"type": "dashboard",
"workspaces": Array [
"foo",
],
},
Object {
"attributes": Object {},
"error": Object {
"error": "Not Found",
"message": "Saved object [visualization/visualization_id] not found",
"statusCode": 404,
},
"id": "visualization_id",
"references": Array [],
"type": "visualization",
"workspaces": Array [
"bar",
],
},
Object {
"attributes": Object {},
"id": "config_id",
"references": Array [],
"type": "config",
},
Object {
"attributes": Object {},
"id": "workspace_id",
"references": Array [],
"type": "workspace",
},
Object {
"attributes": Object {},
"id": "global_data_source_id",
"references": Array [],
"type": "data-source",
},
Object {
"attributes": Object {},
"id": "data_source_id",
"references": Array [],
"type": "data-source",
"workspaces": Array [
"foo",
],
},
],
}
`);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
SavedObjectsBulkResponse,
} from '../../../../core/server';
import { IWorkspaceClientImpl } from '../types';
import { validateIsWorkspaceDataSourceAndConnectionObjectType } from '../../common/utils';

const UI_SETTINGS_SAVED_OBJECTS_TYPE = 'config';

Expand Down Expand Up @@ -51,24 +52,30 @@ export class WorkspaceIdConsumerWrapper {
return type === UI_SETTINGS_SAVED_OBJECTS_TYPE;
}

// If the object.workspaces is null/[] (as global object), return it directly.
// If the workspace is specified, validate whether the object exists in the workspace.
private formatObjectForWorkspaceValidation<T>(
private validateObjectInAWorkspace<T>(
object: SavedObject<T>,
workspace: string
): SavedObject<T> {
if (object.workspaces && object.workspaces.length > 0) {
if (!object.workspaces.includes(workspace)) {
return {
...object,
error: {
...SavedObjectsErrorHelpers.createGenericNotFoundError(object.type, object.id).output
.payload,
},
};
workspace: string,
request: OpenSearchDashboardsRequest
) {
// Data source is a workspace level object, validate if the request has access to the data source within the requested workspace.
if (validateIsWorkspaceDataSourceAndConnectionObjectType(object.type)) {
if (!!getWorkspaceState(request).isDataSourceAdmin) {
return true;
}
// Deny access if the object is a global data source (no workspaces assigned)
if (!object.workspaces || object.workspaces.length === 0) {
return false;
}
}
return object;
/*
* Allow access if the requested workspace matches one of the object's assigned workspaces
* This ensures that the user can only access data sources within their current workspace
*/
if (object.workspaces && object.workspaces.length > 0) {
return object.workspaces.includes(workspace);
}
// Allow access if the object is a global object (object.workspaces is null/[])
return true;
}

public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => {
Expand Down Expand Up @@ -153,15 +160,28 @@ export class WorkspaceIdConsumerWrapper {
objects: SavedObjectsBulkGetObject[] = [],
options: SavedObjectsBaseOptions = {}
): Promise<SavedObjectsBulkResponse<T>> => {
const objectToBulkGet = await wrapperOptions.client.bulkGet<T>(objects, options);
const { workspaces } = this.formatWorkspaceIdParams(wrapperOptions.request, options);
if (!!workspaces && workspaces.length > 1) {
// Version 2.18 does not support the passing of multiple workspaces.
throw SavedObjectsErrorHelpers.createBadRequestError('Multiple workspace parameters');
}

const objectToBulkGet = await wrapperOptions.client.bulkGet<T>(objects, options);

if (workspaces?.length === 1) {
return {
...objectToBulkGet,
saved_objects: objectToBulkGet.saved_objects.map((object) =>
this.formatObjectForWorkspaceValidation(object, workspaces[0])
),
saved_objects: objectToBulkGet.saved_objects.map((object) => {
return this.validateObjectInAWorkspace(object, workspaces[0], wrapperOptions.request)
? object
: {
...object,
error: {
...SavedObjectsErrorHelpers.createGenericNotFoundError(object.type, object.id)
.output.payload,
},
};
}),
};
}

Expand All @@ -172,19 +192,22 @@ export class WorkspaceIdConsumerWrapper {
id: string,
options: SavedObjectsBaseOptions = {}
): Promise<SavedObject<T>> => {
const objectToGet = await wrapperOptions.client.get<T>(type, id, options);
const { workspaces } = this.formatWorkspaceIdParams(wrapperOptions.request, options);
if (!!workspaces && workspaces.length > 1) {
// Version 2.18 does not support the passing of multiple workspaces.
throw SavedObjectsErrorHelpers.createBadRequestError('Multiple workspace parameters');
}

if (workspaces?.length === 1) {
const validatedObject = this.formatObjectForWorkspaceValidation(
objectToGet,
workspaces[0]
);
if (validatedObject.error) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
const objectToGet = await wrapperOptions.client.get<T>(type, id, options);

if (
workspaces?.length === 1 &&
!this.validateObjectInAWorkspace(objectToGet, workspaces[0], wrapperOptions.request)
) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}

// Allow access if no specific workspace is requested.
return objectToGet;
},
update: wrapperOptions.client.update,
Expand Down
Loading

0 comments on commit cece0f3

Please sign in to comment.