Skip to content

Commit

Permalink
Add data source permission wrapper
Browse files Browse the repository at this point in the history
Signed-off-by: yubonluo <[email protected]>
  • Loading branch information
yubonluo committed Sep 2, 2024
1 parent b370871 commit d3681c0
Show file tree
Hide file tree
Showing 11 changed files with 635 additions and 49 deletions.
4 changes: 4 additions & 0 deletions config/opensearch_dashboards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,10 @@
# "all": The data source can be managed by all users. Default to "all".
# data_source_management.manageableBy: "all"

# Set the backend roles in groups, whoever has the backend roles defined in this config will be regard as dataSourceAdmin.
# DataSource Admin will have the access to all the data source saved objects inside OpenSearch Dashboards by api.
# data_source_management.dataSourceAdmin: []

# Set the value of this setting to false to hide the help menu link to the OpenSearch Dashboards user survey
# opensearchDashboards.survey.url: "https://survey.opensearch.org"

Expand Down
24 changes: 0 additions & 24 deletions src/core/server/saved_objects/service/saved_objects_client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,30 +246,6 @@ test(`#deleteFromWorkspaces Should use update if there is existing workspaces`,
});
});

test(`#deleteFromWorkspaces Should use overwrite create if there is no existing workspaces`, async () => {
const returnValue = Symbol();
const create = jest.fn();
const mockRepository = {
get: jest.fn().mockResolvedValue({
workspaces: [],
}),
update: jest.fn().mockResolvedValue(returnValue),
create,
};
const client = new SavedObjectsClient(mockRepository);

const type = Symbol();
const id = Symbol();
const workspaces = ['id1'];
await client.deleteFromWorkspaces(type, id, workspaces);
expect(mockRepository.get).toHaveBeenCalledWith(type, id, {});
expect(mockRepository.create).toHaveBeenCalledWith(
type,
{},
{ id, overwrite: true, permissions: undefined, version: undefined }
);
});

