[Role Mappings] Warn when empty any or all rule role mappings are added (#189340)

Closes https://github.com/elastic/kibana/issues/187752

## Summary

Display a warning to users when a role mapping is created/updated to
include empty `any` or `all` rules

### Screenshots

<img width="1480" alt="image"
src="https://github.com/user-attachments/assets/6cb7e505-95d9-43c6-b8b7-a1f9114cdcda">


### Release notes

Display a warning to users whenever role mappings with empty `any` or
`all` rules are created or updated.

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Sid 2024-08-01 12:33:36 +02:00 committed by GitHub
parent ce8f6ea882
commit 63a1cbe25e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 122 additions and 6 deletions

View file

@ -12,6 +12,11 @@ export type {
BuiltinESPrivileges,
RawKibanaPrivileges,
RoleMapping,
RoleMappingRule,
RoleMappingAllRule,
RoleMappingAnyRule,
RoleMappingExceptRule,
RoleMappingFieldRule,
RoleTemplate,
StoredRoleTemplate,
InvalidRoleTemplate,

View file

@ -40,4 +40,9 @@ export type {
InvalidRoleTemplate,
RoleTemplate,
RoleMapping,
RoleMappingRule,
RoleMappingAllRule,
RoleMappingAnyRule,
RoleMappingExceptRule,
RoleMappingFieldRule,
} from './role_mapping';

View file

@ -5,23 +5,23 @@
* 2.0.
*/
interface RoleMappingAnyRule {
export interface RoleMappingAnyRule {
any: RoleMappingRule[];
}
interface RoleMappingAllRule {
export interface RoleMappingAllRule {
all: RoleMappingRule[];
}
interface RoleMappingFieldRule {
export interface RoleMappingFieldRule {
field: Record<string, any>;
}
interface RoleMappingExceptRule {
export interface RoleMappingExceptRule {
except: RoleMappingRule;
}
type RoleMappingRule =
export type RoleMappingRule =
| RoleMappingAnyRule
| RoleMappingAllRule
| RoleMappingFieldRule

View file

@ -441,4 +441,43 @@ describe('EditRoleMappingPage', () => {
expect(rulePanels).toHaveLength(1);
expect(rulePanels.at(0).props().readOnly).toBeTruthy();
});
it('renders a warning when empty any or all rules are present', 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: [],
},
],
},
});
securityFeaturesAPI.checkFeatures.mockResolvedValue({
canReadSecurity: true,
hasCompatibleRealms: true,
canUseInlineScripts: true,
canUseStoredScripts: true,
});
const wrapper = renderView(roleMappingsAPI, securityFeaturesAPI, 'foo');
await nextTick();
wrapper.update();
expect(findTestSubject(wrapper, 'emptyAnyOrAllRulesWarning')).toHaveLength(1);
});
});

View file

@ -8,6 +8,7 @@
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
@ -26,7 +27,12 @@ import type { PublicMethodsOf } from '@kbn/utility-types';
import { MappingInfoPanel } from './mapping_info_panel';
import { RuleEditorPanel } from './rule_editor_panel';
import { validateRoleMappingForSave } from './services/role_mapping_validation';
import type { RoleMapping } from '../../../../common';
import type {
RoleMapping,
RoleMappingAllRule,
RoleMappingAnyRule,
RoleMappingRule,
} from '../../../../common';
import type { RolesAPIClient } from '../../roles';
import type { SecurityFeaturesAPIClient } from '../../security_features';
import {
@ -176,6 +182,8 @@ export class EditRoleMappingPage extends Component<Props, State> {
readOnly={this.props.readOnly}
/>
<EuiSpacer />
{this.getFormWarnings()}
<EuiSpacer />
{this.getFormButtons()}
</EuiForm>
</>
@ -211,6 +219,65 @@ export class EditRoleMappingPage extends Component<Props, State> {
);
};
private isObject = (record?: any): record is object => {
return typeof record === 'object' && record !== null;
};
private isRoleMappingAnyRule = (obj: unknown): obj is RoleMappingAnyRule => {
return this.isObject(obj) && 'any' in obj && Array.isArray(obj.any);
};
private isRoleMappingAllRule = (obj: unknown): obj is RoleMappingAllRule => {
return this.isObject(obj) && 'all' in obj && Array.isArray(obj.all);
};
private checkEmptyAnyAllMappings = (obj: RoleMappingRule) => {
const arrToCheck: RoleMappingRule[] = [obj];
while (arrToCheck.length > 0) {
const currentObj = arrToCheck.pop();
if (this.isObject(obj)) {
for (const key in currentObj) {
if (Object.hasOwn(currentObj, key)) {
if (this.isRoleMappingAnyRule(currentObj)) {
if (currentObj.any.length === 0) {
return true;
}
arrToCheck.push(...currentObj.any);
} else if (this.isRoleMappingAllRule(currentObj)) {
if (currentObj.all.length === 0) {
return true;
}
arrToCheck.push(...currentObj.all);
} else if (this.isObject(currentObj[key as keyof RoleMappingRule])) {
arrToCheck.push(currentObj[key as keyof RoleMappingRule] as RoleMappingRule);
}
}
}
}
}
return false;
};
private getFormWarnings = () => {
if (this.checkEmptyAnyAllMappings(this.state.roleMapping!.rules as RoleMappingRule)) {
return (
<EuiCallOut
title="Warning"
color="warning"
iconType="alert"
data-test-subj="emptyAnyOrAllRulesWarning"
>
<FormattedMessage
id="xpack.security.management.editRoleMapping.emptyAnyAllMappingsWarning"
defaultMessage="Role mapping rules contains empty 'any' or 'all' rules. These empty rule groups will always evaluate to true. Please proceed with caution"
/>
</EuiCallOut>
);
}
return null;
};
private getFormButtons = () => {
if (this.props.readOnly === true) {
return this.getReturnToRoleMappingListButton();