Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DataSource] Restrict to manage data source on the DSM UI #7214

Merged
merged 10 commits into from
Jul 17, 2024
2 changes: 2 additions & 0 deletions changelogs/fragments/7214.yml
Original file line number Diff line number Diff line change
@@ -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))
6 changes: 6 additions & 0 deletions config/opensearch_dashboards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,12 @@
# AWSSigV4:
# enabled: true

# Optional setting that controls the permissions of data source to create, update and delete.
# "read_only": The data source is readonly for all users including OSD admin.
# "admin_only": The data source can only be edited by OSD admin.
# "none": The data source can be edited by all users. Default to "none".
# data_source.editMode: "none"
yubonluo marked this conversation as resolved.
Show resolved Hide resolved

# 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
6 changes: 6 additions & 0 deletions src/plugins/data_source/common/data_sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,9 @@ export enum DataSourceEngineType {
Elasticsearch = 'Elasticsearch',
NA = 'No Engine Type Available',
}

export enum EditMode {
ReadOnly = 'read_only',
AdminOnly = 'admin_only',
None = 'none',
}
4 changes: 4 additions & 0 deletions src/plugins/data_source/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
}),
}),
editMode: schema.oneOf(
[schema.literal('read_only'), schema.literal('admin_only'), schema.literal('none')],
{ defaultValue: 'none' }
),
});

export type DataSourcePluginConfigType = TypeOf<typeof configSchema>;
22 changes: 22 additions & 0 deletions src/plugins/data_source/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { EditMode } from '../common/data_sources';
import { getWorkspaceState } from '../../../../src/core/server/utils';

export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourcePluginStart> {
private readonly logger: Logger;
Expand Down Expand Up @@ -81,6 +83,26 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
dataSourceSavedObjectsClientWrapper.wrapperFactory
);

const { editMode } = config;
core.capabilities.registerProvider(() => ({
dataSource: {
canEdit: false,
canAssign: false,
yubonluo marked this conversation as resolved.
Show resolved Hide resolved
},
}));

core.capabilities.registerSwitcher((request) => {
const { requestWorkspaceId, isDashboardAdmin } = getWorkspaceState(request);
// User can not edit data source in the workspace.
const canEdit =
(editMode === EditMode.None && !requestWorkspaceId) ||
(editMode === EditMode.AdminOnly && isDashboardAdmin !== false && !requestWorkspaceId);
// Only OSD admin user can assign data source in the workspace.
const canAssign = !!requestWorkspaceId && isDashboardAdmin !== false;

return { dataSource: { canEdit, canAssign } };
});

core.logging.configure(
this.config$.pipe<LoggerContextConfigInput>(
map((dataSourceConfig) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('DataSourceHomePanel', () => {
http: {},
savedObjects: {},
uiSettings: {},
application: { capabilities: { dataSource: { canEdit: true } } },
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ export const DataSourceHomePanel: React.FC<DataSourceHomePanelProps> = ({
featureFlagStatus,
...props
}) => {
const { setBreadcrumbs, notifications, http, savedObjects, uiSettings } = useOpenSearchDashboards<
DataSourceManagementContext
>().services;
const {
setBreadcrumbs,
notifications,
http,
savedObjects,
uiSettings,
application,
} = useOpenSearchDashboards<DataSourceManagementContext>().services;

const [selectedTabId, setSelectedTabId] = useState('manageDirectQueryDataSources');
const canEditDataSource = !!application.capabilities?.dataSource?.canEdit;

useEffect(() => {
setBreadcrumbs(getListBreadcrumbs());
Expand Down Expand Up @@ -80,9 +86,11 @@ export const DataSourceHomePanel: React.FC<DataSourceHomePanelProps> = ({
<EuiFlexItem grow={false}>
<DataSourceHeader history={props.history} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CreateButton history={props.history} dataTestSubj="createDataSourceButton" />
</EuiFlexItem>
{canEditDataSource ? (
<EuiFlexItem grow={false}>
<CreateButton history={props.history} dataTestSubj="createDataSourceButton" />
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiPageHeader>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: { canEdit: true } } },
};
const uiSettings = mockedContext.uiSettings;
let component: ReactWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>;
const history = (scopedHistoryMock.create() as unknown) as ScopedHistory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => {
savedObjects,
notifications: { toasts },
uiSettings,
application,
} = useOpenSearchDashboards<DataSourceManagementContext>().services;

/* Component state variables */
Expand All @@ -61,6 +62,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [isDeleting, setIsDeleting] = React.useState<boolean>(false);
const [confirmDeleteVisible, setConfirmDeleteVisible] = React.useState(false);
const canEditDataSource = !!application.capabilities?.dataSource?.canEdit;

/* useEffectOnce hook to avoid these methods called multiple times when state is updated. */
useEffectOnce(() => {
Expand Down Expand Up @@ -111,11 +113,11 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => {
};

const renderToolsRight = () => {
return (
return canEditDataSource ? (
<EuiFlexItem key="delete" grow={false}>
{renderDeleteButton()}
</EuiFlexItem>
);
) : null;
};

const search = {
Expand Down Expand Up @@ -323,7 +325,7 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => {
/>
</EuiText>
<EuiSpacer />
{createButtonEmptyState}
{canEditDataSource ? createButtonEmptyState : null}
</EuiPanel>
<EuiSpacer size="l" />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ describe('Datasource Management: Edit Datasource Form', () => {
onSetDefaultDataSource={mockFn}
handleTestConnection={mockFn}
displayToastMessage={mockFn}
canEditDataSource={true}
/>
),
{
Expand Down Expand Up @@ -261,6 +262,7 @@ describe('Datasource Management: Edit Datasource Form', () => {
handleSubmit={mockFn}
handleTestConnection={mockFn}
displayToastMessage={mockFn}
canEditDataSource={true}
/>
),
{
Expand Down Expand Up @@ -373,6 +375,7 @@ describe('Datasource Management: Edit Datasource Form', () => {
onSetDefaultDataSource={mockFn}
handleTestConnection={mockFn}
displayToastMessage={mockFn}
canEditDataSource={true}
/>
),
{
Expand Down Expand Up @@ -593,6 +596,7 @@ describe('With Registered Authentication', () => {
onSetDefaultDataSource={jest.fn()}
handleTestConnection={jest.fn()}
displayToastMessage={jest.fn()}
canEditDataSource={true}
/>
),
{
Expand Down Expand Up @@ -634,6 +638,7 @@ describe('With Registered Authentication', () => {
onSetDefaultDataSource={jest.fn()}
handleTestConnection={jest.fn()}
displayToastMessage={jest.fn()}
canEditDataSource={true}
/>
),
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface EditDataSourceProps {
onDeleteDataSource?: () => Promise<void>;
onSetDefaultDataSource: () => Promise<void>;
displayToastMessage: (info: ToastMessageItem) => void;
canEditDataSource: boolean;
}
export interface EditDataSourceState {
formErrorsByField: CreateEditDataSourceValidation;
Expand Down Expand Up @@ -644,6 +645,7 @@ export class EditDataSourceForm extends React.Component<EditDataSourceProps, Edi
onClickTestConnection={this.onClickTestConnection}
onClickSetDefault={this.setDefaultDataSource}
isDefault={this.props.isDefault}
canEditDataSource={this.props.canEditDataSource}
/>
);
};
Expand Down Expand Up @@ -1119,24 +1121,26 @@ export class EditDataSourceForm extends React.Component<EditDataSourceProps, Edi
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
className="mgtAdvancedSettingsForm__button"
disabled={!this.isFormValid()}
color="secondary"
fill
size="s"
iconType="check"
isLoading={this.state.isLoading}
onClick={this.onClickUpdateDataSource}
data-test-subj="datasource-edit-saveButton"
>
<FormattedMessage
id="dataSourcesManagement.editDataSource.saveButtonLabel"
defaultMessage="Save changes"
/>
</EuiButton>
</EuiFlexItem>
{this.props.canEditDataSource ? (
<EuiFlexItem grow={false}>
<EuiButton
className="mgtAdvancedSettingsForm__button"
disabled={!this.isFormValid()}
color="secondary"
fill
size="s"
iconType="check"
isLoading={this.state.isLoading}
onClick={this.onClickUpdateDataSource}
data-test-subj="datasource-edit-saveButton"
>
<FormattedMessage
id="dataSourcesManagement.editDataSource.saveButtonLabel"
defaultMessage="Save changes"
/>
</EuiButton>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiBottomBar>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('Datasource Management: Edit Datasource Header', () => {
dataSourceName={dataSourceName}
onClickSetDefault={mockFn}
isDefault={false}
canEditDataSource={true}
/>
),
{
Expand Down Expand Up @@ -87,6 +88,7 @@ describe('Datasource Management: Edit Datasource Header', () => {
dataSourceName={dataSourceName}
onClickSetDefault={mockFn}
isDefault={false}
canEditDataSource={true}
/>
),
{
Expand Down Expand Up @@ -116,6 +118,7 @@ describe('Datasource Management: Edit Datasource Header', () => {
dataSourceName={dataSourceName}
onClickSetDefault={onClickSetDefault}
isDefault={isDefaultDataSourceState}
canEditDataSource={true}
/>
),
{
Expand Down Expand Up @@ -152,6 +155,7 @@ describe('Datasource Management: Edit Datasource Header', () => {
dataSourceName={dataSourceName}
onClickSetDefault={onClickSetDefault}
isDefault={isDefaultDataSourceState}
canEditDataSource={true}
/>
),
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const Header = ({
onClickSetDefault,
dataSourceName,
isDefault,
canEditDataSource,
}: {
showDeleteIcon: boolean;
isFormValid: boolean;
Expand All @@ -37,6 +38,7 @@ export const Header = ({
onClickSetDefault: () => void;
dataSourceName: string;
isDefault: boolean;
canEditDataSource: boolean;
}) => {
/* State Variables */
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
Expand Down Expand Up @@ -175,11 +177,13 @@ export const Header = ({
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="baseline" gutterSize="m" responsive={false}>
{/* Test default button */}
<EuiFlexItem grow={false}>{renderDefaultIcon()}</EuiFlexItem>
{canEditDataSource ? <EuiFlexItem grow={false}>{renderDefaultIcon()}</EuiFlexItem> : null}
{/* Test connection button */}
<EuiFlexItem grow={false}>{renderTestConnectionButton()}</EuiFlexItem>
{/* Delete icon button */}
<EuiFlexItem grow={false}>{showDeleteIcon ? renderDeleteButton() : null}</EuiFlexItem>
{canEditDataSource ? (
<EuiFlexItem grow={false}>{showDeleteIcon ? renderDeleteButton() : null}</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: { canEdit: true } } },
};
const uiSettings = mockedContext.uiSettings;
mockedContext.authenticationMethodRegistry.registerAuthenticationMethod(
noAuthCredentialAuthMethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const EditDataSource: React.FunctionComponent<RouteComponentProps<{ id: s
setBreadcrumbs,
http,
notifications: { toasts },
application,
} = useOpenSearchDashboards<DataSourceManagementContext>().services;
const dataSourceID: string = props.match.params.id;

Expand Down Expand Up @@ -162,6 +163,7 @@ export const EditDataSource: React.FunctionComponent<RouteComponentProps<{ id: s
handleSubmit={handleSubmit}
displayToastMessage={handleDisplayToastMessage}
handleTestConnection={handleTestConnection}
canEditDataSource={!!application.capabilities?.dataSource?.canEdit}
/>
) : null}
{isLoading || !dataSource?.endpoint ? <LoadingMask /> : null}
Expand Down
Loading