Skip to content

Commit

Permalink
[Spaces] - Space aware privileges UI (#21049)
Browse files Browse the repository at this point in the history
This PR includes enhancements to the Role Management screen to allow users to specify Kibana Privileges on a per-space level.

This PR does not include changes to *enforce* space-aware privileges; this only includes the UI/API changes necessary to support editing a role's privileges.
  • Loading branch information
legrego authored Aug 27, 2018
1 parent 8c42a68 commit 2f08590
Show file tree
Hide file tree
Showing 105 changed files with 3,970 additions and 1,051 deletions.
2 changes: 1 addition & 1 deletion x-pack/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module.exports = function (kibana) {
graph(kibana),
monitoring(kibana),
reporting(kibana),
spaces(kibana),
security(kibana),
searchprofiler(kibana),
ml(kibana),
Expand All @@ -44,7 +45,6 @@ module.exports = function (kibana) {
cloud(kibana),
indexManagement(kibana),
consoleExtensions(kibana),
spaces(kibana),
notifications(kibana),
kueryAutocomplete(kibana)
];
Expand Down
14 changes: 14 additions & 0 deletions x-pack/plugins/security/common/model/index_privilege.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export interface IndexPrivilege {
names: string[];
privileges: string[];
field_security?: {
grant?: string[];
};
query?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

export class Role {
name = null;
cluster = [];
indices = [];
run_as = []; //eslint-disable-line camelcase
applications = [];
import { KibanaPrivilege } from './kibana_privilege';

export interface KibanaApplicationPrivilege {
name: KibanaPrivilege;
}
7 changes: 7 additions & 0 deletions x-pack/plugins/security/common/model/kibana_privilege.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export type KibanaPrivilege = 'none' | 'read' | 'all';
29 changes: 29 additions & 0 deletions x-pack/plugins/security/common/model/role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { IndexPrivilege } from './index_privilege';
import { KibanaPrivilege } from './kibana_privilege';

export interface Role {
name: string;
elasticsearch: {
cluster: string[];
indices: IndexPrivilege[];
run_as: string[];
};
kibana: {
global: KibanaPrivilege[];
space: {
[spaceId: string]: KibanaPrivilege[];
};
};
metadata?: {
[anyKey: string]: any;
};
transient_metadata?: {
[anyKey: string]: any;
};
}
1 change: 1 addition & 0 deletions x-pack/plugins/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const security = (kibana) => new kibana.Plugin({
return {
secureCookies: config.get('xpack.security.secureCookies'),
sessionTimeout: config.get('xpack.security.sessionTimeout'),
enableSpaceAwarePrivileges: config.get('xpack.spaces.enabled'),
};
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { isRoleEnabled, isReservedRole } from './role';
import { isReservedRole, isRoleEnabled } from './role';

describe('role', () => {
describe('isRoleEnabled', () => {
test('should return false if role is explicitly not enabled', () => {
const testRole = {
transient_metadata: {
enabled: false
}
enabled: false,
},
};
expect(isRoleEnabled(testRole)).toBe(false);
});

test('should return true if role is explicitly enabled', () => {
const testRole = {
transient_metadata: {
enabled: true
}
enabled: true,
},
};
expect(isRoleEnabled(testRole)).toBe(true);
});
Expand All @@ -36,17 +36,17 @@ describe('role', () => {
test('should return false if role is explicitly not reserved', () => {
const testRole = {
metadata: {
_reserved: false
}
_reserved: false,
},
};
expect(isReservedRole(testRole)).toBe(false);
});

test('should return true if role is explicitly reserved', () => {
const testRole = {
metadata: {
_reserved: true
}
_reserved: true,
},
};
expect(isReservedRole(testRole)).toBe(true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
*/

import { get } from 'lodash';
import { Role } from '../../common/model/role';

/**
* Returns whether given role is enabled or not
*
* @param role Object Role JSON, as returned by roles API
* @return Boolean true if role is enabled; false otherwise
*/
export function isRoleEnabled(role) {
export function isRoleEnabled(role: Partial<Role>) {
return get(role, 'transient_metadata.enabled', true);
}

Expand All @@ -21,6 +22,6 @@ export function isRoleEnabled(role) {
*
* @param {role} the Role as returned by roles API
*/
export function isReservedRole(role) {
export function isReservedRole(role: Partial<Role>) {
return get(role, 'metadata._reserved', false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IHttpResponse } from 'angular';
import chrome from 'ui/chrome';

const apiBase = chrome.addBasePath(`/api/security/v1/fields`);

export async function getFields($http, query) {
export async function getFields($http: any, query: string): Promise<string[]> {
return await $http
.get(`${apiBase}/${query}`)
.then(response => response.data || []);
.then((response: IHttpResponse<string[]>) => response.data || []);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import chrome from 'ui/chrome';
import { omit } from 'lodash';
import chrome from 'ui/chrome';
import { Role } from '../../../common/model/role';

const apiBase = chrome.addBasePath(`/api/security/role`);

export async function saveRole($http, role) {
export async function saveRole($http: any, role: Role) {
const data = omit(role, 'name', 'transient_metadata', '_unrecognized_applications');
return await $http.put(`${apiBase}/${role.name}`, data);
}

export async function deleteRole($http, name) {
export async function deleteRole($http: any, name: string) {
return await $http.delete(`${apiBase}/${name}`);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ exports[`it renders without blowing up 1`] = `
<EuiLink
color="primary"
onClick={[Function]}
size="s"
type="button"
>
hide
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiLink } from '@elastic/eui';
import { mount, shallow } from 'enzyme';
import React from 'react';
import { shallow, mount } from 'enzyme';
import { CollapsiblePanel } from './collapsible_panel';
import { EuiLink } from '@elastic/eui';

test('it renders without blowing up', () => {
const wrapper = shallow(
<CollapsiblePanel
iconType="logoElasticsearch"
title="Elasticsearch"
>
<CollapsiblePanel iconType="logoElasticsearch" title="Elasticsearch">
<p>child</p>
</CollapsiblePanel>
);
Expand All @@ -24,10 +21,7 @@ test('it renders without blowing up', () => {

test('it renders children by default', () => {
const wrapper = mount(
<CollapsiblePanel
iconType="logoElasticsearch"
title="Elasticsearch"
>
<CollapsiblePanel iconType="logoElasticsearch" title="Elasticsearch">
<p className="child">child 1</p>
<p className="child">child 2</p>
</CollapsiblePanel>
Expand All @@ -39,10 +33,7 @@ test('it renders children by default', () => {

test('it hides children when the "hide" link is clicked', () => {
const wrapper = mount(
<CollapsiblePanel
iconType="logoElasticsearch"
title="Elasticsearch"
>
<CollapsiblePanel iconType="logoElasticsearch" title="Elasticsearch">
<p className="child">child 1</p>
<p className="child">child 2</p>
</CollapsiblePanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,33 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import './collapsible_panel.less';
import {
EuiPanel,
EuiLink,
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiIcon,
EuiLink,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import React, { Component, Fragment } from 'react';
import './collapsible_panel.less';

export class CollapsiblePanel extends Component {
static propTypes = {
iconType: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
}
interface Props {
iconType: string | any;
title: string;
}

state = {
collapsed: false
}
interface State {
collapsed: boolean;
}

render() {
export class CollapsiblePanel extends Component<Props, State> {
public state = {
collapsed: false,
};

public render() {
return (
<EuiPanel>
{this.getTitle()}
Expand All @@ -36,24 +39,30 @@ export class CollapsiblePanel extends Component {
);
}

getTitle = () => {
public getTitle = () => {
return (
// @ts-ignore
<EuiFlexGroup alignItems={'baseline'} gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiTitle>
<h2>
<EuiIcon type={this.props.iconType} size={'xl'} className={'collapsiblePanel__logo'} /> {this.props.title}
<EuiIcon
type={this.props.iconType}
size={'xl'}
className={'collapsiblePanel__logo'}
/>{' '}
{this.props.title}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink size={'s'} onClick={this.toggleCollapsed}>{this.state.collapsed ? 'show' : 'hide'}</EuiLink>
<EuiLink onClick={this.toggleCollapsed}>{this.state.collapsed ? 'show' : 'hide'}</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
);
};

getForm = () => {
public getForm = () => {
if (this.state.collapsed) {
return null;
}
Expand All @@ -64,11 +73,11 @@ export class CollapsiblePanel extends Component {
{this.props.children}
</Fragment>
);
}
};

toggleCollapsed = () => {
public toggleCollapsed = () => {
this.setState({
collapsed: !this.state.collapsed
collapsed: !this.state.collapsed,
});
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import {
EuiButton,
EuiButtonEmpty,
// @ts-ignore
EuiConfirmModal,
} from '@elastic/eui';
import { mount, shallow } from 'enzyme';
import React from 'react';
import { DeleteRoleButton } from './delete_role_button';
import {
shallow,
mount
} from 'enzyme';

test('it renders without crashing', () => {
const deleteHandler = jest.fn();
const wrapper = shallow(<DeleteRoleButton canDelete={true} onDelete={deleteHandler} />);
expect(wrapper.find(EuiButton)).toHaveLength(1);
expect(wrapper.find(EuiButtonEmpty)).toHaveLength(1);
expect(deleteHandler).toHaveBeenCalledTimes(0);
});

test('it shows a confirmation dialog when clicked', () => {
const deleteHandler = jest.fn();
const wrapper = mount(<DeleteRoleButton canDelete={true} onDelete={deleteHandler} />);

wrapper.find(EuiButton).simulate('click');
wrapper.find(EuiButtonEmpty).simulate('click');

expect(wrapper.find(EuiConfirmModal)).toHaveLength(1);

Expand Down
Loading

0 comments on commit 2f08590

Please sign in to comment.