test(`#deleteFromWorkspaces should throw error if no workspaces passed`, () => {
const mockRepository = {};
const client = new SavedObjectsClient(mockRepository);
Expand Down
27 changes: 5 additions & 22 deletions src/core/server/saved_objects/service/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,28 +488,11 @@ export class SavedObjectsClient {
const newWorkspaces = existingWorkspaces.filter((item) => {
return targetWorkspaces.indexOf(item) === -1;
});
if (newWorkspaces.length > 0) {
return await this.update<T>(type, id, object.attributes, {
...options,
workspaces: newWorkspaces,
version: object.version,
});
} else {
// If there is no workspaces assigned, will create object with overwrite to delete workspace property.
return await this.create(
type,
{
...object.attributes,
},
{
...options,
id,
permissions: object.permissions,
overwrite: true,
version: object.version,
}
);
}
return await this.update<T>(type, id, object.attributes, {
...options,
workspaces: newWorkspaces,
version: object.version,
});
};

/**
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 @@ -12,10 +12,12 @@ describe('updateWorkspaceState', () => {
updateWorkspaceState(requestMock, {
requestWorkspaceId: 'foo',
isDashboardAdmin: true,
isDataSourceAdmin: true,
});
expect(getWorkspaceState(requestMock)).toEqual({
requestWorkspaceId: 'foo',
isDashboardAdmin: true,
isDataSourceAdmin: true,
});
});
});
5 changes: 4 additions & 1 deletion src/core/server/utils/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { OpenSearchDashboardsRequest, ensureRawRequest } from '../http/router';
export interface WorkspaceState {
requestWorkspaceId?: string;
isDashboardAdmin?: boolean;
isDataSourceAdmin?: boolean;
}

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

export const getWorkspaceState = (request: OpenSearchDashboardsRequest): WorkspaceState => {
const { requestWorkspaceId, isDashboardAdmin } = ensureRawRequest(request).app as WorkspaceState;
const { requestWorkspaceId, isDashboardAdmin, isDataSourceAdmin } = ensureRawRequest(request)
.app as WorkspaceState;
return {
requestWorkspaceId,
isDashboardAdmin,
isDataSourceAdmin,
};
};
3 changes: 3 additions & 0 deletions src/plugins/data_source_management/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ export const PLUGIN_ID = 'dataSourceManagement';
export const PLUGIN_NAME = 'Data sources';
export const DEFAULT_DATA_SOURCE_UI_SETTINGS_ID = 'defaultDataSource';
export * from './types';
export const DATA_SOURCE_SAVED_OBJECT_TYPE = 'data-source';
export const DATA_SOURCE_PERMISSION_CLIENT_WRAPPER_ID = 'data-source-permission';
export const ORDER_FOR_DATA_SOURCE_PERMISSION_WRAPPER = 50;
3 changes: 3 additions & 0 deletions src/plugins/data_source_management/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export const configSchema = schema.object({
[schema.literal('all'), schema.literal('dashboard_admin'), schema.literal('none')],
{ defaultValue: 'all' }
),
dataSourceAdmin: schema.arrayOf(schema.string(), {
defaultValue: [],
}),
});

export type ConfigSchema = TypeOf<typeof configSchema>;
41 changes: 39 additions & 2 deletions src/plugins/data_source_management/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ import { DataSourceManagementPluginSetup, DataSourceManagementPluginStart } from
import { OpenSearchDataSourceManagementPlugin } from './adaptors/opensearch_data_source_management_plugin';
import { PPLPlugin } from './adaptors/ppl_plugin';
import { ConfigSchema } from '../config';
import { getWorkspaceState } from '../../../../src/core/server/utils';
import { ManageableBy } from '../common';
import { getWorkspaceState, updateWorkspaceState } from '../../../../src/core/server/utils';
import {
DATA_SOURCE_PERMISSION_CLIENT_WRAPPER_ID,
ManageableBy,
ORDER_FOR_DATA_SOURCE_PERMISSION_WRAPPER,
} from '../common';
import { DataSourcePermissionClientWrapper } from './saved_objects/data_source_premission_client_wrapper';
import { getPrincipalsFromRequest } from './utils';

export interface DataSourceManagementPluginDependencies {
dataSource: DataSourcePluginSetup;
Expand All @@ -33,6 +39,35 @@ export class DataSourceManagementPlugin
private readonly config$: Observable<ConfigSchema>;
private readonly logger: Logger;

private setupDataSourcePermission(core: CoreSetup, config: ConfigSchema) {
core.http.registerOnPostAuth(async (request, response, toolkit) => {
let groups: string[];
const [coreStart] = await core.getStartServices();

try {
({ groups = [] } = getPrincipalsFromRequest(request, coreStart.http.auth));
} catch (e) {
return toolkit.next();
}

const configGroups = config.dataSourceAdmin;
const isDataSourceAdmin = configGroups.some((configGroup) => groups.includes(configGroup));
updateWorkspaceState(request, {
isDataSourceAdmin,
});
return toolkit.next();
});

const dataSourcePermissionWrapper = new DataSourcePermissionClientWrapper(config.manageableBy);

// Add data source permission client wrapper factory
core.savedObjects.addClientWrapper(
ORDER_FOR_DATA_SOURCE_PERMISSION_WRAPPER,
DATA_SOURCE_PERMISSION_CLIENT_WRAPPER_ID,
dataSourcePermissionWrapper.wrapperFactory
);
}

constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
this.config$ = initializerContext.config.create<ConfigSchema>();
Expand Down Expand Up @@ -82,6 +117,8 @@ export class DataSourceManagementPlugin
if (dataSourceEnabled) {
dataSource.registerCustomApiSchema(PPLPlugin);
dataSource.registerCustomApiSchema(OpenSearchDataSourceManagementPlugin);

this.setupDataSourcePermission(core, config);
}
// @ts-ignore
core.http.registerRouteHandlerContext(
Expand Down
Loading

0 comments on commit d3681c0

Please sign in to comment.