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] Dashboard admin(groups/users) implementation #6554

Merged
merged 37 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9488790
[Workspace] dashboard admin(groups/users) implementation and integrat…
yubonluo Apr 12, 2024
4209c2a
Modify change log
yubonluo Apr 15, 2024
8f06c87
solve code conflict
yubonluo Apr 17, 2024
32224a6
optimize the code
yubonluo Apr 19, 2024
87c4288
optimize the code
yubonluo Apr 19, 2024
ed1ff7f
Merge branch 'main' into main-admin-back
yubonluo Apr 19, 2024
c4cd56a
modify change log
yubonluo Apr 19, 2024
efb52e1
Merge branch 'main-admin-back' of github.com:yubonluo/OpenSearch-Dash…
yubonluo Apr 19, 2024
34f0164
modify change log
yubonluo Apr 19, 2024
f434c84
solve change log issue
yubonluo Apr 19, 2024
67237c5
Changeset file for PR #6554 created/updated
opensearch-changeset-bot[bot] Apr 22, 2024
ee48ea3
[Workspace] delete useless code
yubonluo Apr 22, 2024
783c498
Merge branch 'main' of github.com:opensearch-project/OpenSearch-Dashb…
yubonluo Apr 22, 2024
1e96d92
Merge branch 'main' into main-admin-back
yubonluo Apr 24, 2024
5105dd9
Changeset file for PR #6554 created/updated
opensearch-changeset-bot[bot] Apr 24, 2024
be8aa31
Merge branch 'main' into main-admin-back
yubonluo Apr 24, 2024
df772bc
Merge branch 'main' into main-admin-back
yubonluo Apr 25, 2024
0ebe3d5
Merge branch 'main' into main-admin-back
yubonluo Apr 25, 2024
ea629d1
Merge branch 'main' into main-admin-back
yubonluo Apr 25, 2024
5fcb298
solve code conflict
yubonluo Apr 30, 2024
f4a9dc3
delete useless code
yubonluo Apr 30, 2024
27d63f8
Merge branch 'main' into main-admin-back
yubonluo Apr 30, 2024
03cf65d
solve the code conflicts
yubonluo May 6, 2024
e1abe9f
Merge branch 'main-admin-back' of github.com:yubonluo/OpenSearch-Dash…
yubonluo May 6, 2024
eea9e86
Resolve the code conflict
yubonluo May 17, 2024
797698f
Optimize the code
yubonluo May 17, 2024
da8e238
Merge branch 'main' of github.com:opensearch-project/OpenSearch-Dashb…
yubonluo May 20, 2024
1e0b749
Add unit test to cover setupPermission in plugin.
yubonluo May 20, 2024
422ee19
Merge branch 'main' of github.com:opensearch-project/OpenSearch-Dashb…
yubonluo May 21, 2024
570e684
delete the logic of dynamic application config
yubonluo May 21, 2024
347ed1b
Merge branch 'main' into main-admin-back
yubonluo May 23, 2024
05a3454
Merge branch 'main' of github.com:opensearch-project/OpenSearch-Dashb…
yubonluo May 23, 2024
fd10be4
Default to OSD admin if security uninstall
yubonluo May 23, 2024
9010a31
Merge branch 'main-admin-back' of github.com:yubonluo/OpenSearch-Dash…
yubonluo May 23, 2024
a9569af
Default to OSD admin if security uninstall
yubonluo May 24, 2024
6dd28ef
Merge branch 'main' of github.com:opensearch-project/OpenSearch-Dashb…
yubonluo May 24, 2024
e6f9a18
Merge branch 'main' into main-admin-back
yubonluo May 24, 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/6554.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- [Workspace] Dashboard admin(groups/users) implementation. ([#6554](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6554))
5 changes: 5 additions & 0 deletions config/opensearch_dashboards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,8 @@

# Set the value to true to enable enhancements for the data plugin
# data.enhancements.enabled: false

# Set the backend roles in groups or users, whoever has the backend roles or exactly match the user ids defined in this config will be regard as dashboard admin.
# Dashboard admin will have the access to all the workspaces(workspace.enabled: true) and objects inside OpenSearch Dashboards.
# opensearchDashboards.dashboardAdmin.groups: ["dashboard_admin"]
# opensearchDashboards.dashboardAdmin.users: ["dashboard_admin"]
1 change: 1 addition & 0 deletions src/core/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function pluginInitializerContextConfigMock<T>(config: T) {
configIndex: '.opensearch_dashboards_config_tests',
autocompleteTerminateAfter: duration(100000),
autocompleteTimeout: duration(1000),
dashboardAdmin: { groups: [], users: [] },
futureNavigation: false,
},
opensearch: {
Expand Down
8 changes: 8 additions & 0 deletions src/core/server/opensearch_dashboards_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ export const config = {
defaultValue: 'https://survey.opensearch.org',
}),
}),
dashboardAdmin: schema.object({
groups: schema.arrayOf(schema.string(), {
defaultValue: [],
}),
users: schema.arrayOf(schema.string(), {
defaultValue: [],
}),
}),
futureNavigation: schema.boolean({ defaultValue: false }),
}),
deprecations,
Expand Down
1 change: 1 addition & 0 deletions src/core/server/plugins/plugin_context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ describe('createPluginInitializerContext', () => {
configIndex: '.opensearch_dashboards_config',
autocompleteTerminateAfter: duration(100000),
autocompleteTimeout: duration(1000),
dashboardAdmin: { groups: [], users: [] },
futureNavigation: false,
},
opensearch: {
Expand Down
1 change: 1 addition & 0 deletions src/core/server/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export const SharedGlobalConfigKeys = {
'configIndex',
'autocompleteTerminateAfter',
'autocompleteTimeout',
'dashboardAdmin',
'futureNavigation',
] as const,
opensearch: ['shardTimeout', 'requestTimeout', 'pingTimeout'] as const,
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/utils/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ describe('updateWorkspaceState', () => {
const requestMock = httpServerMock.createOpenSearchDashboardsRequest();
updateWorkspaceState(requestMock, {
requestWorkspaceId: 'foo',
isDashboardAdmin: true,
});
expect(getWorkspaceState(requestMock)).toEqual({
requestWorkspaceId: 'foo',
isDashboardAdmin: true,
});
});
});
4 changes: 3 additions & 1 deletion src/core/server/utils/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { OpenSearchDashboardsRequest, ensureRawRequest } from '../http/router';

