Skip to content

Commit b839da1

Browse files
[Workspace] Dashboard admin(groups/users) implementation (#6554)
* [Workspace] dashboard admin(groups/users) implementation and integrating with dynamic application config Signed-off-by: yubonluo <[email protected]> * Modify change log Signed-off-by: yubonluo <[email protected]> * optimize the code Signed-off-by: yubonluo <[email protected]> * modify change log Signed-off-by: yubonluo <[email protected]> * modify change log Signed-off-by: yubonluo <[email protected]> * solve change log issue Signed-off-by: yubonluo <[email protected]> * Changeset file for PR #6554 created/updated * [Workspace] delete useless code Signed-off-by: yubonluo <[email protected]> * Changeset file for PR #6554 created/updated * delete useless code Signed-off-by: yubonluo <[email protected]> * Optimize the code Signed-off-by: yubonluo <[email protected]> * Add unit test to cover setupPermission in plugin. Signed-off-by: yubonluo <[email protected]> * delete the logic of dynamic application config Signed-off-by: yubonluo <[email protected]> * Default to OSD admin if security uninstall Signed-off-by: yubonluo <[email protected]> * Default to OSD admin if security uninstall Signed-off-by: yubonluo <[email protected]> --------- Signed-off-by: yubonluo <[email protected]> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
1 parent 3e9a159 commit b839da1

16 files changed

+552
-19
lines changed

changelogs/fragments/6554.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- [Workspace] Dashboard admin(groups/users) implementation. ([#6554](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6554))

config/opensearch_dashboards.yml

+5
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,8 @@
334334

335335
# Set the value to true to enable enhancements for the data plugin
336336
# data.enhancements.enabled: false
337+
338+
# 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.
339+
# Dashboard admin will have the access to all the workspaces(workspace.enabled: true) and objects inside OpenSearch Dashboards.
340+
# opensearchDashboards.dashboardAdmin.groups: ["dashboard_admin"]
341+
# opensearchDashboards.dashboardAdmin.users: ["dashboard_admin"]

src/core/server/mocks.ts

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export function pluginInitializerContextConfigMock<T>(config: T) {
8080
configIndex: '.opensearch_dashboards_config_tests',
8181
autocompleteTerminateAfter: duration(100000),
8282
autocompleteTimeout: duration(1000),
83+
dashboardAdmin: { groups: [], users: [] },
8384
futureNavigation: false,
8485
},
8586
opensearch: {

src/core/server/opensearch_dashboards_config.ts

+8
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ export const config = {
9191
defaultValue: 'https://survey.opensearch.org',
9292
}),
9393
}),
94+
dashboardAdmin: schema.object({
95+
groups: schema.arrayOf(schema.string(), {
96+
defaultValue: [],
97+
}),
98+
users: schema.arrayOf(schema.string(), {
99+
defaultValue: [],
100+
}),
101+
}),
94102
futureNavigation: schema.boolean({ defaultValue: false }),
95103
}),
96104
deprecations,

src/core/server/plugins/plugin_context.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ describe('createPluginInitializerContext', () => {
101101
configIndex: '.opensearch_dashboards_config',
102102
autocompleteTerminateAfter: duration(100000),
103103
autocompleteTimeout: duration(1000),
104+
dashboardAdmin: { groups: [], users: [] },
104105
futureNavigation: false,
105106
},
106107
opensearch: {

src/core/server/plugins/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ export const SharedGlobalConfigKeys = {
292292
'configIndex',
293293
'autocompleteTerminateAfter',
294294
'autocompleteTimeout',
295+
'dashboardAdmin',
295296
'futureNavigation',
296297
] as const,
297298
opensearch: ['shardTimeout', 'requestTimeout', 'pingTimeout'] as const,

src/core/server/utils/workspace.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ describe('updateWorkspaceState', () => {
1111
const requestMock = httpServerMock.createOpenSearchDashboardsRequest();
1212
updateWorkspaceState(requestMock, {
1313
requestWorkspaceId: 'foo',
14+
isDashboardAdmin: true,
1415
});
1516
expect(getWorkspaceState(requestMock)).toEqual({
1617
requestWorkspaceId: 'foo',
18+
isDashboardAdmin: true,
1719
});
1820
});
1921
});

