mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Roles] Improved handling for operator-defined role mappings (#208710)
## Summary Improved handling for operator-defined role mappings: - Detail page with operator-defined role mappings is now marked with lock icon and tooltip - Operator-defined role mappings have a read-only experience. <img width="1256" alt="Screenshot 2025-01-29 at 11 45 27" src="https://github.com/user-attachments/assets/911dd2cd-4fe8-4141-8d8a-ffde974884d7" /> <img width="1234" alt="Screenshot 2025-01-28 at 15 21 44" src="https://github.com/user-attachments/assets/d9a03431-f8da-471e-8c94-f504aa00623d" /> ### How to test 1. Create a `settings.json` file in `$ES_HOME/config/operator/settings.json`, and define some role mappings there. Here's an example that will create 2 operator mappings: ```json { "metadata": { "version": "1", "compatibility": "8.4.0" }, "state": { "role_mappings": { "operator_role_mapping_1": { "enabled": true, "roles": [ "kibana_user" ], "metadata": { "from_file": true }, "rules": { "field": { "username": "role-mapping-test-user" } } }, "operator_role_mapping_2": { "enabled": true, "roles": [ "fleet_user" ], "metadata": { "from_file": true }, "rules": { "field": { "username": "role-mapping-test-user" } } } } } } ``` 2. Navigate to `Role Mappings` page and check the UI has a read only view. 3. Navigate to `Role Mappings Details` page and check the UI has a read only view. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) __Closes: https://github.com/elastic/kibana/issues/194635__ ### Release Notes Improved handling for operator-defined role mappings --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
2fd10dbabe
commit
7adc337c5f
4 changed files with 152 additions and 10 deletions
|
@ -437,6 +437,72 @@ describe('EditRoleMappingPage', () => {
|
|||
expect(rulePanels.at(0).props().readOnly).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders a readonly view when role mapping has metadata._readonly=true', async () => {
|
||||
const roleMappingsAPI = roleMappingsAPIClientMock.create();
|
||||
const securityFeaturesAPI = securityFeaturesAPIClientMock.create();
|
||||
roleMappingsAPI.saveRoleMapping.mockResolvedValue(null);
|
||||
roleMappingsAPI.getRoleMapping.mockResolvedValue({
|
||||
name: 'foo',
|
||||
role_templates: [
|
||||
{
|
||||
template: { id: 'foo' },
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rules: {
|
||||
all: [
|
||||
{
|
||||
field: {
|
||||
username: '*',
|
||||
},
|
||||
},
|
||||
{
|
||||
all: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
metadata: {
|
||||
_read_only: true,
|
||||
},
|
||||
});
|
||||
securityFeaturesAPI.checkFeatures.mockResolvedValue({
|
||||
canReadSecurity: true,
|
||||
hasCompatibleRealms: true,
|
||||
canUseInlineScripts: true,
|
||||
canUseStoredScripts: true,
|
||||
});
|
||||
|
||||
const wrapper = renderView(roleMappingsAPI, securityFeaturesAPI, 'foo');
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
|
||||
// back button
|
||||
const backButton = wrapper.find('button[data-test-subj="roleMappingFormReturnButton"]');
|
||||
expect(backButton).toHaveLength(1);
|
||||
|
||||
// no save button
|
||||
const saveButton = wrapper.find('button[data-test-subj="saveRoleMappingButton"]');
|
||||
expect(saveButton).toHaveLength(0);
|
||||
|
||||
// no delete button
|
||||
const deleteButton = wrapper.find('emptyButton[data-test-subj="deleteRoleMappingButton"]');
|
||||
expect(deleteButton).toHaveLength(0);
|
||||
|
||||
// Info panel is read-only (view mode)
|
||||
const infoPanels = wrapper.find('MappingInfoPanel[data-test-subj="roleMappingInfoPanel"]');
|
||||
expect(infoPanels).toHaveLength(1);
|
||||
expect(infoPanels.at(0).props().mode).toEqual('view');
|
||||
|
||||
// Rule panel is read-only
|
||||
const rulePanels = wrapper.find('RuleEditorPanel[data-test-subj="roleMappingRulePanel"]');
|
||||
expect(rulePanels).toHaveLength(1);
|
||||
expect(rulePanels.at(0).props().readOnly).toBeTruthy();
|
||||
|
||||
// Lock icon is displayed
|
||||
const lockIcon = wrapper.find('EuiToolTip[data-test-subj="readOnlyRoleMappingTooltip"]');
|
||||
expect(lockIcon).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a warning when empty any or all rules are present', async () => {
|
||||
const roleMappingsAPI = roleMappingsAPIClientMock.create();
|
||||
const securityFeaturesAPI = securityFeaturesAPIClientMock.create();
|
||||
|
|
|
@ -12,10 +12,12 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPageHeader,
|
||||
EuiPageSection,
|
||||
EuiSpacer,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
|
@ -179,7 +181,7 @@ export class EditRoleMappingPage extends Component<Props, State> {
|
|||
})
|
||||
}
|
||||
docLinks={this.props.docLinks}
|
||||
readOnly={this.props.readOnly}
|
||||
readOnly={this.isReadOnly()}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
{this.getFormWarnings()}
|
||||
|
@ -191,16 +193,32 @@ export class EditRoleMappingPage extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private getInfoPanelMode = () => {
|
||||
return this.props.readOnly ? 'view' : this.editingExistingRoleMapping() ? 'edit' : 'create';
|
||||
return this.isReadOnly() ? 'view' : this.editingExistingRoleMapping() ? 'edit' : 'create';
|
||||
};
|
||||
|
||||
private getFormTitle = () => {
|
||||
if (this.props.readOnly) {
|
||||
if (this.isReadOnly()) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRoleMapping.readOnlyRoleMappingTitle"
|
||||
defaultMessage="Viewing role mapping"
|
||||
/>
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRoleMapping.readOnlyRoleMappingTitle"
|
||||
defaultMessage="Viewing role mapping"
|
||||
/>
|
||||
|
||||
{this.isReadOnlyRoleMapping() && (
|
||||
<EuiToolTip
|
||||
data-test-subj="readOnlyRoleMappingTooltip"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRoleMapping.readOnlyRoleMappingBadge.readOnlyRoleMappingCanNotBeModifiedTooltip"
|
||||
defaultMessage="Read only role mappings are built-in and cannot be removed or modified."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiIcon style={{ verticalAlign: 'super' }} type={'lock'} />
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (this.editingExistingRoleMapping()) {
|
||||
|
@ -279,7 +297,7 @@ export class EditRoleMappingPage extends Component<Props, State> {
|
|||
};
|
||||
|
||||
private getFormButtons = () => {
|
||||
if (this.props.readOnly === true) {
|
||||
if (this.isReadOnly() === true) {
|
||||
return this.getReturnToRoleMappingListButton();
|
||||
}
|
||||
|
||||
|
@ -338,7 +356,7 @@ export class EditRoleMappingPage extends Component<Props, State> {
|
|||
};
|
||||
|
||||
private getDeleteButton = () => {
|
||||
if (this.editingExistingRoleMapping() && !this.props.readOnly) {
|
||||
if (this.editingExistingRoleMapping() && !this.isReadOnly()) {
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<DeleteProvider
|
||||
|
@ -427,6 +445,10 @@ export class EditRoleMappingPage extends Component<Props, State> {
|
|||
private cloningExistingRoleMapping = () =>
|
||||
typeof this.props.name === 'string' && this.props.action === 'clone';
|
||||
|
||||
private isReadOnlyRoleMapping = () => this.state.roleMapping?.metadata?._read_only;
|
||||
|
||||
private isReadOnly = () => this.props.readOnly || this.isReadOnlyRoleMapping();
|
||||
|
||||
private async loadAppData() {
|
||||
try {
|
||||
const [features, roleMapping] = await Promise.all([
|
||||
|
|
|
@ -396,5 +396,46 @@ describe('RoleMappingsGridPage', () => {
|
|||
const actionMenuButton = wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]');
|
||||
expect(actionMenuButton).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('hides controls when role mapping is read only', async () => {
|
||||
const roleMappingsAPI = roleMappingsAPIClientMock.create();
|
||||
const securityFeaturesAPI = securityFeaturesAPIClientMock.create();
|
||||
roleMappingsAPI.getRoleMappings.mockResolvedValue([
|
||||
{
|
||||
name: 'some-realm',
|
||||
enabled: true,
|
||||
roles: ['superuser'],
|
||||
metadata: { _read_only: true },
|
||||
rules: { field: { username: '*' } },
|
||||
},
|
||||
]);
|
||||
securityFeaturesAPI.checkFeatures.mockResolvedValue({
|
||||
canReadSecurity: true,
|
||||
hasCompatibleRealms: true,
|
||||
});
|
||||
roleMappingsAPI.deleteRoleMappings.mockResolvedValue([
|
||||
{
|
||||
name: 'some-realm',
|
||||
success: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = renderView(roleMappingsAPI, securityFeaturesAPI);
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
|
||||
// role mapping actions are hidden
|
||||
const editButton = wrapper.find('[data-test-subj="editRoleMappingButton-some-realm"]');
|
||||
expect(editButton).toHaveLength(0);
|
||||
|
||||
const cloneButton = wrapper.find('[data-test-subj="cloneRoleMappingButton-some-realm"]');
|
||||
expect(cloneButton).toHaveLength(0);
|
||||
|
||||
const deleteButton = wrapper.find('[data-test-subj="deleteRoleMappingButton-some-realm"]');
|
||||
expect(deleteButton).toHaveLength(0);
|
||||
|
||||
const actionMenuButton = wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]');
|
||||
expect(actionMenuButton).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -194,6 +194,8 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
private isReadOnlyRoleMapping = (record: RoleMapping) => record.metadata?._read_only;
|
||||
|
||||
private renderTable = () => {
|
||||
const { roleMappings, selectedItems, loadState } = this.state;
|
||||
|
||||
|
@ -283,7 +285,15 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
columns={this.getColumnConfig(deleteRoleMappingPrompt)}
|
||||
search={search}
|
||||
sorting={sorting}
|
||||
selection={this.props.readOnly ? undefined : selection}
|
||||
selection={
|
||||
this.props.readOnly
|
||||
? undefined
|
||||
: {
|
||||
selectable: (roleMapping: RoleMapping) =>
|
||||
!this.isReadOnlyRoleMapping(roleMapping),
|
||||
...selection,
|
||||
}
|
||||
}
|
||||
pagination={pagination}
|
||||
loading={loadState === 'loadingTable'}
|
||||
message={message}
|
||||
|
@ -386,6 +396,7 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
name: i18n.translate('xpack.security.management.roleMappings.actionCloneTooltip', {
|
||||
defaultMessage: 'Clone',
|
||||
}),
|
||||
available: (roleMapping: RoleMapping) => !this.isReadOnlyRoleMapping(roleMapping),
|
||||
description: (record: RoleMapping) =>
|
||||
i18n.translate('xpack.security.management.roleMappings.actionCloneAriaLabel', {
|
||||
defaultMessage: `Clone ''{name}''`,
|
||||
|
@ -406,6 +417,7 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
name: i18n.translate('xpack.security.management.roleMappings.actionDeleteTooltip', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
available: (roleMapping: RoleMapping) => !this.isReadOnlyRoleMapping(roleMapping),
|
||||
description: (record: RoleMapping) =>
|
||||
i18n.translate('xpack.security.management.roleMappings.actionDeleteAriaLabel', {
|
||||
defaultMessage: `Delete ''{name}''`,
|
||||
|
@ -422,6 +434,7 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
name: i18n.translate('xpack.security.management.roleMappings.actionEditTooltip', {
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
available: (roleMapping: RoleMapping) => !this.isReadOnlyRoleMapping(roleMapping),
|
||||
description: (record: RoleMapping) =>
|
||||
i18n.translate('xpack.security.management.roleMappings.actionEditAriaLabel', {
|
||||
defaultMessage: `Edit ''{name}''`,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue