Skip to content

Commit

Permalink
Merge branch 'main' into fix-management-section-id-not-match-capabili…
Browse files Browse the repository at this point in the history
…ties
  • Loading branch information
ruanyl committed May 27, 2024
2 parents 6e8d9ad + 8d50974 commit 374a6dc
Show file tree
Hide file tree
Showing 21 changed files with 610 additions and 21 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))
2 changes: 2 additions & 0 deletions changelogs/fragments/6777.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fix:
- Error message is not formatted in vis_type_vega url parser. ([#6777](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6777))
2 changes: 2 additions & 0 deletions changelogs/fragments/6782.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fix:
- Quickrange selection fix ([#6782](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6782))
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
31 changes: 31 additions & 0 deletions src/plugins/data/common/data_frames/data_frame_utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { TimeRange } from '../types';
import { formatTimePickerDate } from './utils';

describe('Data Frame Utils', () => {
describe('formatTimePickerDate function', () => {
Date.now = jest.fn(() => new Date('2024-05-04T12:30:00.000Z'));

test('should return a correctly formatted date', () => {
const range = { from: 'now-15m', to: 'now' } as TimeRange;
const formattedDate = formatTimePickerDate(range, 'YYYY-MM-DD HH:mm:ss.SSS');
expect(formattedDate).toStrictEqual({
fromDate: '2024-05-04 12:15:00.000',
toDate: '2024-05-04 12:30:00.000',
});
});

test('should indicate invalid when given bad dates', () => {
const range = { from: 'fake', to: 'date' } as TimeRange;
const formattedDate = formatTimePickerDate(range, 'YYYY-MM-DD HH:mm:ss.SSS');
expect(formattedDate).toStrictEqual({
fromDate: 'Invalid date',
toDate: 'Invalid date',
});
});
});
});
23 changes: 22 additions & 1 deletion src/plugins/data/common/data_frames/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { IFieldType } from './fields';
import { IndexPatternFieldMap, IndexPatternSpec } from '../index_patterns';
import { IOpenSearchDashboardsSearchRequest } from '../search';
import { GetAggTypeFn, GetDataFrameAggQsFn } from '../types';
import { GetAggTypeFn, GetDataFrameAggQsFn, TimeRange } from '../types';

/**
* Returns the raw data frame from the search request.
Expand Down Expand Up @@ -290,6 +290,27 @@ export const getTimeField = (
: fields.find((field) => field.type === 'date');
};

/**
* Parses timepicker datetimes using datemath package. Will attempt to parse strings such as
* "now - 15m"
*
* @param dateRange - of type TimeRange
* @param dateFormat - formatting string (should work with Moment)
* @returns object with `fromDate` and `toDate` strings, both of which will be in utc time and formatted to
* the `dateFormat` parameter
*/
export const formatTimePickerDate = (dateRange: TimeRange, dateFormat: string) => {
const dateMathParse = (date: string) => {
const parsedDate = datemath.parse(date);
return parsedDate ? parsedDate.utc().format(dateFormat) : '';
};

const fromDate = dateMathParse(dateRange.from);
const toDate = dateMathParse(dateRange.to);

return { fromDate, toDate };
};

/**
* Checks if the value is a GeoPoint. Expects an object with lat and lon properties.
*
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/vis_type_vega/public/data_model/vega_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never
i18n.translate('visTypeVega.vegaParser.notSupportedUrlTypeErrorMessage', {
defaultMessage: '{urlObject} is not supported',
values: {
urlObject: 'url: {"%type%": "${type}"}',
urlObject: `url: {"%type%": "${type}"}`,
},
})
);
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 374a6dc

Please sign in to comment.