src/core/server/utils/workspace.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { OpenSearchDashboardsRequest, ensureRawRequest } from '../http/router';
77

88
export interface WorkspaceState {
99
requestWorkspaceId?: string;
10+
isDashboardAdmin?: boolean;
1011
}
1112

1213
/**
@@ -29,8 +30,9 @@ export const updateWorkspaceState = (
2930
};
3031

3132
export const getWorkspaceState = (request: OpenSearchDashboardsRequest): WorkspaceState => {
32-
const { requestWorkspaceId } = ensureRawRequest(request).app as WorkspaceState;
33+
const { requestWorkspaceId, isDashboardAdmin } = ensureRawRequest(request).app as WorkspaceState;
3334
return {
3435
requestWorkspaceId,
36+
isDashboardAdmin,
3537
};
3638
};

src/legacy/server/config/schema.js

+4
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ export default () =>
252252
survey: Joi.object({
253253
url: Joi.any().default('/'),
254254
}),
255+
dashboardAdmin: Joi.object({
256+
groups: Joi.array().items(Joi.string()).default([]),
257+
users: Joi.array().items(Joi.string()).default([]),
258+
}),
255259
futureNavigation: Joi.boolean().default(false),
256260
}).default(),
257261

src/plugins/workspace/server/plugin.test.ts

+66-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { OnPreRoutingHandler } from 'src/core/server';
6+
import { OnPostAuthHandler, OnPreRoutingHandler } from 'src/core/server';
77
import { coreMock, httpServerMock } from '../../../core/server/mocks';
88
import { WorkspacePlugin } from './plugin';
99
import { getWorkspaceState } from '../../../core/server/utils';
10+
import * as utilsExports from './utils';
1011

1112
describe('Workspace server plugin', () => {
1213
it('#setup', async () => {
@@ -67,6 +68,70 @@ describe('Workspace server plugin', () => {
6768
expect(toolKitMock.next).toBeCalledTimes(1);
6869
});
6970

71+
describe('#setupPermission', () => {
72+
const setupMock = coreMock.createSetup();
73+
const initializerContextConfigMock = coreMock.createPluginInitializerContext({
74+
enabled: true,
75+
permission: {
76+
enabled: true,
77+
},
78+
});
79+
let registerOnPostAuthFn: OnPostAuthHandler = () => httpServerMock.createResponseFactory().ok();
80+
setupMock.http.registerOnPostAuth.mockImplementation((fn) => {
81+
registerOnPostAuthFn = fn;
82+
return fn;
83+
});
84+
const workspacePlugin = new WorkspacePlugin(initializerContextConfigMock);
85+
const requestWithWorkspaceInUrl = httpServerMock.createOpenSearchDashboardsRequest({
86+
path: '/w/foo/app',
87+
});
88+
89+
it('catch error', async () => {
90+
await workspacePlugin.setup(setupMock);
91+
const toolKitMock = httpServerMock.createToolkit();
92+
93+
await registerOnPostAuthFn(
94+
requestWithWorkspaceInUrl,
95+
httpServerMock.createResponseFactory(),
96+
toolKitMock
97+
);
98+
expect(toolKitMock.next).toBeCalledTimes(1);
99+
});
100+
101+
it('with yml config', async () => {
102+
jest
103+
.spyOn(utilsExports, 'getPrincipalsFromRequest')
104+
.mockImplementation(() => ({ users: [`user1`] }));
105+
jest
106+
.spyOn(utilsExports, 'getOSDAdminConfigFromYMLConfig')
107+
.mockResolvedValue([['group1'], ['user1']]);
108+
109+
await workspacePlugin.setup(setupMock);
110+
const toolKitMock = httpServerMock.createToolkit();
111+
112+
await registerOnPostAuthFn(
113+
requestWithWorkspaceInUrl,
114+
httpServerMock.createResponseFactory(),
115+
toolKitMock
116+
);
117+
expect(toolKitMock.next).toBeCalledTimes(1);
118+
});
119+
120+
it('uninstall security plugin', async () => {
121+
jest.spyOn(utilsExports, 'getPrincipalsFromRequest').mockImplementation(() => ({}));
122+
123+
await workspacePlugin.setup(setupMock);
124+
const toolKitMock = httpServerMock.createToolkit();
125+
126+
await registerOnPostAuthFn(
127+
requestWithWorkspaceInUrl,
128+
httpServerMock.createResponseFactory(),
129+
toolKitMock
130+
);
131+
expect(toolKitMock.next).toBeCalledTimes(1);
132+
});
133+
});
134+
70135
it('#start', async () => {
71136
const setupMock = coreMock.createSetup();
72137
const startMock = coreMock.createStart();

src/plugins/workspace/server/plugin.ts

+32-13
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
SavedObjectsPermissionControl,
3838
SavedObjectsPermissionControlContract,
3939
} from './permission_control/client';
40+
import { getOSDAdminConfigFromYMLConfig, updateDashboardAdminStateForRequest } from './utils';
4041
import { WorkspaceIdConsumerWrapper } from './saved_objects/workspace_id_consumer_wrapper';
4142
import { WorkspaceUiSettingsClientWrapper } from './saved_objects/workspace_ui_settings_client_wrapper';
4243

@@ -71,6 +72,36 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
7172
});
7273
}
7374

75+
private setupPermission(core: CoreSetup) {
76+
this.permissionControl = new SavedObjectsPermissionControl(this.logger);
77+
78+
core.http.registerOnPostAuth(async (request, response, toolkit) => {
79+
let groups: string[];
80+
let users: string[];
81+
82+
// 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.
83+
try {
84+
({ groups = [], users = [] } = this.permissionControl!.getPrincipalsFromRequest(request));
85+
} catch (e) {
86+
return toolkit.next();
87+
}
88+
89+
const [configGroups, configUsers] = await getOSDAdminConfigFromYMLConfig(this.globalConfig$);
90+
updateDashboardAdminStateForRequest(request, groups, users, configGroups, configUsers);
91+
return toolkit.next();
92+
});
93+
94+
this.workspaceSavedObjectsClientWrapper = new WorkspaceSavedObjectsClientWrapper(
95+
this.permissionControl
96+
);
97+
98+
core.savedObjects.addClientWrapper(
99+
PRIORITY_FOR_PERMISSION_CONTROL_WRAPPER,
100+
WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID,
101+
this.workspaceSavedObjectsClientWrapper.wrapperFactory
102+
);
103+
}
104+
74105
constructor(initializerContext: PluginInitializerContext) {
75106
this.logger = initializerContext.logger.get();
76107
this.globalConfig$ = initializerContext.config.legacy.globalConfig$;
@@ -110,19 +141,7 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
110141

111142
const maxImportExportSize = core.savedObjects.getImportExportObjectLimit();
112143
this.logger.info('Workspace permission control enabled:' + isPermissionControlEnabled);
113-
if (isPermissionControlEnabled) {
114-
this.permissionControl = new SavedObjectsPermissionControl(this.logger);
115-
116-
this.workspaceSavedObjectsClientWrapper = new WorkspaceSavedObjectsClientWrapper(
117-
this.permissionControl
118-
);
119-
120-
core.savedObjects.addClientWrapper(
121-
PRIORITY_FOR_PERMISSION_CONTROL_WRAPPER,
122-
WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID,
123-
this.workspaceSavedObjectsClientWrapper.wrapperFactory
124-
);
125-
}
144+
if (isPermissionControlEnabled) this.setupPermission(core);
126145

127146
registerRoutes({
128147
http: core.http,

0 commit comments

Comments
 (0)