Skip to content

Commit

Permalink
[Workspace] Dashboard admin(groups/users) implementation (opensearch-…
Browse files Browse the repository at this point in the history
…project#6554)

* [Workspace] dashboard admin(groups/users) implementation and integrating with dynamic application config

Signed-off-by: yubonluo <yubonluo@amazon.com>

* Modify change log

Signed-off-by: yubonluo <yubonluo@amazon.com>

* optimize the code

Signed-off-by: yubonluo <yubonluo@amazon.com>

* modify change log

Signed-off-by: yubonluo <yubonluo@amazon.com>

* modify change log

Signed-off-by: yubonluo <yubonluo@amazon.com>

* solve change log issue

Signed-off-by: yubonluo <yubonluo@amazon.com>

* Changeset file for PR opensearch-project#6554 created/updated

* [Workspace] delete useless code

Signed-off-by: yubonluo <yubonluo@amazon.com>

* Changeset file for PR opensearch-project#6554 created/updated

* delete useless code

Signed-off-by: yubonluo <yubonluo@amazon.com>

* Optimize the code

Signed-off-by: yubonluo <yubonluo@amazon.com>

* Add unit test to cover setupPermission in plugin.

Signed-off-by: yubonluo <yubonluo@amazon.com>

* delete the logic of dynamic application config

Signed-off-by: yubonluo <yubonluo@amazon.com>

* Default to OSD admin if security uninstall

Signed-off-by: yubonluo <yubonluo@amazon.com>

* Default to OSD admin if security uninstall

Signed-off-by: yubonluo <yubonluo@amazon.com>

---------

Signed-off-by: yubonluo <yubonluo@amazon.com>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
  • Loading branch information
2 people authored and LDrago27 committed Jun 3, 2024
1 parent c7e7d2b commit acfbdd3
Show file tree
Hide file tree
Showing 16 changed files with 552 additions and 19 deletions.
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
45 changes: 32 additions & 13 deletions src/plugins/workspace/server/plugin.ts
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,36 @@ 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();
}

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 +141,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

0 comments on commit acfbdd3

Please sign in to comment.