diff --git a/changelogs/fragments/7214.yml b/changelogs/fragments/7214.yml new file mode 100644 index 000000000000..7369e06aa40a --- /dev/null +++ b/changelogs/fragments/7214.yml @@ -0,0 +1,2 @@ +feat: +- [DataSource] Restrict to edit data source on the DSM UI. ([#7214](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7214)) \ No newline at end of file diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 61f7522b5dae..d79c0986de07 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -321,6 +321,12 @@ # AWSSigV4: # enabled: true +# Optional setting that controls the permissions of data source to create, update and delete. +# "none": The data source is readonly for all users. +# "dashboard_admin": The data source can only be managed by dashboard admin. +# "all": The data source can be managed by all users. Default to "all". +# data_source.manageableBy: "all" + # 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" diff --git a/src/plugins/data_source/common/data_sources/types.ts b/src/plugins/data_source/common/data_sources/types.ts index cde21f648c61..bd147ac00c04 100644 --- a/src/plugins/data_source/common/data_sources/types.ts +++ b/src/plugins/data_source/common/data_sources/types.ts @@ -60,3 +60,9 @@ export enum DataSourceEngineType { Elasticsearch = 'Elasticsearch', NA = 'No Engine Type Available', } + +export enum ManageableBy { + All = 'all', + DashboardAdmin = 'dashboard_admin', + None = 'none', +} diff --git a/src/plugins/data_source/config.ts b/src/plugins/data_source/config.ts index 30824b486257..36c298cde119 100644 --- a/src/plugins/data_source/config.ts +++ b/src/plugins/data_source/config.ts @@ -59,6 +59,10 @@ export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), }), }), + manageableBy: schema.oneOf( + [schema.literal('all'), schema.literal('dashboard_admin'), schema.literal('none')], + { defaultValue: 'all' } + ), }); export type DataSourcePluginConfigType = TypeOf; diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts index bbf5a89d1b53..fa3085a63935 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -33,6 +33,8 @@ import { registerTestConnectionRoute } from './routes/test_connection'; import { registerFetchDataSourceMetaDataRoute } from './routes/fetch_data_source_metadata'; import { AuthenticationMethodRegistry, IAuthenticationMethodRegistry } from './auth_registry'; import { CustomApiSchemaRegistry } from './schema_registry'; +import { ManageableBy } from '../common/data_sources'; +import { getWorkspaceState } from '../../../../src/core/server/utils'; export class DataSourcePlugin implements Plugin { private readonly logger: Logger; @@ -81,6 +83,25 @@ export class DataSourcePlugin implements Plugin ({ + dataSource: { + canManage: false, + }, + })); + + core.capabilities.registerSwitcher((request) => { + const { requestWorkspaceId, isDashboardAdmin } = getWorkspaceState(request); + // User can not manage data source in the workspace. + const canManage = + (manageableBy === ManageableBy.All && !requestWorkspaceId) || + (manageableBy === ManageableBy.DashboardAdmin && + isDashboardAdmin !== false && + !requestWorkspaceId); + + return { dataSource: { canManage } }; + }); + core.logging.configure( this.config$.pipe( map((dataSourceConfig) => ({ diff --git a/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.test.tsx b/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.test.tsx index cbc1f015c6f3..017f450c11ae 100644 --- a/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.test.tsx @@ -41,6 +41,7 @@ describe('DataSourceHomePanel', () => { http: {}, savedObjects: {}, uiSettings: {}, + application: { capabilities: { dataSource: { canManage: true } } }, }, }; diff --git a/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.tsx b/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.tsx index 3cedbb9641c0..219e37e93345 100644 --- a/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.tsx +++ b/src/plugins/data_source_management/public/components/data_source_home_panel/data_source_home_panel.tsx @@ -30,11 +30,17 @@ export const DataSourceHomePanel: React.FC = ({ featureFlagStatus, ...props }) => { - const { setBreadcrumbs, notifications, http, savedObjects, uiSettings } = useOpenSearchDashboards< - DataSourceManagementContext - >().services; + const { + setBreadcrumbs, + notifications, + http, + savedObjects, + uiSettings, + application, + } = useOpenSearchDashboards().services; const [selectedTabId, setSelectedTabId] = useState('manageDirectQueryDataSources'); + const canManageDataSource = !!application.capabilities?.dataSource?.canManage; useEffect(() => { setBreadcrumbs(getListBreadcrumbs()); @@ -80,9 +86,11 @@ export const DataSourceHomePanel: React.FC = ({ - - - + {canManageDataSource ? ( + + + + ) : null} diff --git a/src/plugins/data_source_management/public/components/data_source_table/__snapshots__/data_source_table.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_table/__snapshots__/data_source_table.test.tsx.snap index dabd8d387e19..842c3e3d07ba 100644 --- a/src/plugins/data_source_management/public/components/data_source_table/__snapshots__/data_source_table.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/data_source_table/__snapshots__/data_source_table.test.tsx.snap @@ -2111,3 +2111,1105 @@ exports[`DataSourceTable should get datasources successful should render normall `; + +exports[`DataSourceTable should not manage datasources when canManageDataSource is false should render empty table 1`] = ` + + + +
+ + +
+ +
+ + + No Data Source Connections have been created yet. + + +
+
+ +
+ +
+ + +
+ + + +`; diff --git a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx index b0cb56bdac62..acb4d5d7d853 100644 --- a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.test.tsx @@ -24,7 +24,10 @@ const tableColumnHeaderButtonIdentifier = 'EuiTableHeaderCell .euiTableHeaderBut const emptyStateIdentifier = '[data-test-subj="datasourceTableEmptyState"]'; describe('DataSourceTable', () => { - const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + const mockedContext = { + ...mockManagementPlugin.createDataSourceManagementContext(), + application: { capabilities: { dataSource: { canManage: true } } }, + }; const uiSettings = mockedContext.uiSettings; let component: ReactWrapper, React.Component<{}, {}, any>>; const history = (scopedHistoryMock.create() as unknown) as ScopedHistory; @@ -169,4 +172,36 @@ describe('DataSourceTable', () => { expect(component.find(confirmModalIdentifier).exists()).toBe(false); }); }); + + describe('should not manage datasources when canManageDataSource is false', () => { + const mockedContextWithFalseManage = { + ...mockManagementPlugin.createDataSourceManagementContext(), + application: { capabilities: { dataSource: { canManage: false } } }, + }; + beforeEach(async () => { + spyOn(utils, 'getDataSources').and.returnValue(Promise.reject()); + await act(async () => { + component = await mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContextWithFalseManage, + }, + } + ); + }); + component.update(); + }); + test('should render empty table', () => { + expect(component).toMatchSnapshot(); + expect(component.find(emptyStateIdentifier).exists()).toBe(true); + }); + }); }); diff --git a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx index 3426b947d3b1..9a4264f1ec4f 100644 --- a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx +++ b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx @@ -53,6 +53,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { savedObjects, notifications: { toasts }, uiSettings, + application, } = useOpenSearchDashboards().services; /* Component state variables */ @@ -61,6 +62,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { const [isLoading, setIsLoading] = React.useState(false); const [isDeleting, setIsDeleting] = React.useState(false); const [confirmDeleteVisible, setConfirmDeleteVisible] = React.useState(false); + const canManageDataSource = !!application.capabilities?.dataSource?.canManage; /* useEffectOnce hook to avoid these methods called multiple times when state is updated. */ useEffectOnce(() => { @@ -111,11 +113,11 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { }; const renderToolsRight = () => { - return ( + return canManageDataSource ? ( {renderDeleteButton()} - ); + ) : null; }; const search = { @@ -323,7 +325,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { /> - {createButtonEmptyState} + {canManageDataSource ? createButtonEmptyState : null} diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx index 876e36bf56ff..73357cafa62e 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx @@ -90,6 +90,7 @@ describe('Datasource Management: Edit Datasource Form', () => { onSetDefaultDataSource={mockFn} handleTestConnection={mockFn} displayToastMessage={mockFn} + canManageDataSource={true} /> ), { @@ -261,6 +262,7 @@ describe('Datasource Management: Edit Datasource Form', () => { handleSubmit={mockFn} handleTestConnection={mockFn} displayToastMessage={mockFn} + canManageDataSource={true} /> ), { @@ -373,6 +375,7 @@ describe('Datasource Management: Edit Datasource Form', () => { onSetDefaultDataSource={mockFn} handleTestConnection={mockFn} displayToastMessage={mockFn} + canManageDataSource={true} /> ), { @@ -593,6 +596,7 @@ describe('With Registered Authentication', () => { onSetDefaultDataSource={jest.fn()} handleTestConnection={jest.fn()} displayToastMessage={jest.fn()} + canManageDataSource={true} /> ), { @@ -634,6 +638,7 @@ describe('With Registered Authentication', () => { onSetDefaultDataSource={jest.fn()} handleTestConnection={jest.fn()} displayToastMessage={jest.fn()} + canManageDataSource={true} /> ), { diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx index e227e5e2087c..74ec7362381a 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx @@ -56,6 +56,7 @@ export interface EditDataSourceProps { onDeleteDataSource?: () => Promise; onSetDefaultDataSource: () => Promise; displayToastMessage: (info: ToastMessageItem) => void; + canManageDataSource: boolean; } export interface EditDataSourceState { formErrorsByField: CreateEditDataSourceValidation; @@ -644,6 +645,7 @@ export class EditDataSourceForm extends React.Component ); }; @@ -1119,24 +1121,26 @@ export class EditDataSourceForm extends React.Component - - - - - + {this.props.canManageDataSource ? ( + + + + + + ) : null} ); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx index 5a23b72881a1..4953e21647c7 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx @@ -34,6 +34,7 @@ describe('Datasource Management: Edit Datasource Header', () => { dataSourceName={dataSourceName} onClickSetDefault={mockFn} isDefault={false} + canManageDataSource={true} /> ), { @@ -87,6 +88,7 @@ describe('Datasource Management: Edit Datasource Header', () => { dataSourceName={dataSourceName} onClickSetDefault={mockFn} isDefault={false} + canManageDataSource={true} /> ), { @@ -116,6 +118,7 @@ describe('Datasource Management: Edit Datasource Header', () => { dataSourceName={dataSourceName} onClickSetDefault={onClickSetDefault} isDefault={isDefaultDataSourceState} + canManageDataSource={true} /> ), { @@ -152,6 +155,7 @@ describe('Datasource Management: Edit Datasource Header', () => { dataSourceName={dataSourceName} onClickSetDefault={onClickSetDefault} isDefault={isDefaultDataSourceState} + canManageDataSource={true} /> ), { @@ -174,4 +178,35 @@ describe('Datasource Management: Edit Datasource Header', () => { ); }); }); + describe('should not manage data source', () => { + beforeEach(() => { + component = mount( + wrapWithIntl( +
+ ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + }); + test('should not show delete', () => { + expect(component.find(headerTitleIdentifier).last().text()).toBe(dataSourceName); + expect(component.find(deleteIconIdentifier).exists()).toBe(false); + }); + test('should not show default icon', () => { + expect(component.find(setDefaultButtonIdentifier).exists()).toBe(false); + }); + }); }); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx index 264647882574..dece5cb78b2b 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx @@ -29,6 +29,7 @@ export const Header = ({ onClickSetDefault, dataSourceName, isDefault, + canManageDataSource, }: { showDeleteIcon: boolean; isFormValid: boolean; @@ -37,6 +38,7 @@ export const Header = ({ onClickSetDefault: () => void; dataSourceName: string; isDefault: boolean; + canManageDataSource: boolean; }) => { /* State Variables */ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -175,11 +177,15 @@ export const Header = ({ {/* Test default button */} - {renderDefaultIcon()} + {canManageDataSource ? ( + {renderDefaultIcon()} + ) : null} {/* Test connection button */} {renderTestConnectionButton()} {/* Delete icon button */} - {showDeleteIcon ? renderDeleteButton() : null} + {canManageDataSource ? ( + {showDeleteIcon ? renderDeleteButton() : null} + ) : null} diff --git a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx index d8d175a920d9..f53744d67716 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx @@ -28,7 +28,10 @@ const formIdentifier = 'EditDataSourceForm'; const notFoundIdentifier = '[data-test-subj="dataSourceNotFound"]'; describe('Datasource Management: Edit Datasource Wizard', () => { - const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + const mockedContext = { + ...mockManagementPlugin.createDataSourceManagementContext(), + application: { capabilities: { dataSource: { canManage: true } } }, + }; const uiSettings = mockedContext.uiSettings; mockedContext.authenticationMethodRegistry.registerAuthenticationMethod( noAuthCredentialAuthMethod diff --git a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx index 334a34322ec2..fbf63aaecb0d 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx @@ -46,6 +46,7 @@ export const EditDataSource: React.FunctionComponent().services; const dataSourceID: string = props.match.params.id; @@ -162,6 +163,7 @@ export const EditDataSource: React.FunctionComponent ) : null} {isLoading || !dataSource?.endpoint ? : null} diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index 1c9bf676d5b3..9c4fa0ff9e06 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -69,6 +69,11 @@ export const mountManagementSection = async ({ if (allowedObjectTypes === undefined) { allowedObjectTypes = await getAllowedTypes(coreStart.http); } + // Restrict user to manage data source in the saved object management page according the manageableBy flag. + const showDataSource = !!coreStart.application.capabilities?.dataSource?.canManage; + allowedObjectTypes = showDataSource + ? allowedObjectTypes + : allowedObjectTypes.filter((type) => type !== 'data-source'); coreStart.chrome.docTitle.change(title);