Skip to content

Commit

Permalink
Limit data source saved objects finding and access when workspace ena…
Browse files Browse the repository at this point in the history
…bled

Signed-off-by: Lin Wang <[email protected]>
  • Loading branch information
wanglam committed Jun 28, 2024
1 parent 5ee6f58 commit 30fab0d
Show file tree
Hide file tree
Showing 3 changed files with 272 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import * as utilsExports from '../../utils';
import { updateWorkspaceState } from '../../../../../core/server/utils';
import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../../../data_source/common';

const repositoryKit = (() => {
const savedObjects: Array<{ type: string; id: string }> = [];
Expand Down Expand Up @@ -78,6 +79,9 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
enabled: true,
},
},
data_source: {
enabled: true,
},
migrations: { skip: false },
},
},
Expand Down Expand Up @@ -894,4 +898,112 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
expect(SavedObjectsErrorHelpers.isNotFoundError(ACLError)).toBe(true);
});
});

describe('data source', () => {
beforeAll(async () => {
await repositoryKit.create(
internalSavedObjectsRepository,
DATA_SOURCE_SAVED_OBJECT_TYPE,
{
title: 'Global data source',
},
{
id: 'global-data-source',
}
);
await repositoryKit.create(
internalSavedObjectsRepository,
DATA_SOURCE_SAVED_OBJECT_TYPE,
{
title: 'Data source in workspace 1',
},
{
id: 'data-source-in-workspace-1',
workspaces: ['workspace-1'],
}
);
});

it('should throw permission error when get global data source with non dashboard admin', async () => {
let error;
try {
await permittedSavedObjectedClient.get(DATA_SOURCE_SAVED_OBJECT_TYPE, 'global-data-source');
} catch (e) {
error = e;
}
expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(true);
});

it('should return requested data source normally for non dashboard admin', async () => {
const dataSource = await permittedSavedObjectedClient.get(
DATA_SOURCE_SAVED_OBJECT_TYPE,
'data-source-in-workspace-1'
);
expect(dataSource).toEqual(
expect.objectContaining({
attributes: expect.objectContaining({
title: 'Data source in workspace 1',
}),
})
);
});

it('should return requested global data source for dashboard admin', async () => {
const dataSource = await dashboardAdminSavedObjectedClient.get(
DATA_SOURCE_SAVED_OBJECT_TYPE,
'global-data-source'
);
expect(dataSource).toEqual(
expect.objectContaining({
attributes: expect.objectContaining({
title: 'Global data source',
}),
})
);
});

it('should filter out global data source for non dashboard admin', async () => {
const dataSourcesResult = await permittedSavedObjectedClient.find({
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
});
expect(dataSourcesResult.total).toEqual(1);
expect(dataSourcesResult.saved_objects).toEqual([
expect.objectContaining({
attributes: expect.objectContaining({
title: 'Data source in workspace 1',
}),
}),
]);
});

it('should return empty data source list when find with not permitted workspace', async () => {
const dataSourcesResult = await permittedSavedObjectedClient.find({
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
workspaces: ['one-not-permitted-workspace'],
});
expect(dataSourcesResult.total).toEqual(0);
expect(dataSourcesResult.saved_objects).toEqual([]);
});

it('should return all data sources for dashboard admin', async () => {
const dataSourcesResult = await dashboardAdminSavedObjectedClient.find({
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
});
expect(dataSourcesResult.total).toEqual(2);
expect(dataSourcesResult.saved_objects).toEqual(
expect.arrayContaining([
expect.objectContaining({
attributes: expect.objectContaining({
title: 'Global data source',
}),
}),
expect.objectContaining({
attributes: expect.objectContaining({
title: 'Data source in workspace 1',
}),
}),
])
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getWorkspaceState, updateWorkspaceState } from '../../../../core/server
import { SavedObjectsErrorHelpers } from '../../../../core/server';
import { WorkspaceSavedObjectsClientWrapper } from './workspace_saved_objects_client_wrapper';
import { httpServerMock } from '../../../../core/server/mocks';
import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../../data_source/common';

const DASHBOARD_ADMIN = 'dashnoard_admin';
const NO_DASHBOARD_ADMIN = 'no_dashnoard_admin';
Expand Down Expand Up @@ -46,6 +47,17 @@ const generateWorkspaceSavedObjectsClientWrapper = (role = NO_DASHBOARD_ADMIN) =
id: 'not-permitted-workspace',
attributes: { name: 'Not permitted workspace' },
},
{
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
id: 'global-data-source',
attributes: { title: 'Global data source' },
},
{
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
id: 'workspace-1-data-source',
attributes: { title: 'Workspace 1 data source' },
workspaces: ['workspace-1'],
},
];
const clientMock = {
get: jest.fn().mockImplementation(async (type, id) => {
Expand Down Expand Up @@ -77,7 +89,17 @@ const generateWorkspaceSavedObjectsClientWrapper = (role = NO_DASHBOARD_ADMIN) =
),
};
}),
find: jest.fn(),
find: jest.fn().mockImplementation(({ type, workspaces }) => {
const savedObjects = savedObjectsStore.filter(
(item) =>
item.type === type &&
(!workspaces || item.workspaces?.some((workspaceId) => workspaces.includes(workspaceId)))
);
return {
saved_objects: savedObjects,
total: savedObjects.length,
};
}),
deleteByWorkspace: jest.fn(),
};
const requestMock = httpServerMock.createOpenSearchDashboardsRequest();
Expand Down Expand Up @@ -880,5 +902,110 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
expect(permissionControlMock.validate).not.toHaveBeenCalled();
});
});

describe('data source', () => {
it('should throw permission error when accessing global data source with non dashboard admin', async () => {
const { wrapper } = generateWorkspaceSavedObjectsClientWrapper();
let errorCatched;
try {
await wrapper.get(DATA_SOURCE_SAVED_OBJECT_TYPE, 'global-data-source');
} catch (e) {
errorCatched = e;
}
expect(errorCatched?.message).toEqual('Invalid saved objects permission');
});
it('should return global data source for dashboard admin', async () => {
const { wrapper } = generateWorkspaceSavedObjectsClientWrapper(DASHBOARD_ADMIN);
const dataSource = await wrapper.get(DATA_SOURCE_SAVED_OBJECT_TYPE, 'global-data-source');
expect(dataSource).toEqual(
expect.objectContaining({
attributes: expect.objectContaining({
title: 'Global data source',
}),
})
);
});

it('should return workspace 1 data source for permitted user', async () => {
const { wrapper } = generateWorkspaceSavedObjectsClientWrapper();
const dataSource = await wrapper.get(
DATA_SOURCE_SAVED_OBJECT_TYPE,
'workspace-1-data-source'
);
expect(dataSource).toEqual(
expect.objectContaining({
attributes: expect.objectContaining({
title: 'Workspace 1 data source',
}),
})
);
});

it('should return all data sources for dashboard admin', async () => {
const { wrapper, clientMock } = generateWorkspaceSavedObjectsClientWrapper(DASHBOARD_ADMIN);
const result = await wrapper.find({ type: DATA_SOURCE_SAVED_OBJECT_TYPE });

expect(clientMock.find).toHaveBeenCalledWith({
type: 'data-source',
});
expect(result.saved_objects).toMatchInlineSnapshot(`
Array [
Object {
"attributes": Object {
"title": "Global data source",
},
"id": "global-data-source",
"type": "data-source",
},
Object {
"attributes": Object {
"title": "Workspace 1 data source",
},
"id": "workspace-1-data-source",
"type": "data-source",
"workspaces": Array [
"workspace-1",
],
},
]
`);
});

it('should only return permitted data source for non dashboard admin', async () => {
const { wrapper, clientMock } = generateWorkspaceSavedObjectsClientWrapper();
const result = await wrapper.find({ type: DATA_SOURCE_SAVED_OBJECT_TYPE });

expect(clientMock.find).toHaveBeenCalledWith({
type: 'data-source',
workspaces: ['workspace-1'],
ACLSearchParams: {},
workspacesSearchOperator: 'AND',
});
expect(result.saved_objects).toMatchInlineSnapshot(`
Array [
Object {
"attributes": Object {
"title": "Workspace 1 data source",
},
"id": "workspace-1-data-source",
"type": "data-source",
"workspaces": Array [
"workspace-1",
],
},
]
`);
});

it('should return empty data source list for not permitted workspace non dashboard admin', async () => {
const { wrapper, clientMock } = generateWorkspaceSavedObjectsClientWrapper();
const result = await wrapper.find({
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
workspaces: ['workspace-2'],
});

expect(result.saved_objects).toMatchInlineSnapshot(`Array []`);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID,
WorkspacePermissionMode,
} from '../../common/constants';
import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../../data_source/common';

// Can't throw unauthorized for now, the page will be refreshed if unauthorized
const generateWorkspacePermissionError = () =>
Expand Down Expand Up @@ -158,6 +159,14 @@ export class WorkspaceSavedObjectsClientWrapper {
objectPermissionModes: WorkspacePermissionMode[],
validateAllWorkspaces = true
) {
/**
* A data source saved object without workspaces attributes will be treated as a global data source.
* This kind of data source is not allowed for non dashboard admin. The dashboard admin will bypass this
* client wrapper, so denied all access to the global data source saved object here.
*/
if (savedObject.type === DATA_SOURCE_SAVED_OBJECT_TYPE && !savedObject.workspaces) {
return false;
}
/**
*
* Checks if the provided saved object lacks both workspaces and permissions.
Expand Down Expand Up @@ -490,7 +499,29 @@ export class WorkspaceSavedObjectsClientWrapper {
})
).saved_objects.map((item) => item.id);

if (options.workspaces && options.workspaces.length > 0) {
if (options.type === DATA_SOURCE_SAVED_OBJECT_TYPE) {
// Overwrite the acl search params and workspace search operator here to avoid any global data source saved objects be response.
options.ACLSearchParams = {};
options.workspacesSearchOperator = 'AND';
/**
* If options.workspaces is not defined, find all data sources within user permitted workspaces.
* If options.workspaces is defined, filter out not permitted workspaces before find data sources.
*/
options.workspaces = options.workspaces
? options.workspaces.filter((item) => permittedWorkspaceIds.includes(item))
: permittedWorkspaceIds;

/**
* Passing an empty workspaces array will lead to the generated query missing the workspaces condition,
* which would return all data. Therefore, direct return an empty result when no permitted workspaces.
*/
if (!options.workspaces.length) {
return {
saved_objects: [],
total: 0,
};
}
} else if (options.workspaces && options.workspaces.length > 0) {
const permittedWorkspaces = options.workspaces.filter((item) =>
permittedWorkspaceIds.includes(item)
);
Expand Down

0 comments on commit 30fab0d

Please sign in to comment.