From e17046adeb65441871832c557fa06a309acb06a6 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 21 Aug 2023 18:11:00 +0800 Subject: [PATCH 1/3] feat: accomplish dashboard_admin Signed-off-by: SuZhou-Joe --- src/plugins/workspace/config.ts | 24 ++++++++ src/plugins/workspace/server/index.ts | 10 +--- src/plugins/workspace/server/plugin.ts | 9 ++- .../workspace_saved_objects_client_wrapper.ts | 58 ++++++++++++++++++- 4 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 src/plugins/workspace/config.ts diff --git a/src/plugins/workspace/config.ts b/src/plugins/workspace/config.ts new file mode 100644 index 000000000000..6fc163b67e45 --- /dev/null +++ b/src/plugins/workspace/config.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema, TypeOf } from '@osd/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + dashboardAdmin: schema.object( + { + backendRoles: schema.arrayOf(schema.string(), { + defaultValue: ['dashboard_admin'], + }), + }, + { + defaultValue: { + backendRoles: ['dashboard_admin'], + }, + } + ), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/workspace/server/index.ts b/src/plugins/workspace/server/index.ts index 4e11dc50dab9..230b53b6f663 100644 --- a/src/plugins/workspace/server/index.ts +++ b/src/plugins/workspace/server/index.ts @@ -2,11 +2,9 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - -import { schema } from '@osd/config-schema'; - import { PluginConfigDescriptor, PluginInitializerContext } from '../../../core/server'; import { WorkspacePlugin } from './plugin'; +import { configSchema } from '../config'; // This exports static code and TypeScript types, // as well as, OpenSearch Dashboards Platform `plugin()` initializer. @@ -15,10 +13,6 @@ export function plugin(initializerContext: PluginInitializerContext) { return new WorkspacePlugin(initializerContext); } -export { MlCommonsPluginSetup, MlCommonsPluginStart } from './types'; - export const config: PluginConfigDescriptor = { - schema: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - }), + schema: configSchema, }; diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index 710eaeea1819..1ee3df0dfa51 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import { i18n } from '@osd/i18n'; +import { Observable } from 'rxjs'; import { PluginInitializerContext, @@ -23,10 +24,12 @@ import { IWorkspaceDBImpl, WorkspaceAttribute } from './types'; import { WorkspaceClientWithSavedObject } from './workspace_client'; import { WorkspaceSavedObjectsClientWrapper } from './saved_objects'; import { registerRoutes } from './routes'; +import { ConfigSchema } from '../config'; export class WorkspacePlugin implements Plugin<{}, {}> { private readonly logger: Logger; private client?: IWorkspaceDBImpl; + private config$: Observable; private proxyWorkspaceTrafficToRealHandler(setupDeps: CoreSetup) { /** @@ -47,6 +50,7 @@ export class WorkspacePlugin implements Plugin<{}, {}> { constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('plugins', 'workspace'); + this.config$ = initializerContext.config.create(); } public async setup(core: CoreSetup) { @@ -56,7 +60,10 @@ export class WorkspacePlugin implements Plugin<{}, {}> { await this.client.setup(core); const workspaceSavedObjectsClientWrapper = new WorkspaceSavedObjectsClientWrapper( - core.savedObjects.permissionControl + core.savedObjects.permissionControl, + { + config$: this.config$, + } ); core.savedObjects.addClientWrapper( diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index 5b2b45b353d7..658a03b689d5 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -5,6 +5,8 @@ import { i18n } from '@osd/i18n'; import Boom from '@hapi/boom'; +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; import { OpenSearchDashboardsRequest, @@ -29,6 +31,7 @@ import { ACL, WorkspacePermissionMode, } from '../../../../core/server'; +import { ConfigSchema } from '../../config'; // Can't throw unauthorized for now, the page will be refreshed if unauthorized const generateWorkspacePermissionError = () => @@ -140,6 +143,14 @@ export class WorkspaceSavedObjectsClientWrapper { } } + private async isDashboardAdmin(request: OpenSearchDashboardsRequest): Promise { + const config: ConfigSchema = await this.options.config$.pipe(first()).toPromise(); + const principals = this.permissionControl.getPrincipalsFromRequest(request); + const adminBackendRoles = config?.dashboardAdmin?.backendRoles || []; + const matchAny = principals?.groups?.some((item) => adminBackendRoles.includes(item)) || false; + return matchAny; + } + /** * check if the type include workspace * Workspace permission check is totally different from object permission check. @@ -156,6 +167,10 @@ export class WorkspaceSavedObjectsClientWrapper { id: string, options: SavedObjectsDeleteOptions = {} ) => { + const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); + if (isDashboardAdmin) { + return wrapperOptions.client.delete(type, id, options); + } if (this.isRelatedToWorkspace(type)) { await this.validateSingleWorkspacePermissions(id, wrapperOptions.request, [ WorkspacePermissionMode.Management, @@ -177,6 +192,10 @@ export class WorkspaceSavedObjectsClientWrapper { attributes: Partial, options: SavedObjectsUpdateOptions = {} ): Promise> => { + const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); + if (isDashboardAdmin) { + return wrapperOptions.client.update(type, id, attributes, options); + } if (this.isRelatedToWorkspace(type)) { await this.validateSingleWorkspacePermissions(id, wrapperOptions.request, [ WorkspacePermissionMode.Management, @@ -189,6 +208,10 @@ export class WorkspaceSavedObjectsClientWrapper { objects: Array>, options?: SavedObjectsBulkUpdateOptions ): Promise> => { + const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); + if (isDashboardAdmin) { + return wrapperOptions.client.bulkUpdate(objects, options); + } const workspaceIds = objects.reduce((acc, cur) => { if (this.isRelatedToWorkspace(cur.type)) { acc.push(cur.id); @@ -211,6 +234,10 @@ export class WorkspaceSavedObjectsClientWrapper { objects: Array>, options: SavedObjectsCreateOptions = {} ): Promise> => { + const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); + if (isDashboardAdmin) { + return wrapperOptions.client.bulkCreate(objects, options); + } if (options.workspaces) { await this.validateMultiWorkspacesPermissions(options.workspaces, wrapperOptions.request, [ WorkspacePermissionMode.Write, @@ -225,6 +252,10 @@ export class WorkspaceSavedObjectsClientWrapper { attributes: T, options?: SavedObjectsCreateOptions ) => { + const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); + if (isDashboardAdmin) { + return wrapperOptions.client.create(type, attributes, options); + } if (isWorkspacesLikeAttributes(attributes)) { await this.validateMultiWorkspacesPermissions( attributes.workspaces, @@ -240,6 +271,10 @@ export class WorkspaceSavedObjectsClientWrapper { id: string, options: SavedObjectsBaseOptions = {} ): Promise> => { + const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); + if (isDashboardAdmin) { + return wrapperOptions.client.get(type, id, options); + } const objectToGet = await wrapperOptions.client.get(type, id, options); await this.validateAtLeastOnePermittedWorkspaces( objectToGet.workspaces, @@ -253,6 +288,10 @@ export class WorkspaceSavedObjectsClientWrapper { objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ): Promise> => { + const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); + if (isDashboardAdmin) { + return wrapperOptions.client.bulkGet(objects, options); + } const objectToBulkGet = await wrapperOptions.client.bulkGet(objects, options); for (const object of objectToBulkGet.saved_objects) { await this.validateAtLeastOnePermittedWorkspaces( @@ -267,9 +306,15 @@ export class WorkspaceSavedObjectsClientWrapper { const findWithWorkspacePermissionControl = async ( options: SavedObjectsFindOptions ) => { + const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); const principals = this.permissionControl.getPrincipalsFromRequest(wrapperOptions.request); - if (this.isRelatedToWorkspace(options.type)) { + if (isDashboardAdmin) { + /** + * For dashbaord admin, we will fetch all the records no matter + * what the ACL is or if there is workspaces attribute. + */ + } else if (this.isRelatedToWorkspace(options.type)) { const queryDSLForQueryingWorkspaces = ACL.genereateGetPermittedSavedObjectsQueryDSL( [ WorkspacePermissionMode.LibraryRead, @@ -355,6 +400,10 @@ export class WorkspaceSavedObjectsClientWrapper { targetWorkspaces: string[], options: SavedObjectsAddToWorkspacesOptions = {} ) => { + const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); + if (isDashboardAdmin) { + return wrapperOptions.client.addToWorkspaces(objects, targetWorkspaces, options); + } // target workspaces await this.validateMultiWorkspacesPermissions(targetWorkspaces, wrapperOptions.request, [ WorkspacePermissionMode.LibraryWrite, @@ -395,5 +444,10 @@ export class WorkspaceSavedObjectsClientWrapper { }; }; - constructor(private readonly permissionControl: SavedObjectsPermissionControlContract) {} + constructor( + private readonly permissionControl: SavedObjectsPermissionControlContract, + private readonly options: { + config$: Observable; + } + ) {} } From bb451eee7442313e790b1cfa0dbc6c337f3c7284 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 21 Aug 2023 18:14:04 +0800 Subject: [PATCH 2/3] feat: add yml default config and comment Signed-off-by: SuZhou-Joe --- config/opensearch_dashboards.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 73f31233a783..8c43da0d2004 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -268,3 +268,6 @@ # Set the value of this setting to true to enable plugin augmentation on Dashboard # vis_augmenter.pluginAugmentationEnabled: true +# Set the backend roles, whoever has the backend roles defined in this config will be regard as dashboard admin. +# Dashboard admin will have the access to all the workspaces and objects inside OpenSearch Dashboards. +# workspace.dashboardAdmin.backendRoles: ["dashboard_admin"] \ No newline at end of file From 99d936e4c568e688d53d932c49d5d65ac75da225 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 21 Aug 2023 18:29:22 +0800 Subject: [PATCH 3/3] feat: update Signed-off-by: SuZhou-Joe --- .../workspace_saved_objects_client_wrapper.ts | 63 +++++++------------ 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index 658a03b689d5..6998428de1b4 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -59,6 +59,7 @@ const isWorkspacesLikeAttributes = (attributes: unknown): attributes is Attribut Array.isArray((attributes as { workspaces: unknown }).workspaces); export class WorkspaceSavedObjectsClientWrapper { + private config?: ConfigSchema; private formatWorkspacePermissionModeToStringArray( permission: WorkspacePermissionMode | WorkspacePermissionMode[] ): string[] { @@ -143,8 +144,8 @@ export class WorkspaceSavedObjectsClientWrapper { } } - private async isDashboardAdmin(request: OpenSearchDashboardsRequest): Promise { - const config: ConfigSchema = await this.options.config$.pipe(first()).toPromise(); + private isDashboardAdmin(request: OpenSearchDashboardsRequest): boolean { + const config = this.config || ({} as ConfigSchema); const principals = this.permissionControl.getPrincipalsFromRequest(request); const adminBackendRoles = config?.dashboardAdmin?.backendRoles || []; const matchAny = principals?.groups?.some((item) => adminBackendRoles.includes(item)) || false; @@ -167,10 +168,6 @@ export class WorkspaceSavedObjectsClientWrapper { id: string, options: SavedObjectsDeleteOptions = {} ) => { - const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); - if (isDashboardAdmin) { - return wrapperOptions.client.delete(type, id, options); - } if (this.isRelatedToWorkspace(type)) { await this.validateSingleWorkspacePermissions(id, wrapperOptions.request, [ WorkspacePermissionMode.Management, @@ -192,10 +189,6 @@ export class WorkspaceSavedObjectsClientWrapper { attributes: Partial, options: SavedObjectsUpdateOptions = {} ): Promise> => { - const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); - if (isDashboardAdmin) { - return wrapperOptions.client.update(type, id, attributes, options); - } if (this.isRelatedToWorkspace(type)) { await this.validateSingleWorkspacePermissions(id, wrapperOptions.request, [ WorkspacePermissionMode.Management, @@ -208,10 +201,6 @@ export class WorkspaceSavedObjectsClientWrapper { objects: Array>, options?: SavedObjectsBulkUpdateOptions ): Promise> => { - const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); - if (isDashboardAdmin) { - return wrapperOptions.client.bulkUpdate(objects, options); - } const workspaceIds = objects.reduce((acc, cur) => { if (this.isRelatedToWorkspace(cur.type)) { acc.push(cur.id); @@ -234,10 +223,6 @@ export class WorkspaceSavedObjectsClientWrapper { objects: Array>, options: SavedObjectsCreateOptions = {} ): Promise> => { - const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); - if (isDashboardAdmin) { - return wrapperOptions.client.bulkCreate(objects, options); - } if (options.workspaces) { await this.validateMultiWorkspacesPermissions(options.workspaces, wrapperOptions.request, [ WorkspacePermissionMode.Write, @@ -252,10 +237,6 @@ export class WorkspaceSavedObjectsClientWrapper { attributes: T, options?: SavedObjectsCreateOptions ) => { - const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); - if (isDashboardAdmin) { - return wrapperOptions.client.create(type, attributes, options); - } if (isWorkspacesLikeAttributes(attributes)) { await this.validateMultiWorkspacesPermissions( attributes.workspaces, @@ -271,10 +252,6 @@ export class WorkspaceSavedObjectsClientWrapper { id: string, options: SavedObjectsBaseOptions = {} ): Promise> => { - const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); - if (isDashboardAdmin) { - return wrapperOptions.client.get(type, id, options); - } const objectToGet = await wrapperOptions.client.get(type, id, options); await this.validateAtLeastOnePermittedWorkspaces( objectToGet.workspaces, @@ -288,10 +265,6 @@ export class WorkspaceSavedObjectsClientWrapper { objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ): Promise> => { - const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); - if (isDashboardAdmin) { - return wrapperOptions.client.bulkGet(objects, options); - } const objectToBulkGet = await wrapperOptions.client.bulkGet(objects, options); for (const object of objectToBulkGet.saved_objects) { await this.validateAtLeastOnePermittedWorkspaces( @@ -306,15 +279,9 @@ export class WorkspaceSavedObjectsClientWrapper { const findWithWorkspacePermissionControl = async ( options: SavedObjectsFindOptions ) => { - const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); const principals = this.permissionControl.getPrincipalsFromRequest(wrapperOptions.request); - if (isDashboardAdmin) { - /** - * For dashbaord admin, we will fetch all the records no matter - * what the ACL is or if there is workspaces attribute. - */ - } else if (this.isRelatedToWorkspace(options.type)) { + if (this.isRelatedToWorkspace(options.type)) { const queryDSLForQueryingWorkspaces = ACL.genereateGetPermittedSavedObjectsQueryDSL( [ WorkspacePermissionMode.LibraryRead, @@ -400,10 +367,6 @@ export class WorkspaceSavedObjectsClientWrapper { targetWorkspaces: string[], options: SavedObjectsAddToWorkspacesOptions = {} ) => { - const isDashboardAdmin = await this.isDashboardAdmin(wrapperOptions.request); - if (isDashboardAdmin) { - return wrapperOptions.client.addToWorkspaces(objects, targetWorkspaces, options); - } // target workspaces await this.validateMultiWorkspacesPermissions(targetWorkspaces, wrapperOptions.request, [ WorkspacePermissionMode.LibraryWrite, @@ -426,6 +389,12 @@ export class WorkspaceSavedObjectsClientWrapper { return await wrapperOptions.client.addToWorkspaces(objects, targetWorkspaces, options); }; + const isDashboardAdmin = this.isDashboardAdmin(wrapperOptions.request); + + if (isDashboardAdmin) { + return wrapperOptions.client; + } + return { ...wrapperOptions.client, get: getWithWorkspacePermissionControl, @@ -449,5 +418,15 @@ export class WorkspaceSavedObjectsClientWrapper { private readonly options: { config$: Observable; } - ) {} + ) { + this.options.config$.subscribe((config) => { + this.config = config; + }); + this.options.config$ + .pipe(first()) + .toPromise() + .then((config) => { + this.config = config; + }); + } }