export interface WorkspaceState {
requestWorkspaceId?: string;
isDashboardAdmin?: boolean;
}

/**
Expand All @@ -29,8 +30,9 @@ export const updateWorkspaceState = (
};

export const getWorkspaceState = (request: OpenSearchDashboardsRequest): WorkspaceState => {
const { requestWorkspaceId } = ensureRawRequest(request).app as WorkspaceState;
const { requestWorkspaceId, isDashboardAdmin } = ensureRawRequest(request).app as WorkspaceState;
return {
requestWorkspaceId,
isDashboardAdmin,
};
};
4 changes: 4 additions & 0 deletions src/legacy/server/config/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ export default () =>
survey: Joi.object({
url: Joi.any().default('/'),
}),
dashboardAdmin: Joi.object({
groups: Joi.array().items(Joi.string()).default([]),
users: Joi.array().items(Joi.string()).default([]),
}),
futureNavigation: Joi.boolean().default(false),
}).default(),

Expand Down
67 changes: 66 additions & 1 deletion src/plugins/workspace/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { OnPreRoutingHandler } from 'src/core/server';
import { OnPostAuthHandler, OnPreRoutingHandler } from 'src/core/server';
import { coreMock, httpServerMock } from '../../../core/server/mocks';
import { WorkspacePlugin } from './plugin';
import { getWorkspaceState } from '../../../core/server/utils';
import * as utilsExports from './utils';

describe('Workspace server plugin', () => {
it('#setup', async () => {
Expand Down Expand Up @@ -67,6 +68,70 @@ describe('Workspace server plugin', () => {
expect(toolKitMock.next).toBeCalledTimes(1);
});

describe('#setupPermission', () => {
const setupMock = coreMock.createSetup();
const initializerContextConfigMock = coreMock.createPluginInitializerContext({
enabled: true,
permission: {
enabled: true,
},
});
let registerOnPostAuthFn: OnPostAuthHandler = () => httpServerMock.createResponseFactory().ok();
setupMock.http.registerOnPostAuth.mockImplementation((fn) => {
registerOnPostAuthFn = fn;
return fn;
});
const workspacePlugin = new WorkspacePlugin(initializerContextConfigMock);
const requestWithWorkspaceInUrl = httpServerMock.createOpenSearchDashboardsRequest({
path: '/w/foo/app',
});

it('catch error', async () => {
await workspacePlugin.setup(setupMock);
const toolKitMock = httpServerMock.createToolkit();

await registerOnPostAuthFn(
requestWithWorkspaceInUrl,
httpServerMock.createResponseFactory(),
toolKitMock
);
expect(toolKitMock.next).toBeCalledTimes(1);
});

it('with yml config', async () => {
jest
.spyOn(utilsExports, 'getPrincipalsFromRequest')
.mockImplementation(() => ({ users: [`user1`] }));
jest
.spyOn(utilsExports, 'getOSDAdminConfigFromYMLConfig')
.mockResolvedValue([['group1'], ['user1']]);

await workspacePlugin.setup(setupMock);
const toolKitMock = httpServerMock.createToolkit();

await registerOnPostAuthFn(
requestWithWorkspaceInUrl,
httpServerMock.createResponseFactory(),
toolKitMock
);
expect(toolKitMock.next).toBeCalledTimes(1);
});

it('uninstall security plugin', async () => {
jest.spyOn(utilsExports, 'getPrincipalsFromRequest').mockImplementation(() => ({}));

await workspacePlugin.setup(setupMock);
const toolKitMock = httpServerMock.createToolkit();

await registerOnPostAuthFn(
requestWithWorkspaceInUrl,
httpServerMock.createResponseFactory(),
toolKitMock
);
expect(toolKitMock.next).toBeCalledTimes(1);
});
});

it('#start', async () => {
const setupMock = coreMock.createSetup();
const startMock = coreMock.createStart();
Expand Down
52 changes: 39 additions & 13 deletions src/plugins/workspace/server/plugin.ts
yubonluo marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
SavedObjectsPermissionControl,
SavedObjectsPermissionControlContract,
} from './permission_control/client';
import { getOSDAdminConfigFromYMLConfig, updateDashboardAdminStateForRequest } from './utils';
import { WorkspaceIdConsumerWrapper } from './saved_objects/workspace_id_consumer_wrapper';
import { WorkspaceUiSettingsClientWrapper } from './saved_objects/workspace_ui_settings_client_wrapper';

Expand Down Expand Up @@ -71,6 +72,43 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
});
}

private setupPermission(core: CoreSetup) {
this.permissionControl = new SavedObjectsPermissionControl(this.logger);

core.http.registerOnPostAuth(async (request, response, toolkit) => {
let groups: string[];
let users: string[];

// There may be calls to saved objects client before user get authenticated, need to add a try catch here as `getPrincipalsFromRequest` will throw error when user is not authenticated.
try {
({ groups = [], users = [] } = this.permissionControl!.getPrincipalsFromRequest(request));
} catch (e) {
return toolkit.next();
}
// If the security plugin is not installed, login defaults to OSD Admin
if (!groups.length && !users.length) {
updateWorkspaceState(request, {
isDashboardAdmin: true,
});
return toolkit.next();
}

const [configGroups, configUsers] = await getOSDAdminConfigFromYMLConfig(this.globalConfig$);
updateDashboardAdminStateForRequest(request, groups, users, configGroups, configUsers);
return toolkit.next();
});

this.workspaceSavedObjectsClientWrapper = new WorkspaceSavedObjectsClientWrapper(
this.permissionControl
);

core.savedObjects.addClientWrapper(
PRIORITY_FOR_PERMISSION_CONTROL_WRAPPER,
WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID,
this.workspaceSavedObjectsClientWrapper.wrapperFactory
);
}

constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
this.globalConfig$ = initializerContext.config.legacy.globalConfig$;
Expand Down Expand Up @@ -110,19 +148,7 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl

const maxImportExportSize = core.savedObjects.getImportExportObjectLimit();
this.logger.info('Workspace permission control enabled:' + isPermissionControlEnabled);
if (isPermissionControlEnabled) {
this.permissionControl = new SavedObjectsPermissionControl(this.logger);

this.workspaceSavedObjectsClientWrapper = new WorkspaceSavedObjectsClientWrapper(
this.permissionControl
);

core.savedObjects.addClientWrapper(
PRIORITY_FOR_PERMISSION_CONTROL_WRAPPER,
WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID,
this.workspaceSavedObjectsClientWrapper.wrapperFactory
);
}
if (isPermissionControlEnabled) this.setupPermission(core);

registerRoutes({
http: core.http,
Expand Down
Loading
Loading