Skip to content

Commit

Permalink
dashboard admin(groups/users) implementation and integrating with dyn…
Browse files Browse the repository at this point in the history
…amic application config (#303)

* dashboard admin(groups/users) implementation and add unit/integration test

This reverts commit 47e10e4.

* Add test cases of user Id matching dashboard admin

* delete useless code

* change isDashboard function name to isRequestByDashboardAdmin

* dashboard admin config integrating with dynamic application config

Signed-off-by: yubonluo <[email protected]>

* Optimize the code

Signed-off-by: yubonluo <[email protected]>

* fix test error

Signed-off-by: yubonluo <[email protected]>

* delete useless code

Signed-off-by: yubonluo <[email protected]>

* optimize code

Signed-off-by: yubonluo <[email protected]>

* optimize code and add unit test

Signed-off-by: yubonluo <[email protected]>

* optimize code according to comments

Signed-off-by: yubonluo <[email protected]>

* change dashboardAdmin yml config to openseachDashboard

Signed-off-by: yubonluo <[email protected]>

* add missed code

Signed-off-by: yubonluo <[email protected]>

* optimize code

Signed-off-by: yubonluo <[email protected]>

* delete useless code

Signed-off-by: yubonluo <[email protected]>

* delete useless code

Signed-off-by: yubonluo <[email protected]>

* optimize code

Signed-off-by: yubonluo <[email protected]>

* delete useless code

Signed-off-by: yubonluo <[email protected]>

* add utils  unit tests

Signed-off-by: yubonluo <[email protected]>

* optimize code

Signed-off-by: yubonluo <[email protected]>

* Fix the wrong reference

Signed-off-by: yubonluo <[email protected]>

---------

Signed-off-by: yubonluo <[email protected]>
  • Loading branch information
yubonluo authored Apr 12, 2024
1 parent f792a32 commit dffd70b
Show file tree
Hide file tree
Showing 17 changed files with 639 additions and 25 deletions.
5 changes: 5 additions & 0 deletions config/opensearch_dashboards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,8 @@

# Set the value to true to enable workspace feature
# workspace.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: [] },
},
opensearch: {
shardTimeout: duration('30s'),
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: [],
}),
}),
}),
deprecations,
};
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: [] },
},
opensearch: {
shardTimeout: duration(30, 's'),
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',
] as const,
opensearch: ['shardTimeout', 'requestTimeout', 'pingTimeout'] as const,
path: ['data'] 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 @@ -251,6 +251,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([]),
}),
}).default(),

savedObjects: HANDLED_IN_NEW_PLATFORM,
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/workspace/opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"requiredPlugins": [
"savedObjects"
],
"optionalPlugins": ["savedObjectsManagement"],
"optionalPlugins": ["savedObjectsManagement", "applicationConfig"],
"requiredBundles": ["opensearchDashboardsReact"]
}
14 changes: 11 additions & 3 deletions src/plugins/workspace/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@
import { OnPreRoutingHandler } from 'src/core/server';
import { coreMock, httpServerMock } from '../../../core/server/mocks';
import { WorkspacePlugin } from './plugin';
import { AppPluginSetupDependencies } from './types';
import { getWorkspaceState } from '../../../core/server/utils';

describe('Workspace server plugin', () => {
const mockApplicationConfig = {
getConfigurationClient: jest.fn().mockResolvedValue({}),
registerConfigurationClient: jest.fn().mockResolvedValue({}),
};
const mockDependencies: AppPluginSetupDependencies = {
applicationConfig: mockApplicationConfig,
};
it('#setup', async () => {
let value;
const setupMock = coreMock.createSetup();
Expand All @@ -17,7 +25,7 @@ describe('Workspace server plugin', () => {
});
setupMock.capabilities.registerProvider.mockImplementationOnce((fn) => (value = fn()));
const workspacePlugin = new WorkspacePlugin(initializerContextConfigMock);
await workspacePlugin.setup(setupMock);
await workspacePlugin.setup(setupMock, mockDependencies);
expect(value).toMatchInlineSnapshot(`
Object {
"workspaces": Object {
Expand All @@ -43,7 +51,7 @@ describe('Workspace server plugin', () => {
return fn;
});
const workspacePlugin = new WorkspacePlugin(initializerContextConfigMock);
await workspacePlugin.setup(setupMock);
await workspacePlugin.setup(setupMock, mockDependencies);
const toolKitMock = httpServerMock.createToolkit();

const requestWithWorkspaceInUrl = httpServerMock.createOpenSearchDashboardsRequest({
Expand Down Expand Up @@ -78,7 +86,7 @@ describe('Workspace server plugin', () => {
});

const workspacePlugin = new WorkspacePlugin(initializerContextConfigMock);
await workspacePlugin.setup(setupMock);
await workspacePlugin.setup(setupMock, mockDependencies);
await workspacePlugin.start(startMock);
expect(startMock.savedObjects.createSerializer).toBeCalledTimes(1);
});
Expand Down
67 changes: 52 additions & 15 deletions src/plugins/workspace/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import {
WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID,
WORKSPACE_ID_CONSUMER_WRAPPER_ID,
} from '../common/constants';
import { IWorkspaceClientImpl, WorkspacePluginSetup, WorkspacePluginStart } from './types';
import {
IWorkspaceClientImpl,
WorkspacePluginSetup,
WorkspacePluginStart,
AppPluginSetupDependencies,
} from './types';
import { WorkspaceClient } from './workspace_client';
import { registerRoutes } from './routes';
import { WorkspaceSavedObjectsClientWrapper } from './saved_objects';
Expand All @@ -32,6 +37,11 @@ import {
SavedObjectsPermissionControl,
SavedObjectsPermissionControlContract,
} from './permission_control/client';
import {
getApplicationOSDAdminConfig,
getOSDAdminConfig,
updateDashboardAdminStateForRequest,
} from './utils';
import { WorkspaceIdConsumerWrapper } from './saved_objects/workspace_id_consumer_wrapper';

export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePluginStart> {
Expand Down Expand Up @@ -64,12 +74,51 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
});
}

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

core.http.registerOnPostAuth(async (request, response, toolkit) => {
let groups: string[];
let users: string[];
let configGroups: string[];
let configUsers: 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 (!!applicationConfig) {
[configGroups, configUsers] = await getApplicationOSDAdminConfig(
{ applicationConfig },
request
);
} else {
[configGroups, configUsers] = await getOSDAdminConfig(this.globalConfig$);
}
updateDashboardAdminStateForRequest(request, groups, users, configGroups, configUsers);
return toolkit.next();
});

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

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

constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get('plugins', 'workspace');
this.globalConfig$ = initializerContext.config.legacy.globalConfig$;
}

public async setup(core: CoreSetup) {
public async setup(core: CoreSetup, { applicationConfig }: AppPluginSetupDependencies) {
this.logger.debug('Setting up Workspaces service');
const globalConfig = await this.globalConfig$.pipe(first()).toPromise();
const isPermissionControlEnabled = globalConfig.savedObjects.permission.enabled === true;
Expand All @@ -95,19 +144,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(
0,
WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID,
this.workspaceSavedObjectsClientWrapper.wrapperFactory
);
}
if (isPermissionControlEnabled) this.setupPermission(core, { applicationConfig });

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

0 comments on commit dffd70b

Please sign in to comment.