[7.x] Expose ability to deny ('except') access to fields via FLS (#26472) (#35180)

Backports the following commits to 7.x:
 - Expose ability to deny ('except')  access to fields via FLS  (#26472)
This commit is contained in:
Larry Gregory 2019-04-16 15:56:05 -04:00 committed by GitHub
parent 2209bdf257
commit 4f574e1b4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 240 additions and 91 deletions

View file

@ -11,6 +11,7 @@ export interface RoleIndexPrivilege {
privileges: string[];
field_security?: {
grant?: string[];
except?: string[];
};
query?: string;
}

View file

@ -112,41 +112,25 @@ exports[`it renders without crashing 1`] = `
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup
direction="column"
>
<EuiFlexItem>
<EuiFormRow
className="indexPrivilegeForm__grantedFieldsRow"
describedByIds={Array []}
fullWidth={true}
hasEmptyLabelSpace={false}
helpText={
<FormattedMessage
defaultMessage="If no fields are granted, then users assigned to this role will not be able to see any data for this index."
id="xpack.security.management.editRole.indexPrivilegeForm.grantedFieldsFormRowHelpText"
values={Object {}}
/>
}
<EuiSwitch
checked={false}
compressed={true}
data-test-subj="restrictFieldsQuery0"
label={
<FormattedMessage
defaultMessage="Granted fields (optional)"
id="xpack.security.management.editRole.indexPrivilegeForm.grantedFieldsFormRowLabel"
defaultMessage="Grant access to specific fields"
id="xpack.security.management.editRoles.indexPrivilegeForm.grantFieldPrivilegesLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiComboBox
compressed={false}
data-test-subj="fieldInput0"
fullWidth={false}
isClearable={true}
isDisabled={false}
onChange={[Function]}
onCreateOption={[Function]}
options={Array []}
selectedOptions={Array []}
singleSelection={false}
/>
</EuiFormRow>
onChange={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButtonIcon, EuiSwitch, EuiTextArea } from '@elastic/eui';
import { EuiButtonIcon, EuiTextArea } from '@elastic/eui';
import React from 'react';
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { RoleValidator } from '../../../lib/validate_role';
@ -118,7 +118,7 @@ describe(`document level security`, () => {
};
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
expect(wrapper.find(EuiSwitch)).toHaveLength(0);
expect(wrapper.find('EuiSwitch[data-test-subj="restrictDocumentsQuery0"]')).toHaveLength(0);
expect(wrapper.find(EuiTextArea)).toHaveLength(0);
});
@ -132,7 +132,7 @@ describe(`document level security`, () => {
};
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
expect(wrapper.find(EuiSwitch)).toHaveLength(1);
expect(wrapper.find('EuiSwitch[data-test-subj="restrictDocumentsQuery0"]')).toHaveLength(1);
expect(wrapper.find(EuiTextArea)).toHaveLength(0);
});
@ -142,7 +142,7 @@ describe(`document level security`, () => {
};
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
expect(wrapper.find(EuiSwitch)).toHaveLength(1);
expect(wrapper.find('EuiSwitch[data-test-subj="restrictDocumentsQuery0"]')).toHaveLength(1);
expect(wrapper.find(EuiTextArea)).toHaveLength(1);
});
});
@ -170,23 +170,41 @@ describe('field level security', () => {
intl: {} as any,
};
test(`input is hidden when FLS is not allowed`, () => {
test(`inputs are hidden when FLS is not allowed`, () => {
const testProps = {
...props,
allowFieldLevelSecurity: false,
};
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
expect(wrapper.find('EuiSwitch[data-test-subj="restrictFieldsQuery0"]')).toHaveLength(0);
expect(wrapper.find('.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(0);
expect(wrapper.find('.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(0);
});
test('input is shown when allowed', () => {
test('only the switch is shown when allowed, and FLS is empty', () => {
const testProps = {
...props,
indexPrivilege: {
...props.indexPrivilege,
field_security: {},
},
};
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
expect(wrapper.find('EuiSwitch[data-test-subj="restrictFieldsQuery0"]')).toHaveLength(1);
expect(wrapper.find('.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(0);
expect(wrapper.find('.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(0);
});
test('inputs are shown when allowed', () => {
const testProps = {
...props,
};
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1);
expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1);
});
test('it displays a warning when no fields are granted', () => {
@ -196,12 +214,14 @@ describe('field level security', () => {
...props.indexPrivilege,
field_security: {
grant: [],
except: ['foo'],
},
},
};
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1);
expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1);
expect(wrapper.find('.euiFormHelpText')).toHaveLength(1);
});
@ -212,6 +232,7 @@ describe('field level security', () => {
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1);
expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1);
expect(wrapper.find('.euiFormHelpText')).toHaveLength(0);
});
});

View file

@ -17,6 +17,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import _ from 'lodash';
import React, { ChangeEvent, Component, Fragment } from 'react';
import { RoleIndexPrivilege } from '../../../../../../../common/model';
// @ts-ignore
@ -42,14 +43,23 @@ interface Props {
interface State {
queryExpanded: boolean;
fieldSecurityExpanded: boolean;
grantedFields: string[];
exceptedFields: string[];
documentQuery?: string;
}
export class IndexPrivilegeForm extends Component<Props, State> {
constructor(props: Props) {
super(props);
const { grant, except } = this.getFieldSecurity(props.indexPrivilege);
this.state = {
queryExpanded: !!props.indexPrivilege.query,
fieldSecurityExpanded: this.isFieldSecurityConfigured(props.indexPrivilege),
grantedFields: grant,
exceptedFields: except,
documentQuery: props.indexPrivilege.query,
};
}
@ -80,7 +90,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
);
}
public getPrivilegeForm = () => {
private getPrivilegeForm = () => {
return (
<Fragment>
<EuiFlexGroup>
@ -124,72 +134,123 @@ export class IndexPrivilegeForm extends Component<Props, State> {
/>
</EuiFormRow>
</EuiFlexItem>
{this.getGrantedFieldsControl()}
</EuiFlexGroup>
<EuiSpacer />
{this.getFieldLevelControls()}
{this.getGrantedDocumentsControl()}
</Fragment>
);
};
public getGrantedFieldsControl = () => {
private getFieldLevelControls = () => {
const {
allowFieldLevelSecurity,
allowDocumentLevelSecurity,
availableFields,
indexPrivilege,
isReadOnlyRole: isRoleReadOnly,
isReadOnlyRole,
} = this.props;
if (!allowFieldLevelSecurity) {
return null;
}
const { grant = [] } = indexPrivilege.field_security || {};
const { grant, except } = this.getFieldSecurity(indexPrivilege);
if (allowFieldLevelSecurity) {
return (
<EuiFlexItem>
<EuiFormRow
label={
<FormattedMessage
id="xpack.security.management.editRole.indexPrivilegeForm.grantedFieldsFormRowLabel"
defaultMessage="Granted fields (optional)"
/>
}
fullWidth={true}
className="indexPrivilegeForm__grantedFieldsRow"
helpText={
!isRoleReadOnly && grant.length === 0 ? (
<FormattedMessage
id="xpack.security.management.editRole.indexPrivilegeForm.grantedFieldsFormRowHelpText"
defaultMessage="If no fields are granted, then users assigned to this role will not be able to see any data for this index."
return (
<>
<EuiFlexGroup direction="column">
{!isReadOnlyRole && (
<EuiFlexItem>
{
// @ts-ignore missing "compressed" prop definition
<EuiSwitch
data-test-subj={`restrictFieldsQuery${this.props.formIndex}`}
label={
<FormattedMessage
id="xpack.security.management.editRoles.indexPrivilegeForm.grantFieldPrivilegesLabel"
defaultMessage="Grant access to specific fields"
/>
}
compressed={true}
checked={this.state.fieldSecurityExpanded}
onChange={this.toggleFieldSecurity}
/>
) : (
undefined
)
}
>
<Fragment>
<EuiComboBox
data-test-subj={`fieldInput${this.props.formIndex}`}
options={availableFields ? availableFields.map(toOption) : []}
selectedOptions={grant.map(toOption)}
onCreateOption={this.onCreateGrantedField}
onChange={this.onGrantedFieldsChange}
isDisabled={this.props.isReadOnlyRole}
/>
</Fragment>
</EuiFormRow>
</EuiFlexItem>
);
}
return null;
}
</EuiFlexItem>
)}
{this.state.fieldSecurityExpanded && (
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label={
<FormattedMessage
id="xpack.security.management.editRoles.indexPrivilegeForm.grantedFieldsFormRowLabel"
defaultMessage="Granted fields"
/>
}
fullWidth={true}
className="indexPrivilegeForm__grantedFieldsRow"
helpText={
!isReadOnlyRole && grant.length === 0 ? (
<FormattedMessage
id="xpack.security.management.editRoles.indexPrivilegeForm.grantedFieldsFormRowHelpText"
defaultMessage="If no fields are granted, then users assigned to this role will not be able to see any data for this index."
/>
) : (
undefined
)
}
>
<Fragment>
<EuiComboBox
data-test-subj={`fieldInput${this.props.formIndex}`}
options={availableFields ? availableFields.map(toOption) : []}
selectedOptions={grant.map(toOption)}
onCreateOption={this.onCreateGrantedField}
onChange={this.onGrantedFieldsChange}
isDisabled={this.props.isReadOnlyRole}
/>
</Fragment>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={
<FormattedMessage
id="xpack.security.management.editRoles.indexPrivilegeForm.deniedFieldsFormRowLabel"
defaultMessage="Denied fields"
/>
}
fullWidth={true}
className="indexPrivilegeForm__deniedFieldsRow"
>
<Fragment>
<EuiComboBox
data-test-subj={`deniedFieldInput${this.props.formIndex}`}
options={availableFields ? availableFields.map(toOption) : []}
selectedOptions={except.map(toOption)}
onCreateOption={this.onCreateDeniedField}
onChange={this.onDeniedFieldsChange}
isDisabled={isReadOnlyRole}
/>
</Fragment>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
{allowDocumentLevelSecurity && <EuiSpacer />}
</>
);
};
public getGrantedDocumentsControl = () => {
private getGrantedDocumentsControl = () => {
const { allowDocumentLevelSecurity, indexPrivilege } = this.props;
if (!allowDocumentLevelSecurity) {
@ -202,7 +263,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
{!this.props.isReadOnlyRole && (
<EuiFlexItem>
{
// @ts-ignore
// @ts-ignore missing "compressed" proptype
<EuiSwitch
data-test-subj={`restrictDocumentsQuery${this.props.formIndex}`}
label={
@ -244,7 +305,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
);
};
public toggleDocumentQuery = () => {
private toggleDocumentQuery = () => {
const willToggleOff = this.state.queryExpanded;
const willToggleOn = !willToggleOff;
@ -271,7 +332,48 @@ export class IndexPrivilegeForm extends Component<Props, State> {
}
};
public onCreateIndexPatternOption = (option: any) => {
private toggleFieldSecurity = () => {
const willToggleOff = this.state.fieldSecurityExpanded;
const willToggleOn = !willToggleOff;
const { grant, except } = this.getFieldSecurity(this.props.indexPrivilege);
// If turning off, then save the current configuration in state so that we can restore it if the user changes their mind.
this.setState({
fieldSecurityExpanded: !this.state.fieldSecurityExpanded,
grantedFields: willToggleOff ? grant : this.state.grantedFields,
exceptedFields: willToggleOff ? except : this.state.exceptedFields,
});
// If turning off, then remove the field security from the Index Privilege
if (willToggleOff) {
this.props.onChange({
...this.props.indexPrivilege,
field_security: {
grant: ['*'],
except: [],
},
});
}
// If turning on, then restore the saved field security if available
const hasConfiguredFieldSecurity = this.isFieldSecurityConfigured(this.props.indexPrivilege);
const hasSavedFieldSecurity =
this.state.exceptedFields.length > 0 || this.state.grantedFields.length > 0;
if (willToggleOn && !hasConfiguredFieldSecurity && hasSavedFieldSecurity) {
this.props.onChange({
...this.props.indexPrivilege,
field_security: {
grant: this.state.grantedFields,
except: this.state.exceptedFields,
},
});
}
};
private onCreateIndexPatternOption = (option: any) => {
const newIndexPatterns = this.props.indexPrivilege.names.concat([option]);
this.props.onChange({
@ -280,28 +382,28 @@ export class IndexPrivilegeForm extends Component<Props, State> {
});
};
public onIndexPatternsChange = (newPatterns: EuiComboBoxOptionProps[]) => {
private onIndexPatternsChange = (newPatterns: EuiComboBoxOptionProps[]) => {
this.props.onChange({
...this.props.indexPrivilege,
names: newPatterns.map(fromOption),
});
};
public onPrivilegeChange = (newPrivileges: EuiComboBoxOptionProps[]) => {
private onPrivilegeChange = (newPrivileges: EuiComboBoxOptionProps[]) => {
this.props.onChange({
...this.props.indexPrivilege,
privileges: newPrivileges.map(fromOption),
});
};
public onQueryChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
private onQueryChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
this.props.onChange({
...this.props.indexPrivilege,
query: e.target.value,
});
};
public onCreateGrantedField = (grant: string) => {
private onCreateGrantedField = (grant: string) => {
if (
!this.props.indexPrivilege.field_security ||
!this.props.indexPrivilege.field_security.grant
@ -320,7 +422,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
});
};
public onGrantedFieldsChange = (grantedFields: EuiComboBoxOptionProps[]) => {
private onGrantedFieldsChange = (grantedFields: EuiComboBoxOptionProps[]) => {
this.props.onChange({
...this.props.indexPrivilege,
field_security: {
@ -329,4 +431,43 @@ export class IndexPrivilegeForm extends Component<Props, State> {
},
});
};
private onCreateDeniedField = (except: string) => {
if (
!this.props.indexPrivilege.field_security ||
!this.props.indexPrivilege.field_security.except
) {
return;
}
const newExcepts = this.props.indexPrivilege.field_security.except.concat([except]);
this.props.onChange({
...this.props.indexPrivilege,
field_security: {
...this.props.indexPrivilege.field_security,
except: newExcepts,
},
});
};
private onDeniedFieldsChange = (deniedFields: EuiComboBoxOptionProps[]) => {
this.props.onChange({
...this.props.indexPrivilege,
field_security: {
...this.props.indexPrivilege.field_security,
except: deniedFields.map(fromOption),
},
});
};
private getFieldSecurity = (indexPrivilege: RoleIndexPrivilege) => {
const { grant = [], except = [] } = indexPrivilege.field_security || {};
return { grant, except };
};
private isFieldSecurityConfigured = (indexPrivilege: RoleIndexPrivilege) => {
const { grant, except } = this.getFieldSecurity(indexPrivilege);
return except.length > 0 || (grant.length > 0 && !_.isEqual(grant, ['*']));
};
}

View file

@ -112,7 +112,8 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
if (allowFieldLevelSecurity) {
emptyOption.field_security = {
grant: ['*']
grant: ['*'],
except: [],
};
}

View file

@ -7469,8 +7469,6 @@
"xpack.security.management.editRole.elasticSearchPrivileges.runAsPrivilegesTitle": "以权限角色运行",
"xpack.security.management.editRole.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "删除索引权限",
"xpack.security.management.editRole.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "授权的文档查询",
"xpack.security.management.editRole.indexPrivilegeForm.grantedFieldsFormRowHelpText": "如果未授权任何字段,则分配到此角色的用户将无法查看此索引的任何数据。",
"xpack.security.management.editRole.indexPrivilegeForm.grantedFieldsFormRowLabel": "授权字段(可选)",
"xpack.security.management.editRole.indexPrivilegeForm.grantReadPrivilegesLabel": "向特定文档授予读取权限",
"xpack.security.management.editRole.indexPrivilegeForm.indicesFormRowLabel": "索引",
"xpack.security.management.editRole.indexPrivilegeForm.privilegesFormRowLabel": "权限",

View file

@ -334,7 +334,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
return addPriv(userObj.elasticsearch.indices[0].privileges);
})
//clicking the Granted fields and removing the asterix
.then(function () {
.then(async function () {
function addGrantedField(field) {
return field.reduce(function (promise, fieldName) {
@ -350,6 +350,9 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
}
if (userObj.elasticsearch.indices[0].field_security) {
// Toggle FLS switch
await testSubjects.click('restrictFieldsQuery0');
// have to remove the '*'
return find.clickByCssSelector('div[data-test-subj="fieldInput0"] .euiBadge[title="*"]')
.then(function () {