Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Workspace]Restrict saved objects finding when workspace enabled #7125

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
30fab0d
Limit data source saved objects finding and access when workspace ena…
wanglam Jun 28, 2024
17b36b6
Remove options.workspaces drop behavior in conflict client wrapper
wanglam Jun 28, 2024
dadd9b7
Changeset file for PR #7125 created/updated
opensearch-changeset-bot[bot] Jun 28, 2024
1d08d8c
Merge branch 'main' into limit-data-source-permission-when-workspace-…
wanglam Jun 30, 2024
c996790
Merge branch 'main' into limit-data-source-permission-when-workspace-…
ruanyl Jul 2, 2024
b54291c
Update src/plugins/workspace/server/saved_objects/workspace_saved_obj…
wanglam Jul 2, 2024
899341c
Update src/plugins/workspace/server/saved_objects/workspace_saved_obj…
wanglam Jul 2, 2024
a197688
Remove formatFindParams
wanglam Jul 2, 2024
7b491fd
Fix code format
wanglam Jul 3, 2024
ca51d01
Merge branch 'main' into limit-data-source-permission-when-workspace-…
wanglam Jul 4, 2024
a2e6c27
Fix ACLSearchParams and empty workspaces
wanglam Jul 4, 2024
9bbb6a3
Refactor workspace client wrapper find
wanglam Jul 4, 2024
15e131e
Remove global saved objects validation logic
wanglam Jul 4, 2024
eb8bc9c
Update uts and its
wanglam Jul 5, 2024
8cb4cb3
Changeset file for PR #7125 created/updated
opensearch-changeset-bot[bot] Jul 5, 2024
97dcde0
Remove no need changes and refactor
wanglam Jul 5, 2024
12cfce7
Merge branch 'main' into limit-data-source-permission-when-workspace-…
wanglam Jul 5, 2024
2b1c28b
Fix global saved objects not found
wanglam Jul 5, 2024
185423f
Change saved object get to assert
wanglam Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/7125.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fix:
- [Workspace]Limit data source permission when workspace enabled ([#7125](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7125))
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 @@ -499,20 +499,4 @@ describe('WorkspaceConflictSavedObjectsClientWrapper', () => {
);
});
});

describe('find', () => {
beforeEach(() => {
mockedClient.find.mockClear();
});

it(`workspaces parameters should be removed when finding data sources`, async () => {
await wrapperClient.find({
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
workspaces: ['foo'],
});
expect(mockedClient.find).toBeCalledWith({
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,7 @@ export class WorkspaceConflictSavedObjectsClientWrapper {
bulkCreate: bulkCreateWithWorkspaceConflictCheck,
checkConflicts: checkConflictWithWorkspaceConflictCheck,
delete: wrapperOptions.client.delete,
find: (options: SavedObjectsFindOptions) =>
// TODO: The `formatFindParams` is a workaround for 2.14 to always list global data sources,
// should remove this workaround in the upcoming release once readonly share is available.
wrapperOptions.client.find(this.formatFindParams(options)),
find: wrapperOptions.client.find,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove formatFindParams since it's no longer used?

bulkGet: wrapperOptions.client.bulkGet,
get: wrapperOptions.client.get,
update: wrapperOptions.client.update,
Expand Down
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) {
wanglam marked this conversation as resolved.
Show resolved Hide resolved
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.
wanglam marked this conversation as resolved.
Show resolved Hide resolved
*/
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
Loading