mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* Add support for deprecated roles * address PR feedback * remove unused import * copy edits * fix snapshots Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
95cf5f9c98
commit
4e624b2b2f
55 changed files with 1477 additions and 304 deletions
|
@ -117,7 +117,8 @@ cluster alert notifications from Monitoring.
|
|||
==== Dashboard
|
||||
|
||||
[horizontal]
|
||||
`xpackDashboardMode:roles`:: The roles that belong to <<xpack-dashboard-only-mode, dashboard only mode>>.
|
||||
`xpackDashboardMode:roles`:: **Deprecated. Use <<kibana-feature-privileges,feature privileges>> instead.**
|
||||
The roles that belong to <<xpack-dashboard-only-mode, dashboard only mode>>.
|
||||
|
||||
[float]
|
||||
[[kibana-discover-settings]]
|
||||
|
|
|
@ -120,6 +120,7 @@ export class DocLinksService {
|
|||
},
|
||||
management: {
|
||||
kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`,
|
||||
dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -33,6 +33,15 @@ export function dashboardMode(kibana) {
|
|||
),
|
||||
value: ['kibana_dashboard_only_user'],
|
||||
category: ['dashboard'],
|
||||
deprecation: {
|
||||
message: i18n.translate(
|
||||
'xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDeprecation',
|
||||
{
|
||||
defaultMessage: 'This setting is deprecated and will be removed in Kibana 8.0.',
|
||||
}
|
||||
),
|
||||
docLinksKey: 'dashboardSettings',
|
||||
},
|
||||
},
|
||||
},
|
||||
app: {
|
||||
|
|
|
@ -15,10 +15,12 @@ export {
|
|||
RoleIndexPrivilege,
|
||||
RoleKibanaPrivilege,
|
||||
copyRole,
|
||||
isReadOnlyRole,
|
||||
isReservedRole,
|
||||
isRoleDeprecated,
|
||||
isRoleReadOnly,
|
||||
isRoleReserved,
|
||||
isRoleEnabled,
|
||||
prepareRoleClone,
|
||||
getExtendedRoleDeprecationNotice,
|
||||
} from './role';
|
||||
export { KibanaPrivileges } from './kibana_privileges';
|
||||
export {
|
||||
|
|
|
@ -4,7 +4,16 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Role, isReadOnlyRole, isReservedRole, isRoleEnabled, copyRole, prepareRoleClone } from '.';
|
||||
import {
|
||||
Role,
|
||||
isRoleEnabled,
|
||||
isRoleReserved,
|
||||
isRoleDeprecated,
|
||||
isRoleReadOnly,
|
||||
copyRole,
|
||||
prepareRoleClone,
|
||||
getExtendedRoleDeprecationNotice,
|
||||
} from '../../common/model';
|
||||
|
||||
describe('role', () => {
|
||||
describe('isRoleEnabled', () => {
|
||||
|
@ -32,14 +41,14 @@ describe('role', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('isReservedRole', () => {
|
||||
describe('isRoleReserved', () => {
|
||||
test('should return false if role is explicitly not reserved', () => {
|
||||
const testRole = {
|
||||
metadata: {
|
||||
_reserved: false,
|
||||
},
|
||||
};
|
||||
expect(isReservedRole(testRole)).toBe(false);
|
||||
expect(isRoleReserved(testRole)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true if role is explicitly reserved', () => {
|
||||
|
@ -48,30 +57,74 @@ describe('role', () => {
|
|||
_reserved: true,
|
||||
},
|
||||
};
|
||||
expect(isReservedRole(testRole)).toBe(true);
|
||||
expect(isRoleReserved(testRole)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if role is NOT explicitly reserved or not reserved', () => {
|
||||
const testRole = {};
|
||||
expect(isReservedRole(testRole)).toBe(false);
|
||||
expect(isRoleReserved(testRole)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isReadOnlyRole', () => {
|
||||
describe('isRoleDeprecated', () => {
|
||||
test('should return false if role is explicitly not deprecated', () => {
|
||||
const testRole = {
|
||||
metadata: {
|
||||
_deprecated: false,
|
||||
},
|
||||
};
|
||||
expect(isRoleDeprecated(testRole)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true if role is explicitly deprecated', () => {
|
||||
const testRole = {
|
||||
metadata: {
|
||||
_deprecated: true,
|
||||
},
|
||||
};
|
||||
expect(isRoleDeprecated(testRole)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if role is NOT explicitly deprecated or not deprecated', () => {
|
||||
const testRole = {};
|
||||
expect(isRoleDeprecated(testRole)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExtendedRoleDeprecationNotice', () => {
|
||||
test('advises not to use the deprecated role', () => {
|
||||
const testRole = { name: 'test-role' };
|
||||
expect(getExtendedRoleDeprecationNotice(testRole)).toMatchInlineSnapshot(
|
||||
`"The test-role role is deprecated. "`
|
||||
);
|
||||
});
|
||||
|
||||
test('includes the deprecation reason when provided', () => {
|
||||
const testRole = {
|
||||
name: 'test-role',
|
||||
metadata: { _deprecated_reason: "We just don't like this role anymore" },
|
||||
};
|
||||
expect(getExtendedRoleDeprecationNotice(testRole)).toMatchInlineSnapshot(
|
||||
`"The test-role role is deprecated. We just don't like this role anymore"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRoleReadOnly', () => {
|
||||
test('returns true for reserved roles', () => {
|
||||
const testRole = {
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
};
|
||||
expect(isReadOnlyRole(testRole)).toBe(true);
|
||||
expect(isRoleReadOnly(testRole)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true for roles with transform errors', () => {
|
||||
const testRole = {
|
||||
_transform_error: ['kibana'],
|
||||
};
|
||||
expect(isReadOnlyRole(testRole)).toBe(true);
|
||||
expect(isRoleReadOnly(testRole)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for disabled roles', () => {
|
||||
|
@ -80,12 +133,12 @@ describe('role', () => {
|
|||
enabled: false,
|
||||
},
|
||||
};
|
||||
expect(isReadOnlyRole(testRole)).toBe(false);
|
||||
expect(isRoleReadOnly(testRole)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for all other roles', () => {
|
||||
const testRole = {};
|
||||
expect(isReadOnlyRole(testRole)).toBe(false);
|
||||
expect(isRoleReadOnly(testRole)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FeaturesPrivileges } from './features_privileges';
|
||||
|
||||
export interface RoleIndexPrivilege {
|
||||
|
@ -57,17 +58,41 @@ export function isRoleEnabled(role: Partial<Role>) {
|
|||
*
|
||||
* @param role Role as returned by roles API
|
||||
*/
|
||||
export function isReservedRole(role: Partial<Role>) {
|
||||
export function isRoleReserved(role: Partial<Role>) {
|
||||
return (role.metadata?._reserved as boolean) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether given role is deprecated or not.
|
||||
*
|
||||
* @param {role} the Role as returned by roles API
|
||||
*/
|
||||
export function isRoleDeprecated(role: Partial<Role>) {
|
||||
return role.metadata?._deprecated ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extended deprecation notice for the provided role.
|
||||
*
|
||||
* @param role the Role as returned by roles API
|
||||
*/
|
||||
export function getExtendedRoleDeprecationNotice(role: Partial<Role>) {
|
||||
return i18n.translate('xpack.security.common.extendedRoleDeprecationNotice', {
|
||||
defaultMessage: `The {roleName} role is deprecated. {reason}`,
|
||||
values: {
|
||||
roleName: role.name,
|
||||
reason: getRoleDeprecatedReason(role),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether given role is editable through the UI or not.
|
||||
*
|
||||
* @param role the Role as returned by roles API
|
||||
*/
|
||||
export function isReadOnlyRole(role: Partial<Role>): boolean {
|
||||
return isReservedRole(role) || (role._transform_error?.length ?? 0) > 0;
|
||||
export function isRoleReadOnly(role: Partial<Role>): boolean {
|
||||
return isRoleReserved(role) || (role._transform_error?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,3 +116,12 @@ export function prepareRoleClone(role: Role): Role {
|
|||
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reason this role is deprecated.
|
||||
*
|
||||
* @param role the Role as returned by roles API
|
||||
*/
|
||||
function getRoleDeprecatedReason(role: Partial<Role>) {
|
||||
return role.metadata?._deprecated_reason ?? '';
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ describe('<AccountManagementPage>', () => {
|
|||
<AccountManagementPage
|
||||
authc={getSecuritySetupMock({ currentUser: user }).authc}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
apiClient={userAPIClientMock.create()}
|
||||
userAPIClient={userAPIClientMock.create()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -70,7 +70,7 @@ describe('<AccountManagementPage>', () => {
|
|||
<AccountManagementPage
|
||||
authc={getSecuritySetupMock({ currentUser: user }).authc}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
apiClient={userAPIClientMock.create()}
|
||||
userAPIClient={userAPIClientMock.create()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -88,7 +88,7 @@ describe('<AccountManagementPage>', () => {
|
|||
<AccountManagementPage
|
||||
authc={getSecuritySetupMock({ currentUser: user }).authc}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
apiClient={userAPIClientMock.create()}
|
||||
userAPIClient={userAPIClientMock.create()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -106,7 +106,7 @@ describe('<AccountManagementPage>', () => {
|
|||
<AccountManagementPage
|
||||
authc={getSecuritySetupMock({ currentUser: user }).authc}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
apiClient={userAPIClientMock.create()}
|
||||
userAPIClient={userAPIClientMock.create()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -125,7 +125,7 @@ describe('<AccountManagementPage>', () => {
|
|||
<AccountManagementPage
|
||||
authc={getSecuritySetupMock({ currentUser: user }).authc}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
apiClient={userAPIClientMock.create()}
|
||||
userAPIClient={userAPIClientMock.create()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -14,11 +14,11 @@ import { PersonalInfo } from './personal_info';
|
|||
|
||||
interface Props {
|
||||
authc: AuthenticationServiceSetup;
|
||||
apiClient: PublicMethodsOf<UserAPIClient>;
|
||||
userAPIClient: PublicMethodsOf<UserAPIClient>;
|
||||
notifications: NotificationsStart;
|
||||
}
|
||||
|
||||
export const AccountManagementPage = ({ apiClient, authc, notifications }: Props) => {
|
||||
export const AccountManagementPage = ({ userAPIClient, authc, notifications }: Props) => {
|
||||
const [currentUser, setCurrentUser] = useState<AuthenticatedUser | null>(null);
|
||||
useEffect(() => {
|
||||
authc.getCurrentUser().then(setCurrentUser);
|
||||
|
@ -40,7 +40,11 @@ export const AccountManagementPage = ({ apiClient, authc, notifications }: Props
|
|||
|
||||
<PersonalInfo user={currentUser} />
|
||||
|
||||
<ChangePassword user={currentUser} apiClient={apiClient} notifications={notifications} />
|
||||
<ChangePassword
|
||||
user={currentUser}
|
||||
userAPIClient={userAPIClient}
|
||||
notifications={notifications}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
|
|
|
@ -13,7 +13,7 @@ import { ChangePasswordForm } from '../../management/users/components/change_pas
|
|||
|
||||
interface Props {
|
||||
user: AuthenticatedUser;
|
||||
apiClient: PublicMethodsOf<UserAPIClient>;
|
||||
userAPIClient: PublicMethodsOf<UserAPIClient>;
|
||||
notifications: NotificationsSetup;
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,7 @@ export class ChangePassword extends Component<Props, {}> {
|
|||
<ChangePasswordForm
|
||||
user={this.props.user}
|
||||
isUserChangingOwnPassword={true}
|
||||
apiClient={this.props.apiClient}
|
||||
userAPIClient={this.props.userAPIClient}
|
||||
notifications={this.props.notifications}
|
||||
/>
|
||||
</EuiDescribedFormGroup>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiBadge, EuiToolTipProps } from '@elastic/eui';
|
||||
import { OptionalToolTip } from './optional_tooltip';
|
||||
|
||||
interface Props {
|
||||
'data-test-subj'?: string;
|
||||
tooltipContent?: EuiToolTipProps['content'];
|
||||
}
|
||||
|
||||
export const DeprecatedBadge = (props: Props) => {
|
||||
return (
|
||||
<OptionalToolTip tooltipContent={props.tooltipContent}>
|
||||
<EuiBadge data-test-subj={props['data-test-subj']} color="warning">
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.deprecatedBadge"
|
||||
defaultMessage="Deprecated"
|
||||
/>
|
||||
</EuiBadge>
|
||||
</OptionalToolTip>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiBadge, EuiToolTipProps } from '@elastic/eui';
|
||||
import { OptionalToolTip } from './optional_tooltip';
|
||||
|
||||
interface Props {
|
||||
'data-test-subj'?: string;
|
||||
tooltipContent?: EuiToolTipProps['content'];
|
||||
}
|
||||
|
||||
export const DisabledBadge = (props: Props) => {
|
||||
return (
|
||||
<OptionalToolTip tooltipContent={props.tooltipContent}>
|
||||
<EuiBadge data-test-subj={props['data-test-subj']} color="hollow">
|
||||
<FormattedMessage id="xpack.security.management.disabledBadge" defaultMessage="Disabled" />
|
||||
</EuiBadge>
|
||||
</OptionalToolTip>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiBadge, EuiToolTipProps } from '@elastic/eui';
|
||||
import { OptionalToolTip } from './optional_tooltip';
|
||||
|
||||
interface Props {
|
||||
'data-test-subj'?: string;
|
||||
tooltipContent?: EuiToolTipProps['content'];
|
||||
}
|
||||
|
||||
export const EnabledBadge = (props: Props) => {
|
||||
return (
|
||||
<OptionalToolTip tooltipContent={props.tooltipContent}>
|
||||
<EuiBadge data-test-subj={props['data-test-subj']} color="secondary">
|
||||
<FormattedMessage id="xpack.security.management.enabledBadge" defaultMessage="Enabled" />
|
||||
</EuiBadge>
|
||||
</OptionalToolTip>
|
||||
);
|
||||
};
|
10
x-pack/plugins/security/public/management/badges/index.ts
Normal file
10
x-pack/plugins/security/public/management/badges/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { DeprecatedBadge } from './deprecated_badge';
|
||||
export { DisabledBadge } from './disabled_badge';
|
||||
export { EnabledBadge } from './enabled_badge';
|
||||
export { ReservedBadge } from './reserved_badge';
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 React, { ReactElement } from 'react';
|
||||
import { EuiToolTipProps, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
children: ReactElement<any>;
|
||||
tooltipContent?: EuiToolTipProps['content'];
|
||||
}
|
||||
export const OptionalToolTip = (props: Props) => {
|
||||
if (props.tooltipContent) {
|
||||
return <EuiToolTip content={props.tooltipContent}>{props.children}</EuiToolTip>;
|
||||
}
|
||||
return props.children;
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiBadge, EuiToolTipProps } from '@elastic/eui';
|
||||
import { OptionalToolTip } from './optional_tooltip';
|
||||
|
||||
interface Props {
|
||||
'data-test-subj'?: string;
|
||||
tooltipContent?: EuiToolTipProps['content'];
|
||||
}
|
||||
|
||||
export const ReservedBadge = (props: Props) => {
|
||||
return (
|
||||
<OptionalToolTip tooltipContent={props.tooltipContent}>
|
||||
<EuiBadge data-test-subj={props['data-test-subj']} color="primary">
|
||||
<FormattedMessage id="xpack.security.management.reservedBadge" defaultMessage="Reserved" />
|
||||
</EuiBadge>
|
||||
</OptionalToolTip>
|
||||
);
|
||||
};
|
|
@ -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 { RoleComboBox } from './role_combo_box';
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import { RoleComboBox } from '.';
|
||||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
|
||||
describe('RoleComboBox', () => {
|
||||
it('renders the provided list of roles via EuiComboBox options', () => {
|
||||
const availableRoles = [
|
||||
{
|
||||
name: 'role-1',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [],
|
||||
metadata: {},
|
||||
},
|
||||
{
|
||||
name: 'role-2',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [],
|
||||
metadata: {},
|
||||
},
|
||||
];
|
||||
const wrapper = mountWithIntl(
|
||||
<RoleComboBox availableRoles={availableRoles} selectedRoleNames={[]} onChange={jest.fn()} />
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"color": "default",
|
||||
"data-test-subj": "roleOption-role-1",
|
||||
"label": "role-1",
|
||||
"value": Object {
|
||||
"isDeprecated": false,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "default",
|
||||
"data-test-subj": "roleOption-role-2",
|
||||
"label": "role-2",
|
||||
"value": Object {
|
||||
"isDeprecated": false,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders deprecated roles as such', () => {
|
||||
const availableRoles = [
|
||||
{
|
||||
name: 'role-1',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [],
|
||||
metadata: { _deprecated: true },
|
||||
},
|
||||
];
|
||||
const wrapper = mountWithIntl(
|
||||
<RoleComboBox availableRoles={availableRoles} selectedRoleNames={[]} onChange={jest.fn()} />
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"color": "warning",
|
||||
"data-test-subj": "roleOption-role-1",
|
||||
"label": "role-1",
|
||||
"value": Object {
|
||||
"isDeprecated": true,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders the selected role names in the expanded list, coded according to deprecated status', () => {
|
||||
const availableRoles = [
|
||||
{
|
||||
name: 'role-1',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [],
|
||||
metadata: {},
|
||||
},
|
||||
{
|
||||
name: 'role-2',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [],
|
||||
metadata: {},
|
||||
},
|
||||
];
|
||||
const wrapper = mountWithIntl(
|
||||
<div>
|
||||
<RoleComboBox availableRoles={availableRoles} selectedRoleNames={[]} onChange={jest.fn()} />
|
||||
</div>
|
||||
);
|
||||
|
||||
findTestSubject(wrapper, 'comboBoxToggleListButton').simulate('click');
|
||||
|
||||
wrapper.find(EuiComboBox).setState({ isListOpen: true });
|
||||
|
||||
expect(findTestSubject(wrapper, 'rolesDropdown-renderOption')).toMatchInlineSnapshot(`null`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { Role, isRoleDeprecated } from '../../../common/model';
|
||||
import { RoleComboBoxOption } from './role_combo_box_option';
|
||||
|
||||
interface Props {
|
||||
availableRoles: Role[];
|
||||
selectedRoleNames: string[];
|
||||
onChange: (selectedRoleNames: string[]) => void;
|
||||
placeholder?: string;
|
||||
isLoading?: boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const RoleComboBox = (props: Props) => {
|
||||
const onRolesChange = (selectedItems: Array<{ label: string }>) => {
|
||||
props.onChange(selectedItems.map(item => item.label));
|
||||
};
|
||||
|
||||
const roleNameToOption = (roleName: string) => {
|
||||
const roleDefinition = props.availableRoles.find(role => role.name === roleName);
|
||||
const isDeprecated: boolean = (roleDefinition && isRoleDeprecated(roleDefinition)) ?? false;
|
||||
return {
|
||||
color: isDeprecated ? 'warning' : 'default',
|
||||
'data-test-subj': `roleOption-${roleName}`,
|
||||
label: roleName,
|
||||
value: {
|
||||
isDeprecated,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const options = props.availableRoles.map(role => roleNameToOption(role.name));
|
||||
|
||||
const selectedOptions = props.selectedRoleNames.map(roleNameToOption);
|
||||
|
||||
return (
|
||||
<EuiComboBox
|
||||
data-test-subj="rolesDropdown"
|
||||
placeholder={
|
||||
props.placeholder ||
|
||||
i18n.translate('xpack.security.management.users.editUser.addRolesPlaceholder', {
|
||||
defaultMessage: 'Add roles',
|
||||
})
|
||||
}
|
||||
onChange={onRolesChange}
|
||||
isLoading={props.isLoading}
|
||||
isDisabled={props.isDisabled}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
renderOption={option => <RoleComboBoxOption option={option} />}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { RoleComboBoxOption } from './role_combo_box_option';
|
||||
|
||||
describe('RoleComboBoxOption', () => {
|
||||
it('renders a regular role correctly', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<RoleComboBoxOption
|
||||
option={{
|
||||
color: 'default',
|
||||
label: 'role-1',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchInlineSnapshot(`
|
||||
<EuiText
|
||||
color="default"
|
||||
data-test-subj="rolesDropdown-renderOption"
|
||||
>
|
||||
role-1
|
||||
|
||||
</EuiText>
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders a deprecated role correctly', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<RoleComboBoxOption
|
||||
option={{
|
||||
color: 'warning',
|
||||
label: 'role-1',
|
||||
value: {
|
||||
isDeprecated: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchInlineSnapshot(`
|
||||
<EuiText
|
||||
color="warning"
|
||||
data-test-subj="rolesDropdown-renderOption"
|
||||
>
|
||||
role-1
|
||||
|
||||
(deprecated)
|
||||
</EuiText>
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiComboBoxOptionProps, EuiText } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
option: EuiComboBoxOptionProps<{ isDeprecated: boolean }>;
|
||||
}
|
||||
|
||||
export const RoleComboBoxOption = ({ option }: Props) => {
|
||||
const isDeprecated = option.value?.isDeprecated ?? false;
|
||||
const deprecatedLabel = i18n.translate(
|
||||
'xpack.security.management.users.editUser.deprecatedRoleText',
|
||||
{
|
||||
defaultMessage: '(deprecated)',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiText color={option.color as any} data-test-subj="rolesDropdown-renderOption">
|
||||
{option.label} {isDeprecated ? deprecatedLabel : ''}
|
||||
</EuiText>
|
||||
);
|
||||
};
|
|
@ -17,7 +17,6 @@ import { EditRoleMappingPage } from '.';
|
|||
import { NoCompatibleRealms, SectionLoading, PermissionDenied } from '../components';
|
||||
import { VisualRuleEditor } from './rule_editor_panel/visual_rule_editor';
|
||||
import { JSONRuleEditor } from './rule_editor_panel/json_rule_editor';
|
||||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { RolesAPIClient } from '../../roles';
|
||||
import { Role } from '../../../../common/model';
|
||||
import { DocumentationLinksService } from '../documentation_links';
|
||||
|
@ -25,6 +24,7 @@ import { DocumentationLinksService } from '../documentation_links';
|
|||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock';
|
||||
import { rolesAPIClientMock } from '../../roles/roles_api_client.mock';
|
||||
import { RoleComboBox } from '../../role_combo_box';
|
||||
|
||||
describe('EditRoleMappingPage', () => {
|
||||
let rolesAPI: PublicMethodsOf<RolesAPIClient>;
|
||||
|
@ -33,6 +33,7 @@ describe('EditRoleMappingPage', () => {
|
|||
(rolesAPI as jest.Mocked<RolesAPIClient>).getRoles.mockResolvedValue([
|
||||
{ name: 'foo_role' },
|
||||
{ name: 'bar role' },
|
||||
{ name: 'some-deprecated-role', metadata: { _deprecated: true } },
|
||||
] as Role[]);
|
||||
});
|
||||
|
||||
|
@ -63,10 +64,10 @@ describe('EditRoleMappingPage', () => {
|
|||
target: { value: 'my-role-mapping' },
|
||||
});
|
||||
|
||||
(wrapper
|
||||
.find(EuiComboBox)
|
||||
.filter('[data-test-subj="roleMappingFormRoleComboBox"]')
|
||||
.props() as any).onChange([{ label: 'foo_role' }]);
|
||||
wrapper
|
||||
.find(RoleComboBox)
|
||||
.props()
|
||||
.onChange(['foo_role']);
|
||||
|
||||
findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click');
|
||||
|
||||
|
@ -126,10 +127,10 @@ describe('EditRoleMappingPage', () => {
|
|||
|
||||
findTestSubject(wrapper, 'switchToRolesButton').simulate('click');
|
||||
|
||||
(wrapper
|
||||
.find(EuiComboBox)
|
||||
.filter('[data-test-subj="roleMappingFormRoleComboBox"]')
|
||||
.props() as any).onChange([{ label: 'foo_role' }]);
|
||||
wrapper
|
||||
.find(RoleComboBox)
|
||||
.props()
|
||||
.onChange(['foo_role']);
|
||||
|
||||
findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click');
|
||||
wrapper.find('button[id="addRuleOption"]').simulate('click');
|
||||
|
@ -207,6 +208,42 @@ describe('EditRoleMappingPage', () => {
|
|||
expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a message when editing a mapping with deprecated roles assigned', async () => {
|
||||
const roleMappingsAPI = roleMappingsAPIClientMock.create();
|
||||
roleMappingsAPI.getRoleMapping.mockResolvedValue({
|
||||
name: 'foo',
|
||||
roles: ['some-deprecated-role'],
|
||||
enabled: true,
|
||||
rules: {
|
||||
field: { username: '*' },
|
||||
},
|
||||
});
|
||||
roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({
|
||||
canManageRoleMappings: true,
|
||||
hasCompatibleRealms: true,
|
||||
canUseInlineScripts: true,
|
||||
canUseStoredScripts: true,
|
||||
});
|
||||
|
||||
const { docLinks, notifications } = coreMock.createStart();
|
||||
const wrapper = mountWithIntl(
|
||||
<EditRoleMappingPage
|
||||
name={'foo'}
|
||||
roleMappingsAPI={roleMappingsAPI}
|
||||
rolesAPIClient={rolesAPI}
|
||||
notifications={notifications}
|
||||
docLinks={new DocumentationLinksService(docLinks)}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(findTestSubject(wrapper, 'deprecatedRolesAssigned')).toHaveLength(0);
|
||||
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
|
||||
expect(findTestSubject(wrapper, 'deprecatedRolesAssigned')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => {
|
||||
const roleMappingsAPI = roleMappingsAPIClientMock.create();
|
||||
roleMappingsAPI.getRoleMapping.mockResolvedValue({
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
EuiIcon,
|
||||
EuiSwitch,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { RoleMapping } from '../../../../../common/model';
|
||||
import { RolesAPIClient } from '../../../roles';
|
||||
|
@ -276,12 +277,12 @@ export class MappingInfoPanel extends Component<Props, State> {
|
|||
>
|
||||
<EuiSwitch
|
||||
name={'enabled'}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRoleMapping.roleMappingEnabledLabel"
|
||||
defaultMessage="Enable mapping"
|
||||
/>
|
||||
}
|
||||
label={i18n.translate(
|
||||
'xpack.security.management.editRoleMapping.roleMappingEnabledLabel',
|
||||
{
|
||||
defaultMessage: 'Enable mapping',
|
||||
}
|
||||
)}
|
||||
showLabel={false}
|
||||
data-test-subj="roleMappingsEnabledSwitch"
|
||||
checked={this.props.roleMapping.enabled}
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
import React, { Fragment } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiComboBox, EuiFormRow, EuiHorizontalRule } from '@elastic/eui';
|
||||
import { RoleMapping, Role } from '../../../../../common/model';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFormRow, EuiHorizontalRule } from '@elastic/eui';
|
||||
import { RoleMapping, Role, isRoleDeprecated } from '../../../../../common/model';
|
||||
import { RolesAPIClient } from '../../../roles';
|
||||
import { AddRoleTemplateButton } from './add_role_template_button';
|
||||
import { RoleTemplateEditor } from './role_template_editor';
|
||||
import { RoleComboBox } from '../../../role_combo_box';
|
||||
|
||||
interface Props {
|
||||
rolesAPIClient: PublicMethodsOf<RolesAPIClient>;
|
||||
|
@ -40,7 +42,7 @@ export class RoleSelector extends React.Component<Props, State> {
|
|||
public render() {
|
||||
const { mode } = this.props;
|
||||
return (
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiFormRow fullWidth helpText={this.getHelpText()}>
|
||||
{mode === 'roles' ? this.getRoleComboBox() : this.getRoleTemplates()}
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
@ -49,19 +51,18 @@ export class RoleSelector extends React.Component<Props, State> {
|
|||
private getRoleComboBox = () => {
|
||||
const { roles = [] } = this.props.roleMapping;
|
||||
return (
|
||||
<EuiComboBox
|
||||
data-test-subj="roleMappingFormRoleComboBox"
|
||||
<RoleComboBox
|
||||
placeholder={i18n.translate(
|
||||
'xpack.security.management.editRoleMapping.selectRolesPlaceholder',
|
||||
{ defaultMessage: 'Select one or more roles' }
|
||||
)}
|
||||
isLoading={this.state.roles.length === 0}
|
||||
options={this.state.roles.map(r => ({ label: r.name }))}
|
||||
selectedOptions={roles!.map(r => ({ label: r }))}
|
||||
onChange={selectedOptions => {
|
||||
availableRoles={this.state.roles}
|
||||
selectedRoleNames={roles}
|
||||
onChange={selectedRoles => {
|
||||
this.props.onChange({
|
||||
...this.props.roleMapping,
|
||||
roles: selectedOptions.map(so => so.label),
|
||||
roles: selectedRoles,
|
||||
role_templates: [],
|
||||
});
|
||||
}}
|
||||
|
@ -130,4 +131,25 @@ export class RoleSelector extends React.Component<Props, State> {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private getHelpText = () => {
|
||||
if (this.props.mode === 'roles' && this.hasDeprecatedRolesAssigned()) {
|
||||
return (
|
||||
<span data-test-subj="deprecatedRolesAssigned">
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRoleMapping.deprecatedRolesAssigned"
|
||||
defaultMessage="This mapping is assigned a deprecated role. Please migrate to a supported role."
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private hasDeprecatedRolesAssigned = () => {
|
||||
return (
|
||||
this.props.roleMapping.roles?.some(r =>
|
||||
this.state.roles.some(role => role.name === r && isRoleDeprecated(role))
|
||||
) ?? false
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { DocumentationLinksService } from '../documentation_links';
|
|||
|
||||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock';
|
||||
import { rolesAPIClientMock } from '../../roles/index.mock';
|
||||
|
||||
describe('RoleMappingsGridPage', () => {
|
||||
it('renders an empty prompt when no role mappings exist', async () => {
|
||||
|
@ -29,6 +30,7 @@ describe('RoleMappingsGridPage', () => {
|
|||
const { docLinks, notifications } = coreMock.createStart();
|
||||
const wrapper = mountWithIntl(
|
||||
<RoleMappingsGridPage
|
||||
rolesAPIClient={rolesAPIClientMock.create()}
|
||||
roleMappingsAPI={roleMappingsAPI}
|
||||
notifications={notifications}
|
||||
docLinks={new DocumentationLinksService(docLinks)}
|
||||
|
@ -55,6 +57,7 @@ describe('RoleMappingsGridPage', () => {
|
|||
const { docLinks, notifications } = coreMock.createStart();
|
||||
const wrapper = mountWithIntl(
|
||||
<RoleMappingsGridPage
|
||||
rolesAPIClient={rolesAPIClientMock.create()}
|
||||
roleMappingsAPI={roleMappingsAPI}
|
||||
notifications={notifications}
|
||||
docLinks={new DocumentationLinksService(docLinks)}
|
||||
|
@ -89,6 +92,7 @@ describe('RoleMappingsGridPage', () => {
|
|||
const { docLinks, notifications } = coreMock.createStart();
|
||||
const wrapper = mountWithIntl(
|
||||
<RoleMappingsGridPage
|
||||
rolesAPIClient={rolesAPIClientMock.create()}
|
||||
roleMappingsAPI={roleMappingsAPI}
|
||||
notifications={notifications}
|
||||
docLinks={new DocumentationLinksService(docLinks)}
|
||||
|
@ -104,7 +108,7 @@ describe('RoleMappingsGridPage', () => {
|
|||
expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders links to mapped roles', async () => {
|
||||
it('renders links to mapped roles, even if the roles API call returns nothing', async () => {
|
||||
const roleMappingsAPI = roleMappingsAPIClientMock.create();
|
||||
roleMappingsAPI.getRoleMappings.mockResolvedValue([
|
||||
{
|
||||
|
@ -122,6 +126,7 @@ describe('RoleMappingsGridPage', () => {
|
|||
const { docLinks, notifications } = coreMock.createStart();
|
||||
const wrapper = mountWithIntl(
|
||||
<RoleMappingsGridPage
|
||||
rolesAPIClient={rolesAPIClientMock.create()}
|
||||
roleMappingsAPI={roleMappingsAPI}
|
||||
notifications={notifications}
|
||||
docLinks={new DocumentationLinksService(docLinks)}
|
||||
|
@ -155,6 +160,7 @@ describe('RoleMappingsGridPage', () => {
|
|||
const { docLinks, notifications } = coreMock.createStart();
|
||||
const wrapper = mountWithIntl(
|
||||
<RoleMappingsGridPage
|
||||
rolesAPIClient={rolesAPIClientMock.create()}
|
||||
roleMappingsAPI={roleMappingsAPI}
|
||||
notifications={notifications}
|
||||
docLinks={new DocumentationLinksService(docLinks)}
|
||||
|
@ -192,6 +198,7 @@ describe('RoleMappingsGridPage', () => {
|
|||
const { docLinks, notifications } = coreMock.createStart();
|
||||
const wrapper = mountWithIntl(
|
||||
<RoleMappingsGridPage
|
||||
rolesAPIClient={rolesAPIClientMock.create()}
|
||||
roleMappingsAPI={roleMappingsAPI}
|
||||
notifications={notifications}
|
||||
docLinks={new DocumentationLinksService(docLinks)}
|
||||
|
@ -216,4 +223,70 @@ describe('RoleMappingsGridPage', () => {
|
|||
// Expect an additional API call to refresh the grid
|
||||
expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('renders a warning when a mapping is assigned a deprecated role', async () => {
|
||||
const roleMappingsAPI = roleMappingsAPIClientMock.create();
|
||||
roleMappingsAPI.getRoleMappings.mockResolvedValue([
|
||||
{
|
||||
name: 'some-realm',
|
||||
enabled: true,
|
||||
roles: ['superuser', 'kibana_user'],
|
||||
rules: { field: { username: '*' } },
|
||||
},
|
||||
]);
|
||||
roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({
|
||||
canManageRoleMappings: true,
|
||||
hasCompatibleRealms: true,
|
||||
});
|
||||
roleMappingsAPI.deleteRoleMappings.mockResolvedValue([
|
||||
{
|
||||
name: 'some-realm',
|
||||
success: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const roleAPIClient = rolesAPIClientMock.create();
|
||||
roleAPIClient.getRoles.mockResolvedValue([
|
||||
{
|
||||
name: 'kibana_user',
|
||||
metadata: {
|
||||
_deprecated: true,
|
||||
_deprecated_reason: `I don't like you.`,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { docLinks, notifications } = coreMock.createStart();
|
||||
const wrapper = mountWithIntl(
|
||||
<RoleMappingsGridPage
|
||||
rolesAPIClient={roleAPIClient}
|
||||
roleMappingsAPI={roleMappingsAPI}
|
||||
notifications={notifications}
|
||||
docLinks={new DocumentationLinksService(docLinks)}
|
||||
/>
|
||||
);
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
|
||||
const deprecationTooltip = wrapper.find('[data-test-subj="roleDeprecationTooltip"]').props();
|
||||
|
||||
expect(deprecationTooltip).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"children": <div>
|
||||
kibana_user
|
||||
|
||||
<EuiIcon
|
||||
className="eui-alignTop"
|
||||
color="warning"
|
||||
size="s"
|
||||
type="alert"
|
||||
/>
|
||||
</div>,
|
||||
"content": "The kibana_user role is deprecated. I don't like you.",
|
||||
"data-test-subj": "roleDeprecationTooltip",
|
||||
"delay": "regular",
|
||||
"position": "top",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiCallOut,
|
||||
|
@ -26,7 +25,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { NotificationsStart } from 'src/core/public';
|
||||
import { RoleMapping } from '../../../../common/model';
|
||||
import { RoleMapping, Role } from '../../../../common/model';
|
||||
import { EmptyPrompt } from './empty_prompt';
|
||||
import {
|
||||
NoCompatibleRealms,
|
||||
|
@ -34,15 +33,15 @@ import {
|
|||
PermissionDenied,
|
||||
SectionLoading,
|
||||
} from '../components';
|
||||
import {
|
||||
getCreateRoleMappingHref,
|
||||
getEditRoleMappingHref,
|
||||
getEditRoleHref,
|
||||
} from '../../management_urls';
|
||||
import { getCreateRoleMappingHref, getEditRoleMappingHref } from '../../management_urls';
|
||||
import { DocumentationLinksService } from '../documentation_links';
|
||||
import { RoleMappingsAPIClient } from '../role_mappings_api_client';
|
||||
import { RoleTableDisplay } from '../../role_table_display';
|
||||
import { RolesAPIClient } from '../../roles';
|
||||
import { EnabledBadge, DisabledBadge } from '../../badges';
|
||||
|
||||
interface Props {
|
||||
rolesAPIClient: PublicMethodsOf<RolesAPIClient>;
|
||||
roleMappingsAPI: PublicMethodsOf<RoleMappingsAPIClient>;
|
||||
notifications: NotificationsStart;
|
||||
docLinks: DocumentationLinksService;
|
||||
|
@ -51,6 +50,7 @@ interface Props {
|
|||
interface State {
|
||||
loadState: 'loadingApp' | 'loadingTable' | 'permissionDenied' | 'finished';
|
||||
roleMappings: null | RoleMapping[];
|
||||
roles: null | Role[];
|
||||
selectedItems: RoleMapping[];
|
||||
hasCompatibleRealms: boolean;
|
||||
error: any;
|
||||
|
@ -62,6 +62,7 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
this.state = {
|
||||
loadState: 'loadingApp',
|
||||
roleMappings: null,
|
||||
roles: null,
|
||||
hasCompatibleRealms: true,
|
||||
selectedItems: [],
|
||||
error: undefined,
|
||||
|
@ -308,7 +309,7 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
}),
|
||||
sortable: true,
|
||||
render: (entry: any, record: RoleMapping) => {
|
||||
const { roles = [], role_templates: roleTemplates = [] } = record;
|
||||
const { roles: assignedRoleNames = [], role_templates: roleTemplates = [] } = record;
|
||||
if (roleTemplates.length > 0) {
|
||||
return (
|
||||
<span data-test-subj="roleMappingRoles">
|
||||
|
@ -322,13 +323,11 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
</span>
|
||||
);
|
||||
}
|
||||
const roleLinks = roles.map((rolename, index) => {
|
||||
return (
|
||||
<Fragment key={rolename}>
|
||||
<EuiLink href={getEditRoleHref(rolename)}>{rolename}</EuiLink>
|
||||
{index === roles.length - 1 ? null : ', '}
|
||||
</Fragment>
|
||||
);
|
||||
const roleLinks = assignedRoleNames.map((rolename, index) => {
|
||||
const role: Role | string =
|
||||
this.state.roles?.find(r => r.name === rolename) ?? rolename;
|
||||
|
||||
return <RoleTableDisplay role={role} key={rolename} />;
|
||||
});
|
||||
return <div data-test-subj="roleMappingRoles">{roleLinks}</div>;
|
||||
},
|
||||
|
@ -341,24 +340,10 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
sortable: true,
|
||||
render: (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
return (
|
||||
<EuiBadge data-test-subj="roleMappingEnabled" color="secondary">
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.roleMappings.enabledBadge"
|
||||
defaultMessage="Enabled"
|
||||
/>
|
||||
</EuiBadge>
|
||||
);
|
||||
return <EnabledBadge data-test-subj="roleMappingEnabled" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiBadge color="hollow" data-test-subj="roleMappingEnabled">
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.roleMappings.disabledBadge"
|
||||
defaultMessage="Disabled"
|
||||
/>
|
||||
</EuiBadge>
|
||||
);
|
||||
return <DisabledBadge data-test-subj="roleMappingEnabled" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -458,13 +443,27 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
});
|
||||
|
||||
if (canManageRoleMappings) {
|
||||
this.loadRoleMappings();
|
||||
this.performInitialLoad();
|
||||
}
|
||||
} catch (e) {
|
||||
this.setState({ error: e, loadState: 'finished' });
|
||||
}
|
||||
}
|
||||
|
||||
private performInitialLoad = async () => {
|
||||
try {
|
||||
const [roleMappings, roles] = await Promise.all([
|
||||
this.props.roleMappingsAPI.getRoleMappings(),
|
||||
this.props.rolesAPIClient.getRoles(),
|
||||
]);
|
||||
this.setState({ roleMappings, roles });
|
||||
} catch (e) {
|
||||
this.setState({ error: e });
|
||||
}
|
||||
|
||||
this.setState({ loadState: 'finished' });
|
||||
};
|
||||
|
||||
private reloadRoleMappings = () => {
|
||||
this.setState({ roleMappings: [], loadState: 'loadingTable' });
|
||||
this.loadRoleMappings();
|
||||
|
|
|
@ -53,7 +53,7 @@ describe('roleMappingsManagementApp', () => {
|
|||
expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Role Mappings' }]);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
Role Mappings Page: {"notifications":{"toasts":{}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}}
|
||||
Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}}
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ export const roleMappingsManagementApp = Object.freeze({
|
|||
return (
|
||||
<RoleMappingsGridPage
|
||||
notifications={notifications}
|
||||
rolesAPIClient={new RolesAPIClient(http)}
|
||||
roleMappingsAPI={roleMappingsAPIClient}
|
||||
docLinks={dockLinksService}
|
||||
/>
|
||||
|
|
|
@ -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 { RoleTableDisplay } from './role_table_display';
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiLink, EuiToolTip, EuiIcon } from '@elastic/eui';
|
||||
import { Role, isRoleDeprecated, getExtendedRoleDeprecationNotice } from '../../../common/model';
|
||||
import { getEditRoleHref } from '../management_urls';
|
||||
|
||||
interface Props {
|
||||
role: Role | string;
|
||||
}
|
||||
|
||||
export const RoleTableDisplay = ({ role }: Props) => {
|
||||
let content;
|
||||
let href;
|
||||
if (typeof role === 'string') {
|
||||
content = <div>{role}</div>;
|
||||
href = getEditRoleHref(role);
|
||||
} else if (isRoleDeprecated(role)) {
|
||||
content = (
|
||||
<EuiToolTip
|
||||
content={getExtendedRoleDeprecationNotice(role)}
|
||||
data-test-subj="roleDeprecationTooltip"
|
||||
>
|
||||
<div>
|
||||
{role.name} <EuiIcon type="alert" color="warning" size="s" className={'eui-alignTop'} />
|
||||
</div>
|
||||
</EuiToolTip>
|
||||
);
|
||||
href = getEditRoleHref(role.name);
|
||||
} else {
|
||||
content = <div>{role.name}</div>;
|
||||
href = getEditRoleHref(role.name);
|
||||
}
|
||||
return <EuiLink href={href}>{content}</EuiLink>;
|
||||
};
|
|
@ -17,6 +17,7 @@ import {
|
|||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -44,11 +45,13 @@ import {
|
|||
RawKibanaPrivileges,
|
||||
Role,
|
||||
BuiltinESPrivileges,
|
||||
isReadOnlyRole as checkIfRoleReadOnly,
|
||||
isReservedRole as checkIfRoleReserved,
|
||||
isRoleReadOnly as checkIfRoleReadOnly,
|
||||
isRoleReserved as checkIfRoleReserved,
|
||||
isRoleDeprecated as checkIfRoleDeprecated,
|
||||
copyRole,
|
||||
prepareRoleClone,
|
||||
RoleIndexPrivilege,
|
||||
getExtendedRoleDeprecationNotice,
|
||||
} from '../../../../common/model';
|
||||
import { ROLES_PATH } from '../../management_urls';
|
||||
import { RoleValidationResult, RoleValidator } from './validate_role';
|
||||
|
@ -299,8 +302,9 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
}
|
||||
|
||||
const isEditingExistingRole = !!roleName && action === 'edit';
|
||||
const isReadOnlyRole = checkIfRoleReadOnly(role);
|
||||
const isReservedRole = checkIfRoleReserved(role);
|
||||
const isRoleReadOnly = checkIfRoleReadOnly(role);
|
||||
const isRoleReserved = checkIfRoleReserved(role);
|
||||
const isDeprecatedRole = checkIfRoleDeprecated(role);
|
||||
|
||||
const [kibanaPrivileges, builtInESPrivileges] = privileges;
|
||||
|
||||
|
@ -309,7 +313,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
const props: HTMLProps<HTMLDivElement> = {
|
||||
tabIndex: 0,
|
||||
};
|
||||
if (isReservedRole) {
|
||||
if (isRoleReserved) {
|
||||
titleText = (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.viewingRoleTitle"
|
||||
|
@ -343,7 +347,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
};
|
||||
|
||||
const getActionButton = () => {
|
||||
if (isEditingExistingRole && !isReadOnlyRole) {
|
||||
if (isEditingExistingRole && !isRoleReadOnly) {
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<DeleteRoleButton canDelete={true} onDelete={handleDeleteRole} />
|
||||
|
@ -365,7 +369,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
/>
|
||||
}
|
||||
helpText={
|
||||
!isReservedRole && isEditingExistingRole ? (
|
||||
!isRoleReserved && isEditingExistingRole ? (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.roleNameFormRowHelpText"
|
||||
defaultMessage="A role's name cannot be changed once it has been created."
|
||||
|
@ -381,7 +385,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
value={role.name || ''}
|
||||
onChange={onNameChange}
|
||||
data-test-subj={'roleFormNameInput'}
|
||||
readOnly={isReservedRole || isEditingExistingRole}
|
||||
readOnly={isRoleReserved || isEditingExistingRole}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
|
@ -400,7 +404,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
<EuiSpacer />
|
||||
<ElasticsearchPrivileges
|
||||
role={role}
|
||||
editable={!isReadOnlyRole}
|
||||
editable={!isRoleReadOnly}
|
||||
indicesAPIClient={indicesAPIClient}
|
||||
onChange={onRoleChange}
|
||||
runAsUsers={runAsUsers}
|
||||
|
@ -426,7 +430,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
spacesEnabled={spacesEnabled}
|
||||
features={features}
|
||||
uiCapabilities={uiCapabilities}
|
||||
editable={!isReadOnlyRole}
|
||||
editable={!isRoleReadOnly}
|
||||
role={role}
|
||||
onChange={onRoleChange}
|
||||
validator={validator}
|
||||
|
@ -436,7 +440,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
};
|
||||
|
||||
const getFormButtons = () => {
|
||||
if (isReadOnlyRole) {
|
||||
if (isRoleReadOnly) {
|
||||
return getReturnToRoleListButton();
|
||||
}
|
||||
|
||||
|
@ -479,7 +483,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
data-test-subj={`roleFormSaveButton`}
|
||||
fill
|
||||
onClick={saveRole}
|
||||
disabled={isReservedRole}
|
||||
disabled={isRoleReserved}
|
||||
>
|
||||
{saveText}
|
||||
</EuiButton>
|
||||
|
@ -563,7 +567,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
|
||||
<EuiText size="s">{description}</EuiText>
|
||||
|
||||
{isReservedRole && (
|
||||
{isRoleReserved && (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s" color="subdued">
|
||||
|
@ -577,6 +581,17 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
</Fragment>
|
||||
)}
|
||||
|
||||
{isDeprecatedRole && (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCallOut
|
||||
title={getExtendedRoleDeprecationNotice(role)}
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{getRoleName()}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { EuiComboBox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { Component } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { Role, isReadOnlyRole } from '../../../../../../common/model';
|
||||
import { Role, isRoleReadOnly } from '../../../../../../common/model';
|
||||
|
||||
interface Props {
|
||||
role: Role;
|
||||
|
@ -38,7 +38,7 @@ export class ClusterPrivileges extends Component<Props, {}> {
|
|||
selectedOptions={selectedOptions}
|
||||
onChange={this.onClusterPrivilegesChange}
|
||||
onCreateOption={this.onCreateCustomPrivilege}
|
||||
isDisabled={isReadOnlyRole(role)}
|
||||
isDisabled={isRoleReadOnly(role)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
|
|
@ -23,7 +23,7 @@ test('it renders without crashing', () => {
|
|||
indexPatterns: [],
|
||||
availableFields: [],
|
||||
availableIndexPrivileges: ['all', 'read', 'write', 'index'],
|
||||
isReadOnlyRole: false,
|
||||
isRoleReadOnly: false,
|
||||
allowDocumentLevelSecurity: true,
|
||||
allowFieldLevelSecurity: true,
|
||||
validator: new RoleValidator(),
|
||||
|
@ -50,7 +50,7 @@ describe('delete button', () => {
|
|||
indexPatterns: [],
|
||||
availableFields: [],
|
||||
availableIndexPrivileges: ['all', 'read', 'write', 'index'],
|
||||
isReadOnlyRole: false,
|
||||
isRoleReadOnly: false,
|
||||
allowDocumentLevelSecurity: true,
|
||||
allowFieldLevelSecurity: true,
|
||||
validator: new RoleValidator(),
|
||||
|
@ -59,19 +59,19 @@ describe('delete button', () => {
|
|||
intl: {} as any,
|
||||
};
|
||||
|
||||
test('it is hidden when isReadOnlyRole is true', () => {
|
||||
test('it is hidden when isRoleReadOnly is true', () => {
|
||||
const testProps = {
|
||||
...props,
|
||||
isReadOnlyRole: true,
|
||||
isRoleReadOnly: true,
|
||||
};
|
||||
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
|
||||
expect(wrapper.find(EuiButtonIcon)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('it is shown when isReadOnlyRole is false', () => {
|
||||
test('it is shown when isRoleReadOnly is false', () => {
|
||||
const testProps = {
|
||||
...props,
|
||||
isReadOnlyRole: false,
|
||||
isRoleReadOnly: false,
|
||||
};
|
||||
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
|
||||
expect(wrapper.find(EuiButtonIcon)).toHaveLength(1);
|
||||
|
@ -80,7 +80,7 @@ describe('delete button', () => {
|
|||
test('it invokes onDelete when clicked', () => {
|
||||
const testProps = {
|
||||
...props,
|
||||
isReadOnlyRole: false,
|
||||
isRoleReadOnly: false,
|
||||
};
|
||||
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
|
||||
wrapper.find(EuiButtonIcon).simulate('click');
|
||||
|
@ -102,7 +102,7 @@ describe(`document level security`, () => {
|
|||
indexPatterns: [],
|
||||
availableFields: [],
|
||||
availableIndexPrivileges: ['all', 'read', 'write', 'index'],
|
||||
isReadOnlyRole: false,
|
||||
isRoleReadOnly: false,
|
||||
allowDocumentLevelSecurity: true,
|
||||
allowFieldLevelSecurity: true,
|
||||
validator: new RoleValidator(),
|
||||
|
@ -161,7 +161,7 @@ describe('field level security', () => {
|
|||
indexPatterns: [],
|
||||
availableFields: [],
|
||||
availableIndexPrivileges: ['all', 'read', 'write', 'index'],
|
||||
isReadOnlyRole: false,
|
||||
isRoleReadOnly: false,
|
||||
allowDocumentLevelSecurity: true,
|
||||
allowFieldLevelSecurity: true,
|
||||
validator: new RoleValidator(),
|
||||
|
|
|
@ -33,7 +33,7 @@ interface Props {
|
|||
availableFields: string[];
|
||||
onChange: (indexPrivilege: RoleIndexPrivilege) => void;
|
||||
onDelete: () => void;
|
||||
isReadOnlyRole: boolean;
|
||||
isRoleReadOnly: boolean;
|
||||
allowDocumentLevelSecurity: boolean;
|
||||
allowFieldLevelSecurity: boolean;
|
||||
validator: RoleValidator;
|
||||
|
@ -68,7 +68,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
<EuiHorizontalRule />
|
||||
<EuiFlexGroup className="index-privilege-form">
|
||||
<EuiFlexItem>{this.getPrivilegeForm()}</EuiFlexItem>
|
||||
{!this.props.isReadOnlyRole && (
|
||||
{!this.props.isRoleReadOnly && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow hasEmptyLabelSpace>
|
||||
<EuiButtonIcon
|
||||
|
@ -109,7 +109,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
selectedOptions={this.props.indexPrivilege.names.map(toOption)}
|
||||
onCreateOption={this.onCreateIndexPatternOption}
|
||||
onChange={this.onIndexPatternsChange}
|
||||
isDisabled={this.props.isReadOnlyRole}
|
||||
isDisabled={this.props.isRoleReadOnly}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
@ -128,7 +128,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
options={this.props.availableIndexPrivileges.map(toOption)}
|
||||
selectedOptions={this.props.indexPrivilege.privileges.map(toOption)}
|
||||
onChange={this.onPrivilegeChange}
|
||||
isDisabled={this.props.isReadOnlyRole}
|
||||
isDisabled={this.props.isRoleReadOnly}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
@ -149,7 +149,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
allowDocumentLevelSecurity,
|
||||
availableFields,
|
||||
indexPrivilege,
|
||||
isReadOnlyRole,
|
||||
isRoleReadOnly,
|
||||
} = this.props;
|
||||
|
||||
if (!allowFieldLevelSecurity) {
|
||||
|
@ -161,7 +161,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column">
|
||||
{!isReadOnlyRole && (
|
||||
{!isRoleReadOnly && (
|
||||
<EuiFlexItem>
|
||||
{
|
||||
<EuiSwitch
|
||||
|
@ -193,7 +193,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
fullWidth={true}
|
||||
className="indexPrivilegeForm__grantedFieldsRow"
|
||||
helpText={
|
||||
!isReadOnlyRole && grant.length === 0 ? (
|
||||
!isRoleReadOnly && 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."
|
||||
|
@ -210,7 +210,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
selectedOptions={grant.map(toOption)}
|
||||
onCreateOption={this.onCreateGrantedField}
|
||||
onChange={this.onGrantedFieldsChange}
|
||||
isDisabled={this.props.isReadOnlyRole}
|
||||
isDisabled={this.props.isRoleReadOnly}
|
||||
/>
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
|
@ -233,7 +233,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
selectedOptions={except.map(toOption)}
|
||||
onCreateOption={this.onCreateDeniedField}
|
||||
onChange={this.onDeniedFieldsChange}
|
||||
isDisabled={isReadOnlyRole}
|
||||
isDisabled={isRoleReadOnly}
|
||||
/>
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
|
@ -248,7 +248,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
};
|
||||
|
||||
private getGrantedDocumentsControl = () => {
|
||||
const { allowDocumentLevelSecurity, indexPrivilege, isReadOnlyRole } = this.props;
|
||||
const { allowDocumentLevelSecurity, indexPrivilege, isRoleReadOnly } = this.props;
|
||||
|
||||
if (!allowDocumentLevelSecurity) {
|
||||
return null;
|
||||
|
@ -256,7 +256,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
{!this.props.isReadOnlyRole && (
|
||||
{!this.props.isRoleReadOnly && (
|
||||
<EuiFlexItem>
|
||||
{
|
||||
<EuiSwitch
|
||||
|
@ -270,7 +270,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
compressed={true}
|
||||
checked={this.state.queryExpanded}
|
||||
onChange={this.toggleDocumentQuery}
|
||||
disabled={isReadOnlyRole}
|
||||
disabled={isRoleReadOnly}
|
||||
/>
|
||||
}
|
||||
</EuiFlexItem>
|
||||
|
@ -292,7 +292,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
fullWidth={true}
|
||||
value={indexPrivilege.query}
|
||||
onChange={this.onQueryChange}
|
||||
readOnly={this.props.isReadOnlyRole}
|
||||
readOnly={this.props.isRoleReadOnly}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -8,7 +8,7 @@ import React, { Component, Fragment } from 'react';
|
|||
import {
|
||||
Role,
|
||||
RoleIndexPrivilege,
|
||||
isReadOnlyRole,
|
||||
isRoleReadOnly,
|
||||
isRoleEnabled,
|
||||
} from '../../../../../../common/model';
|
||||
import { SecurityLicense } from '../../../../../../common/licensing';
|
||||
|
@ -57,7 +57,7 @@ export class IndexPrivileges extends Component<Props, State> {
|
|||
// doesn't permit FLS/DLS).
|
||||
allowDocumentLevelSecurity: allowRoleDocumentLevelSecurity || !isRoleEnabled(this.props.role),
|
||||
allowFieldLevelSecurity: allowRoleFieldLevelSecurity || !isRoleEnabled(this.props.role),
|
||||
isReadOnlyRole: isReadOnlyRole(this.props.role),
|
||||
isRoleReadOnly: isRoleReadOnly(this.props.role),
|
||||
};
|
||||
|
||||
const forms = indices.map((indexPrivilege: RoleIndexPrivilege, idx) => (
|
||||
|
@ -143,7 +143,7 @@ export class IndexPrivileges extends Component<Props, State> {
|
|||
|
||||
public loadAvailableFields(privileges: RoleIndexPrivilege[]) {
|
||||
// readonly roles cannot be edited, and therefore do not need to fetch available fields.
|
||||
if (isReadOnlyRole(this.props.role)) {
|
||||
if (isRoleReadOnly(this.props.role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import React, { Component, Fragment } from 'react';
|
|||
import { Capabilities } from 'src/core/public';
|
||||
import { Space } from '../../../../../../../../spaces/public';
|
||||
import { Feature } from '../../../../../../../../features/public';
|
||||
import { KibanaPrivileges, Role, isReservedRole } from '../../../../../../../common/model';
|
||||
import { KibanaPrivileges, Role, isRoleReserved } from '../../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator';
|
||||
import { RoleValidator } from '../../../validate_role';
|
||||
import { PrivilegeMatrix } from './privilege_matrix';
|
||||
|
@ -219,7 +219,7 @@ class SpaceAwarePrivilegeSectionUI extends Component<Props, State> {
|
|||
return (
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>{addPrivilegeButton}</EuiFlexItem>
|
||||
{hasPrivilegesAssigned && !isReservedRole(this.props.role) && (
|
||||
{hasPrivilegesAssigned && !isRoleReserved(this.props.role) && (
|
||||
<EuiFlexItem grow={false}>{viewMatrixButton}</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -8,7 +8,7 @@ import React from 'react';
|
|||
|
||||
import { EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Role, isReservedRole } from '../../../../common/model';
|
||||
import { Role, isRoleReserved } from '../../../../common/model';
|
||||
|
||||
interface Props {
|
||||
role: Role;
|
||||
|
@ -17,7 +17,7 @@ interface Props {
|
|||
export const ReservedRoleBadge = (props: Props) => {
|
||||
const { role } = props;
|
||||
|
||||
if (isReservedRole(role)) {
|
||||
if (isRoleReserved(role)) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
data-test-subj="reservedRoleBadgeTooltip"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { EuiIcon, EuiBasicTable } from '@elastic/eui';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
@ -14,6 +14,8 @@ import { RolesGridPage } from './roles_grid_page';
|
|||
|
||||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { rolesAPIClientMock } from '../index.mock';
|
||||
import { ReservedBadge, DisabledBadge } from '../../badges';
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
|
||||
const mock403 = () => ({ body: { statusCode: 403 } });
|
||||
|
||||
|
@ -76,8 +78,24 @@ describe('<RolesGridPage />', () => {
|
|||
});
|
||||
|
||||
expect(wrapper.find(PermissionDenied)).toHaveLength(0);
|
||||
expect(wrapper.find('EuiIcon[data-test-subj="reservedRole"]')).toHaveLength(1);
|
||||
expect(wrapper.find('EuiCheckbox[title="Role is reserved"]')).toHaveLength(1);
|
||||
expect(wrapper.find(ReservedBadge)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it(`renders disabled roles as such`, async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RolesGridPage
|
||||
rolesAPIClient={apiClientMock}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
const initialIconCount = wrapper.find(EuiIcon).length;
|
||||
|
||||
await waitForRender(wrapper, updatedWrapper => {
|
||||
return updatedWrapper.find(EuiIcon).length > initialIconCount;
|
||||
});
|
||||
|
||||
expect(wrapper.find(PermissionDenied)).toHaveLength(0);
|
||||
expect(wrapper.find(DisabledBadge)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders permission denied if required', async () => {
|
||||
|
@ -123,4 +141,54 @@ describe('<RolesGridPage />', () => {
|
|||
wrapper.find('EuiButtonIcon[data-test-subj="clone-role-action-disabled-role"]')
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('hides reserved roles when instructed to', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RolesGridPage
|
||||
rolesAPIClient={apiClientMock}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
const initialIconCount = wrapper.find(EuiIcon).length;
|
||||
|
||||
await waitForRender(wrapper, updatedWrapper => {
|
||||
return updatedWrapper.find(EuiIcon).length > initialIconCount;
|
||||
});
|
||||
|
||||
expect(wrapper.find(EuiBasicTable).props().items).toEqual([
|
||||
{
|
||||
name: 'disabled-role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
transient_metadata: { enabled: false },
|
||||
},
|
||||
{
|
||||
name: 'reserved-role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
metadata: { _reserved: true },
|
||||
},
|
||||
{
|
||||
name: 'test-role-1',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
]);
|
||||
|
||||
findTestSubject(wrapper, 'showReservedRolesSwitch').simulate('click');
|
||||
|
||||
expect(wrapper.find(EuiBasicTable).props().items).toEqual([
|
||||
{
|
||||
name: 'disabled-role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
transient_metadata: { enabled: false },
|
||||
},
|
||||
{
|
||||
name: 'test-role-1',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@ import _ from 'lodash';
|
|||
import React, { Component } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiIcon,
|
||||
EuiInMemoryTable,
|
||||
EuiLink,
|
||||
EuiPageContent,
|
||||
|
@ -19,14 +18,26 @@ import {
|
|||
EuiTitle,
|
||||
EuiButtonIcon,
|
||||
EuiBasicTableColumn,
|
||||
EuiSwitchEvent,
|
||||
EuiSwitch,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { NotificationsStart } from 'src/core/public';
|
||||
import { Role, isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../common/model';
|
||||
import {
|
||||
Role,
|
||||
isRoleEnabled,
|
||||
isRoleReadOnly,
|
||||
isRoleReserved,
|
||||
isRoleDeprecated,
|
||||
getExtendedRoleDeprecationNotice,
|
||||
} from '../../../../common/model';
|
||||
import { RolesAPIClient } from '../roles_api_client';
|
||||
import { ConfirmDelete } from './confirm_delete';
|
||||
import { PermissionDenied } from './permission_denied';
|
||||
import { DisabledBadge, DeprecatedBadge, ReservedBadge } from '../../badges';
|
||||
|
||||
interface Props {
|
||||
notifications: NotificationsStart;
|
||||
|
@ -35,10 +46,12 @@ interface Props {
|
|||
|
||||
interface State {
|
||||
roles: Role[];
|
||||
visibleRoles: Role[];
|
||||
selection: Role[];
|
||||
filter: string;
|
||||
showDeleteConfirmation: boolean;
|
||||
permissionDenied: boolean;
|
||||
includeReservedRoles: boolean;
|
||||
}
|
||||
|
||||
const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => {
|
||||
|
@ -50,10 +63,12 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
super(props);
|
||||
this.state = {
|
||||
roles: [],
|
||||
visibleRoles: [],
|
||||
selection: [],
|
||||
filter: '',
|
||||
showDeleteConfirmation: false,
|
||||
permissionDenied: false,
|
||||
includeReservedRoles: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -125,16 +140,22 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
initialPageSize: 20,
|
||||
pageSizeOptions: [10, 20, 30, 50, 100],
|
||||
}}
|
||||
items={this.getVisibleRoles()}
|
||||
items={this.state.visibleRoles}
|
||||
loading={roles.length === 0}
|
||||
search={{
|
||||
toolsLeft: this.renderToolsLeft(),
|
||||
toolsRight: this.renderToolsRight(),
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
onChange: (query: Record<string, any>) => {
|
||||
this.setState({
|
||||
filter: query.queryText,
|
||||
visibleRoles: this.getVisibleRoles(
|
||||
this.state.roles,
|
||||
query.queryText,
|
||||
this.state.includeReservedRoles
|
||||
),
|
||||
});
|
||||
},
|
||||
}}
|
||||
|
@ -158,11 +179,6 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
};
|
||||
|
||||
private getColumnConfig = () => {
|
||||
const reservedRoleDesc = i18n.translate(
|
||||
'xpack.security.management.roles.reservedColumnDescription',
|
||||
{ defaultMessage: 'Reserved roles are built-in and cannot be edited or removed.' }
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
|
@ -177,35 +193,18 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
<EuiLink data-test-subj="roleRowName" href={getRoleManagementHref('edit', name)}>
|
||||
{name}
|
||||
</EuiLink>
|
||||
{!isRoleEnabled(record) && (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.roles.disabledTooltip"
|
||||
defaultMessage=" (disabled)"
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata',
|
||||
name: i18n.translate('xpack.security.management.roles.reservedColumnName', {
|
||||
defaultMessage: 'Reserved',
|
||||
name: i18n.translate('xpack.security.management.roles.statusColumnName', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
sortable: ({ metadata }: Role) => Boolean(metadata && metadata._reserved),
|
||||
dataType: 'boolean',
|
||||
align: 'right',
|
||||
description: reservedRoleDesc,
|
||||
render: (metadata: Role['metadata']) => {
|
||||
const label = i18n.translate('xpack.security.management.roles.reservedRoleIconLabel', {
|
||||
defaultMessage: 'Reserved role',
|
||||
});
|
||||
|
||||
return metadata && metadata._reserved ? (
|
||||
<span title={label}>
|
||||
<EuiIcon aria-label={label} data-test-subj="reservedRole" type="check" />
|
||||
</span>
|
||||
) : null;
|
||||
sortable: (role: Role) => isRoleEnabled(role) && !isRoleDeprecated(role),
|
||||
render: (metadata: Role['metadata'], record: Role) => {
|
||||
return this.getRoleStatusBadges(record);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -215,7 +214,7 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
width: '150px',
|
||||
actions: [
|
||||
{
|
||||
available: (role: Role) => !isReadOnlyRole(role),
|
||||
available: (role: Role) => !isRoleReadOnly(role),
|
||||
render: (role: Role) => {
|
||||
const title = i18n.translate('xpack.security.management.roles.editRoleActionName', {
|
||||
defaultMessage: `Edit {roleName}`,
|
||||
|
@ -235,7 +234,7 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
},
|
||||
},
|
||||
{
|
||||
available: (role: Role) => !isReservedRole(role),
|
||||
available: (role: Role) => !isRoleReserved(role),
|
||||
render: (role: Role) => {
|
||||
const title = i18n.translate('xpack.security.management.roles.cloneRoleActionName', {
|
||||
defaultMessage: `Clone {roleName}`,
|
||||
|
@ -259,16 +258,64 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
] as Array<EuiBasicTableColumn<Role>>;
|
||||
};
|
||||
|
||||
private getVisibleRoles = () => {
|
||||
const { roles, filter } = this.state;
|
||||
private getVisibleRoles = (roles: Role[], filter: string, includeReservedRoles: boolean) => {
|
||||
return roles.filter(role => {
|
||||
const normalized = `${role.name}`.toLowerCase();
|
||||
const normalizedQuery = filter.toLowerCase();
|
||||
return (
|
||||
normalized.indexOf(normalizedQuery) !== -1 &&
|
||||
(includeReservedRoles || !isRoleReserved(role))
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return filter
|
||||
? roles.filter(({ name }) => {
|
||||
const normalized = `${name}`.toLowerCase();
|
||||
const normalizedQuery = filter.toLowerCase();
|
||||
return normalized.indexOf(normalizedQuery) !== -1;
|
||||
})
|
||||
: roles;
|
||||
private onIncludeReservedRolesChange = (e: EuiSwitchEvent) => {
|
||||
this.setState({
|
||||
includeReservedRoles: e.target.checked,
|
||||
visibleRoles: this.getVisibleRoles(this.state.roles, this.state.filter, e.target.checked),
|
||||
});
|
||||
};
|
||||
|
||||
private getRoleStatusBadges = (role: Role) => {
|
||||
const enabled = isRoleEnabled(role);
|
||||
const deprecated = isRoleDeprecated(role);
|
||||
const reserved = isRoleReserved(role);
|
||||
|
||||
const badges = [];
|
||||
if (!enabled) {
|
||||
badges.push(<DisabledBadge data-test-subj="roleDisabled" />);
|
||||
}
|
||||
if (reserved) {
|
||||
badges.push(
|
||||
<ReservedBadge
|
||||
data-test-subj="roleReserved"
|
||||
tooltipContent={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.roles.reservedRoleBadgeTooltip"
|
||||
defaultMessage="Reserved roles are built-in and cannot be edited or removed."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (deprecated) {
|
||||
badges.push(
|
||||
<DeprecatedBadge
|
||||
data-test-subj="roleDeprecated"
|
||||
tooltipContent={getExtendedRoleDeprecationNotice(role)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
{badges.map((badge, index) => (
|
||||
<EuiFlexItem key={index} grow={false}>
|
||||
{badge}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
private handleDelete = () => {
|
||||
|
@ -283,7 +330,14 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
try {
|
||||
const roles = await this.props.rolesAPIClient.getRoles();
|
||||
|
||||
this.setState({ roles });
|
||||
this.setState({
|
||||
roles,
|
||||
visibleRoles: this.getVisibleRoles(
|
||||
roles,
|
||||
this.state.filter,
|
||||
this.state.includeReservedRoles
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
if (_.get(e, 'body.statusCode') === 403) {
|
||||
this.setState({ permissionDenied: true });
|
||||
|
@ -320,6 +374,21 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
</EuiButton>
|
||||
);
|
||||
}
|
||||
private renderToolsRight() {
|
||||
return (
|
||||
<EuiSwitch
|
||||
data-test-subj="showReservedRolesSwitch"
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.roles.showReservedRolesLabel"
|
||||
defaultMessage="Show reserved roles"
|
||||
/>
|
||||
}
|
||||
checked={this.state.includeReservedRoles}
|
||||
onChange={this.onIncludeReservedRolesChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
private onCancelDelete = () => {
|
||||
this.setState({ showDeleteConfirmation: false });
|
||||
};
|
||||
|
|
|
@ -40,7 +40,7 @@ describe('<ChangePasswordForm>', () => {
|
|||
<ChangePasswordForm
|
||||
user={user}
|
||||
isUserChangingOwnPassword={true}
|
||||
apiClient={userAPIClientMock.create()}
|
||||
userAPIClient={userAPIClientMock.create()}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
|
@ -68,7 +68,7 @@ describe('<ChangePasswordForm>', () => {
|
|||
user={user}
|
||||
isUserChangingOwnPassword={true}
|
||||
onChangePassword={callback}
|
||||
apiClient={apiClientMock}
|
||||
userAPIClient={apiClientMock}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
|
@ -107,7 +107,7 @@ describe('<ChangePasswordForm>', () => {
|
|||
<ChangePasswordForm
|
||||
user={user}
|
||||
isUserChangingOwnPassword={false}
|
||||
apiClient={userAPIClientMock.create()}
|
||||
userAPIClient={userAPIClientMock.create()}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -23,7 +23,7 @@ interface Props {
|
|||
user: User;
|
||||
isUserChangingOwnPassword: boolean;
|
||||
onChangePassword?: () => void;
|
||||
apiClient: PublicMethodsOf<UserAPIClient>;
|
||||
userAPIClient: PublicMethodsOf<UserAPIClient>;
|
||||
notifications: NotificationsStart;
|
||||
}
|
||||
|
||||
|
@ -279,7 +279,7 @@ export class ChangePasswordForm extends Component<Props, State> {
|
|||
|
||||
private performPasswordChange = async () => {
|
||||
try {
|
||||
await this.props.apiClient.changePassword(
|
||||
await this.props.userAPIClient.changePassword(
|
||||
this.props.user.username,
|
||||
this.state.newPassword,
|
||||
this.state.currentPassword
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('ConfirmDeleteUsers', () => {
|
|||
it('renders a warning for a single user', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ConfirmDeleteUsers
|
||||
apiClient={userAPIClientMock.create()}
|
||||
userAPIClient={userAPIClientMock.create()}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
usersToDelete={['foo']}
|
||||
onCancel={jest.fn()}
|
||||
|
@ -28,7 +28,7 @@ describe('ConfirmDeleteUsers', () => {
|
|||
it('renders a warning for a multiple users', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ConfirmDeleteUsers
|
||||
apiClient={userAPIClientMock.create()}
|
||||
userAPIClient={userAPIClientMock.create()}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
usersToDelete={['foo', 'bar', 'baz']}
|
||||
onCancel={jest.fn()}
|
||||
|
@ -42,7 +42,7 @@ describe('ConfirmDeleteUsers', () => {
|
|||
const onCancel = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<ConfirmDeleteUsers
|
||||
apiClient={userAPIClientMock.create()}
|
||||
userAPIClient={userAPIClientMock.create()}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
usersToDelete={['foo']}
|
||||
onCancel={onCancel}
|
||||
|
@ -63,7 +63,7 @@ describe('ConfirmDeleteUsers', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ConfirmDeleteUsers
|
||||
usersToDelete={['foo', 'bar']}
|
||||
apiClient={apiClientMock}
|
||||
userAPIClient={apiClientMock}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
|
@ -90,7 +90,7 @@ describe('ConfirmDeleteUsers', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ConfirmDeleteUsers
|
||||
usersToDelete={['foo', 'bar']}
|
||||
apiClient={apiClientMock}
|
||||
userAPIClient={apiClientMock}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
|
|
|
@ -13,7 +13,7 @@ import { UserAPIClient } from '../..';
|
|||
|
||||
interface Props {
|
||||
usersToDelete: string[];
|
||||
apiClient: PublicMethodsOf<UserAPIClient>;
|
||||
userAPIClient: PublicMethodsOf<UserAPIClient>;
|
||||
notifications: NotificationsStart;
|
||||
onCancel: () => void;
|
||||
callback?: (usersToDelete: string[], errors: string[]) => void;
|
||||
|
@ -77,11 +77,11 @@ export class ConfirmDeleteUsers extends Component<Props, unknown> {
|
|||
}
|
||||
|
||||
private deleteUsers = () => {
|
||||
const { usersToDelete, callback, apiClient, notifications } = this.props;
|
||||
const { usersToDelete, callback, userAPIClient, notifications } = this.props;
|
||||
const errors: string[] = [];
|
||||
usersToDelete.forEach(async username => {
|
||||
try {
|
||||
await apiClient.deleteUser(username);
|
||||
await userAPIClient.deleteUser(username);
|
||||
notifications.toasts.addSuccess(
|
||||
i18n.translate(
|
||||
'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage',
|
||||
|
|
|
@ -15,13 +15,14 @@ import { mockAuthenticatedUser } from '../../../../common/model/authenticated_us
|
|||
import { securityMock } from '../../../mocks';
|
||||
import { rolesAPIClientMock } from '../../roles/index.mock';
|
||||
import { userAPIClientMock } from '../index.mock';
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
|
||||
const createUser = (username: string) => {
|
||||
const createUser = (username: string, roles = ['idk', 'something']) => {
|
||||
const user: User = {
|
||||
username,
|
||||
full_name: 'my full name',
|
||||
email: 'foo@bar.com',
|
||||
roles: ['idk', 'something'],
|
||||
roles,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
|
@ -34,9 +35,9 @@ const createUser = (username: string) => {
|
|||
return user;
|
||||
};
|
||||
|
||||
const buildClients = () => {
|
||||
const buildClients = (user: User) => {
|
||||
const apiClient = userAPIClientMock.create();
|
||||
apiClient.getUser.mockImplementation(async (username: string) => createUser(username));
|
||||
apiClient.getUser.mockResolvedValue(user);
|
||||
|
||||
const rolesAPIClient = rolesAPIClientMock.create();
|
||||
rolesAPIClient.getRoles.mockImplementation(() => {
|
||||
|
@ -59,6 +60,18 @@ const buildClients = () => {
|
|||
},
|
||||
kibana: [],
|
||||
},
|
||||
{
|
||||
name: 'deprecated-role',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: ['bar'],
|
||||
},
|
||||
kibana: [],
|
||||
metadata: {
|
||||
_deprecated: true,
|
||||
},
|
||||
},
|
||||
] as Role[]);
|
||||
});
|
||||
|
||||
|
@ -83,12 +96,13 @@ function expectMissingSaveButton(wrapper: ReactWrapper<any, any>) {
|
|||
|
||||
describe('EditUserPage', () => {
|
||||
it('allows reserved users to be viewed', async () => {
|
||||
const { apiClient, rolesAPIClient } = buildClients();
|
||||
const user = createUser('reserved_user');
|
||||
const { apiClient, rolesAPIClient } = buildClients(user);
|
||||
const securitySetup = buildSecuritySetup();
|
||||
const wrapper = mountWithIntl(
|
||||
<EditUserPage
|
||||
username={'reserved_user'}
|
||||
apiClient={apiClient}
|
||||
username={user.username}
|
||||
userAPIClient={apiClient}
|
||||
rolesAPIClient={rolesAPIClient}
|
||||
authc={securitySetup.authc}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
|
@ -104,12 +118,13 @@ describe('EditUserPage', () => {
|
|||
});
|
||||
|
||||
it('allows new users to be created', async () => {
|
||||
const { apiClient, rolesAPIClient } = buildClients();
|
||||
const user = createUser('');
|
||||
const { apiClient, rolesAPIClient } = buildClients(user);
|
||||
const securitySetup = buildSecuritySetup();
|
||||
const wrapper = mountWithIntl(
|
||||
<EditUserPage
|
||||
username={''}
|
||||
apiClient={apiClient}
|
||||
username={user.username}
|
||||
userAPIClient={apiClient}
|
||||
rolesAPIClient={rolesAPIClient}
|
||||
authc={securitySetup.authc}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
|
@ -125,12 +140,13 @@ describe('EditUserPage', () => {
|
|||
});
|
||||
|
||||
it('allows existing users to be edited', async () => {
|
||||
const { apiClient, rolesAPIClient } = buildClients();
|
||||
const user = createUser('existing_user');
|
||||
const { apiClient, rolesAPIClient } = buildClients(user);
|
||||
const securitySetup = buildSecuritySetup();
|
||||
const wrapper = mountWithIntl(
|
||||
<EditUserPage
|
||||
username={'existing_user'}
|
||||
apiClient={apiClient}
|
||||
username={user.username}
|
||||
userAPIClient={apiClient}
|
||||
rolesAPIClient={rolesAPIClient}
|
||||
authc={securitySetup.authc}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
|
@ -142,8 +158,32 @@ describe('EditUserPage', () => {
|
|||
expect(apiClient.getUser).toBeCalledTimes(1);
|
||||
expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1);
|
||||
|
||||
expect(findTestSubject(wrapper, 'hasDeprecatedRolesAssignedHelpText')).toHaveLength(0);
|
||||
expectSaveButton(wrapper);
|
||||
});
|
||||
|
||||
it('warns when user is assigned a deprecated role', async () => {
|
||||
const user = createUser('existing_user', ['deprecated-role']);
|
||||
const { apiClient, rolesAPIClient } = buildClients(user);
|
||||
const securitySetup = buildSecuritySetup();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<EditUserPage
|
||||
username={user.username}
|
||||
userAPIClient={apiClient}
|
||||
rolesAPIClient={rolesAPIClient}
|
||||
authc={securitySetup.authc}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(apiClient.getUser).toBeCalledTimes(1);
|
||||
expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1);
|
||||
|
||||
expect(findTestSubject(wrapper, 'hasDeprecatedRolesAssignedHelpText')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForRender(wrapper: ReactWrapper<any, any>) {
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
EuiIcon,
|
||||
EuiText,
|
||||
EuiFieldText,
|
||||
EuiComboBox,
|
||||
EuiPageContent,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
|
@ -29,17 +28,18 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { NotificationsStart } from 'src/core/public';
|
||||
import { User, EditUser, Role } from '../../../../common/model';
|
||||
import { User, EditUser, Role, isRoleDeprecated } from '../../../../common/model';
|
||||
import { AuthenticationServiceSetup } from '../../../authentication';
|
||||
import { USERS_PATH } from '../../management_urls';
|
||||
import { RolesAPIClient } from '../../roles';
|
||||
import { ConfirmDeleteUsers, ChangePasswordForm } from '../components';
|
||||
import { UserValidator, UserValidationResult } from './validate_user';
|
||||
import { RoleComboBox } from '../../role_combo_box';
|
||||
import { UserAPIClient } from '..';
|
||||
|
||||
interface Props {
|
||||
username?: string;
|
||||
apiClient: PublicMethodsOf<UserAPIClient>;
|
||||
userAPIClient: PublicMethodsOf<UserAPIClient>;
|
||||
rolesAPIClient: PublicMethodsOf<RolesAPIClient>;
|
||||
authc: AuthenticationServiceSetup;
|
||||
notifications: NotificationsStart;
|
||||
|
@ -53,7 +53,7 @@ interface State {
|
|||
showDeleteConfirmation: boolean;
|
||||
user: EditUser;
|
||||
roles: Role[];
|
||||
selectedRoles: Array<{ label: string }>;
|
||||
selectedRoles: string[];
|
||||
formError: UserValidationResult | null;
|
||||
}
|
||||
|
||||
|
@ -99,12 +99,12 @@ export class EditUserPage extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private async setCurrentUser() {
|
||||
const { username, apiClient, rolesAPIClient, notifications, authc } = this.props;
|
||||
const { username, userAPIClient, rolesAPIClient, notifications, authc } = this.props;
|
||||
let { user, currentUser } = this.state;
|
||||
if (username) {
|
||||
try {
|
||||
user = {
|
||||
...(await apiClient.getUser(username)),
|
||||
...(await userAPIClient.getUser(username)),
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
@ -138,7 +138,7 @@ export class EditUserPage extends Component<Props, State> {
|
|||
currentUser,
|
||||
user,
|
||||
roles,
|
||||
selectedRoles: user.roles.map(role => ({ label: role })) || [],
|
||||
selectedRoles: user.roles || [],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -160,18 +160,16 @@ export class EditUserPage extends Component<Props, State> {
|
|||
this.setState({
|
||||
formError: null,
|
||||
});
|
||||
const { apiClient } = this.props;
|
||||
const { userAPIClient } = this.props;
|
||||
const { user, isNewUser, selectedRoles } = this.state;
|
||||
const userToSave: EditUser = { ...user };
|
||||
if (!isNewUser) {
|
||||
delete userToSave.password;
|
||||
}
|
||||
delete userToSave.confirmPassword;
|
||||
userToSave.roles = selectedRoles.map(selectedRole => {
|
||||
return selectedRole.label;
|
||||
});
|
||||
userToSave.roles = [...selectedRoles];
|
||||
try {
|
||||
await apiClient.saveUser(userToSave);
|
||||
await userAPIClient.saveUser(userToSave);
|
||||
this.props.notifications.toasts.addSuccess(
|
||||
i18n.translate(
|
||||
'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage',
|
||||
|
@ -269,7 +267,7 @@ export class EditUserPage extends Component<Props, State> {
|
|||
user={this.state.user}
|
||||
isUserChangingOwnPassword={userIsLoggedInUser}
|
||||
onChangePassword={this.toggleChangePasswordForm}
|
||||
apiClient={this.props.apiClient}
|
||||
userAPIClient={this.props.userAPIClient}
|
||||
notifications={this.props.notifications}
|
||||
/>
|
||||
</Fragment>
|
||||
|
@ -346,7 +344,7 @@ export class EditUserPage extends Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
private onRolesChange = (selectedRoles: Array<{ label: string }>) => {
|
||||
private onRolesChange = (selectedRoles: string[]) => {
|
||||
this.setState({
|
||||
selectedRoles,
|
||||
});
|
||||
|
@ -365,8 +363,8 @@ export class EditUserPage extends Component<Props, State> {
|
|||
public render() {
|
||||
const {
|
||||
user,
|
||||
roles,
|
||||
selectedRoles,
|
||||
roles,
|
||||
showChangePasswordForm,
|
||||
isNewUser,
|
||||
showDeleteConfirmation,
|
||||
|
@ -380,6 +378,22 @@ export class EditUserPage extends Component<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const hasAnyDeprecatedRolesAssigned = selectedRoles.some(selected => {
|
||||
const role = roles.find(r => r.name === selected);
|
||||
return role && isRoleDeprecated(role);
|
||||
});
|
||||
|
||||
const roleHelpText = hasAnyDeprecatedRolesAssigned ? (
|
||||
<span data-test-subj="hasDeprecatedRolesAssignedHelpText">
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.users.editUser.deprecatedRolesAssignedWarning"
|
||||
defaultMessage="This user is assigned a deprecated role. Please migrate to a supported role."
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
undefined
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="secUsersEditPage">
|
||||
<EuiPageContent className="secUsersEditPage__content">
|
||||
|
@ -426,7 +440,7 @@ export class EditUserPage extends Component<Props, State> {
|
|||
onCancel={this.onCancelDelete}
|
||||
usersToDelete={[user.username]}
|
||||
callback={this.handleDelete}
|
||||
apiClient={this.props.apiClient}
|
||||
userAPIClient={this.props.userAPIClient}
|
||||
notifications={this.props.notifications}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -492,19 +506,13 @@ export class EditUserPage extends Component<Props, State> {
|
|||
'xpack.security.management.users.editUser.rolesFormRowLabel',
|
||||
{ defaultMessage: 'Roles' }
|
||||
)}
|
||||
helpText={roleHelpText}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="userFormRolesDropdown"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.security.management.users.editUser.addRolesPlaceholder',
|
||||
{ defaultMessage: 'Add roles' }
|
||||
)}
|
||||
<RoleComboBox
|
||||
availableRoles={roles}
|
||||
selectedRoleNames={selectedRoles}
|
||||
onChange={this.onRolesChange}
|
||||
isDisabled={reserved}
|
||||
options={roles.map(role => {
|
||||
return { 'data-test-subj': `roleOption-${role.name}`, label: role.name };
|
||||
})}
|
||||
selectedOptions={selectedRoles}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { User } from '../../../common/model';
|
||||
|
||||
export const isUserReserved = (user: User) => user.metadata?._reserved ?? false;
|
|
@ -5,12 +5,15 @@
|
|||
*/
|
||||
|
||||
import { User } from '../../../../common/model';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
|
||||
import { UsersGridPage } from './users_grid_page';
|
||||
import React from 'react';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { userAPIClientMock } from '../index.mock';
|
||||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { rolesAPIClientMock } from '../../roles/index.mock';
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
|
||||
describe('UsersGridPage', () => {
|
||||
it('renders the list of users', async () => {
|
||||
|
@ -39,7 +42,8 @@ describe('UsersGridPage', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<UsersGridPage
|
||||
apiClient={apiClientMock}
|
||||
userAPIClient={apiClientMock}
|
||||
rolesAPIClient={rolesAPIClientMock.create()}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
|
@ -49,6 +53,7 @@ describe('UsersGridPage', () => {
|
|||
expect(apiClientMock.getUsers).toBeCalledTimes(1);
|
||||
expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1);
|
||||
expect(wrapper.find('EuiTableRow')).toHaveLength(2);
|
||||
expect(findTestSubject(wrapper, 'userDisabled')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders a forbidden message if user is not authorized', async () => {
|
||||
|
@ -56,7 +61,11 @@ describe('UsersGridPage', () => {
|
|||
apiClient.getUsers.mockRejectedValue({ body: { statusCode: 403 } });
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<UsersGridPage apiClient={apiClient} notifications={coreMock.createStart().notifications} />
|
||||
<UsersGridPage
|
||||
userAPIClient={apiClient}
|
||||
rolesAPIClient={rolesAPIClientMock.create()}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
@ -65,10 +74,172 @@ describe('UsersGridPage', () => {
|
|||
expect(wrapper.find('[data-test-subj="permissionDeniedMessage"]')).toHaveLength(1);
|
||||
expect(wrapper.find('EuiInMemoryTable')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders disabled users', async () => {
|
||||
const apiClientMock = userAPIClientMock.create();
|
||||
apiClientMock.getUsers.mockImplementation(() => {
|
||||
return Promise.resolve<User[]>([
|
||||
{
|
||||
username: 'foo',
|
||||
email: 'foo@bar.net',
|
||||
full_name: 'foo bar',
|
||||
roles: ['kibana_user'],
|
||||
enabled: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<UsersGridPage
|
||||
userAPIClient={apiClientMock}
|
||||
rolesAPIClient={rolesAPIClientMock.create()}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(findTestSubject(wrapper, 'userDisabled')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a warning when a user is assigned a deprecated role', async () => {
|
||||
const apiClientMock = userAPIClientMock.create();
|
||||
apiClientMock.getUsers.mockImplementation(() => {
|
||||
return Promise.resolve<User[]>([
|
||||
{
|
||||
username: 'foo',
|
||||
email: 'foo@bar.net',
|
||||
full_name: 'foo bar',
|
||||
roles: ['kibana_user'],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
username: 'reserved',
|
||||
email: 'reserved@bar.net',
|
||||
full_name: '',
|
||||
roles: ['superuser'],
|
||||
enabled: true,
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const roleAPIClientMock = rolesAPIClientMock.create();
|
||||
roleAPIClientMock.getRoles.mockResolvedValue([
|
||||
{
|
||||
name: 'kibana_user',
|
||||
metadata: {
|
||||
_deprecated: true,
|
||||
_deprecated_reason: `I don't like you.`,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<UsersGridPage
|
||||
userAPIClient={apiClientMock}
|
||||
rolesAPIClient={roleAPIClientMock}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
const deprecationTooltip = wrapper.find('[data-test-subj="roleDeprecationTooltip"]').props();
|
||||
|
||||
expect(deprecationTooltip).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"children": <div>
|
||||
kibana_user
|
||||
|
||||
<EuiIcon
|
||||
className="eui-alignTop"
|
||||
color="warning"
|
||||
size="s"
|
||||
type="alert"
|
||||
/>
|
||||
</div>,
|
||||
"content": "The kibana_user role is deprecated. I don't like you.",
|
||||
"data-test-subj": "roleDeprecationTooltip",
|
||||
"delay": "regular",
|
||||
"position": "top",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('hides reserved users when instructed to', async () => {
|
||||
const apiClientMock = userAPIClientMock.create();
|
||||
apiClientMock.getUsers.mockImplementation(() => {
|
||||
return Promise.resolve<User[]>([
|
||||
{
|
||||
username: 'foo',
|
||||
email: 'foo@bar.net',
|
||||
full_name: 'foo bar',
|
||||
roles: ['kibana_user'],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
username: 'reserved',
|
||||
email: 'reserved@bar.net',
|
||||
full_name: '',
|
||||
roles: ['superuser'],
|
||||
enabled: true,
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const roleAPIClientMock = rolesAPIClientMock.create();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<UsersGridPage
|
||||
userAPIClient={apiClientMock}
|
||||
rolesAPIClient={roleAPIClientMock}
|
||||
notifications={coreMock.createStart().notifications}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(EuiBasicTable).props().items).toEqual([
|
||||
{
|
||||
username: 'foo',
|
||||
email: 'foo@bar.net',
|
||||
full_name: 'foo bar',
|
||||
roles: ['kibana_user'],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
username: 'reserved',
|
||||
email: 'reserved@bar.net',
|
||||
full_name: '',
|
||||
roles: ['superuser'],
|
||||
enabled: true,
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
findTestSubject(wrapper, 'showReservedUsersSwitch').simulate('click');
|
||||
|
||||
expect(wrapper.find(EuiBasicTable).props().items).toEqual([
|
||||
{
|
||||
username: 'foo',
|
||||
email: 'foo@bar.net',
|
||||
full_name: 'foo bar',
|
||||
roles: ['kibana_user'],
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForRender(wrapper: ReactWrapper<any, any>) {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
}
|
||||
|
|
|
@ -4,10 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiFlexGroup,
|
||||
EuiInMemoryTable,
|
||||
|
@ -18,25 +17,36 @@ import {
|
|||
EuiPageContentBody,
|
||||
EuiEmptyPrompt,
|
||||
EuiBasicTableColumn,
|
||||
EuiSwitchEvent,
|
||||
EuiSwitch,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { NotificationsStart } from 'src/core/public';
|
||||
import { User } from '../../../../common/model';
|
||||
import { User, Role } from '../../../../common/model';
|
||||
import { ConfirmDeleteUsers } from '../components';
|
||||
import { isUserReserved } from '../user_utils';
|
||||
import { DisabledBadge, ReservedBadge } from '../../badges';
|
||||
import { RoleTableDisplay } from '../../role_table_display';
|
||||
import { RolesAPIClient } from '../../roles';
|
||||
import { UserAPIClient } from '..';
|
||||
|
||||
interface Props {
|
||||
apiClient: PublicMethodsOf<UserAPIClient>;
|
||||
userAPIClient: PublicMethodsOf<UserAPIClient>;
|
||||
rolesAPIClient: PublicMethodsOf<RolesAPIClient>;
|
||||
notifications: NotificationsStart;
|
||||
}
|
||||
|
||||
interface State {
|
||||
users: User[];
|
||||
visibleUsers: User[];
|
||||
roles: null | Role[];
|
||||
selection: User[];
|
||||
showDeleteConfirmation: boolean;
|
||||
permissionDenied: boolean;
|
||||
filter: string;
|
||||
includeReservedUsers: boolean;
|
||||
}
|
||||
|
||||
export class UsersGridPage extends Component<Props, State> {
|
||||
|
@ -44,19 +54,22 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
super(props);
|
||||
this.state = {
|
||||
users: [],
|
||||
visibleUsers: [],
|
||||
roles: [],
|
||||
selection: [],
|
||||
showDeleteConfirmation: false,
|
||||
permissionDenied: false,
|
||||
filter: '',
|
||||
includeReservedUsers: true,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.loadUsers();
|
||||
this.loadUsersAndRoles();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { users, filter, permissionDenied, showDeleteConfirmation, selection } = this.state;
|
||||
const { users, roles, permissionDenied, showDeleteConfirmation, selection } = this.state;
|
||||
if (permissionDenied) {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
|
@ -86,17 +99,6 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
}
|
||||
const path = '#/management/security/';
|
||||
const columns: Array<EuiBasicTableColumn<User>> = [
|
||||
{
|
||||
field: 'full_name',
|
||||
name: i18n.translate('xpack.security.management.users.fullNameColumnName', {
|
||||
defaultMessage: 'Full Name',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render: (fullName: string) => {
|
||||
return <div data-test-subj="userRowFullName">{fullName}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'username',
|
||||
name: i18n.translate('xpack.security.management.users.userNameColumnName', {
|
||||
|
@ -110,6 +112,18 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
</EuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'full_name',
|
||||
name: i18n.translate('xpack.security.management.users.fullNameColumnName', {
|
||||
defaultMessage: 'Full Name',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render: (fullName: string) => {
|
||||
return <div data-test-subj="userRowFullName">{fullName}</div>;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
field: 'email',
|
||||
name: i18n.translate('xpack.security.management.users.emailAddressColumnName', {
|
||||
|
@ -126,34 +140,27 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
name: i18n.translate('xpack.security.management.users.rolesColumnName', {
|
||||
defaultMessage: 'Roles',
|
||||
}),
|
||||
width: '30%',
|
||||
render: (rolenames: string[]) => {
|
||||
const roleLinks = rolenames.map((rolename, index) => {
|
||||
return (
|
||||
<Fragment key={rolename}>
|
||||
<EuiLink href={`${path}roles/edit/${rolename}`}>{rolename}</EuiLink>
|
||||
{index === rolenames.length - 1 ? null : ', '}
|
||||
</Fragment>
|
||||
);
|
||||
const roleDefinition = roles?.find(role => role.name === rolename) ?? rolename;
|
||||
return <RoleTableDisplay role={roleDefinition} key={rolename} />;
|
||||
});
|
||||
return <div data-test-subj="userRowRoles">{roleLinks}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata',
|
||||
name: i18n.translate('xpack.security.management.users.reservedColumnName', {
|
||||
defaultMessage: 'Reserved',
|
||||
name: i18n.translate('xpack.security.management.users.statusColumnName', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
width: '10%',
|
||||
sortable: ({ metadata }: User) => Boolean(metadata && metadata._reserved),
|
||||
width: '100px',
|
||||
align: 'right',
|
||||
description: i18n.translate('xpack.security.management.users.reservedColumnDescription', {
|
||||
defaultMessage:
|
||||
'Reserved users are built-in and cannot be removed. Only the password can be changed.',
|
||||
}),
|
||||
render: (metadata: User['metadata']) =>
|
||||
metadata && metadata._reserved ? (
|
||||
<EuiIcon aria-label="Reserved user" data-test-subj="reservedUser" type="check" />
|
||||
) : null,
|
||||
render: (metadata: User['metadata'], record: User) => this.getUserStatusBadges(record),
|
||||
},
|
||||
];
|
||||
const pagination = {
|
||||
|
@ -170,18 +177,24 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
};
|
||||
const search = {
|
||||
toolsLeft: this.renderToolsLeft(),
|
||||
toolsRight: this.renderToolsRight(),
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
onChange: (query: any) => {
|
||||
this.setState({
|
||||
filter: query.queryText,
|
||||
visibleUsers: this.getVisibleUsers(
|
||||
this.state.users,
|
||||
query.queryText,
|
||||
this.state.includeReservedUsers
|
||||
),
|
||||
});
|
||||
},
|
||||
};
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'full_name',
|
||||
field: 'username',
|
||||
direction: 'asc',
|
||||
},
|
||||
} as const;
|
||||
|
@ -190,13 +203,7 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
'data-test-subj': 'userRow',
|
||||
};
|
||||
};
|
||||
const usersToShow = filter
|
||||
? users.filter(({ username, roles, full_name: fullName = '', email = '' }) => {
|
||||
const normalized = `${username} ${roles.join(' ')} ${fullName} ${email}`.toLowerCase();
|
||||
const normalizedQuery = filter.toLowerCase();
|
||||
return normalized.indexOf(normalizedQuery) !== -1;
|
||||
})
|
||||
: users;
|
||||
|
||||
return (
|
||||
<div className="secUsersListingPage">
|
||||
<EuiPageContent className="secUsersListingPage__content">
|
||||
|
@ -226,7 +233,7 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
onCancel={this.onCancelDelete}
|
||||
usersToDelete={selection.map(user => user.username)}
|
||||
callback={this.handleDelete}
|
||||
apiClient={this.props.apiClient}
|
||||
userAPIClient={this.props.userAPIClient}
|
||||
notifications={this.props.notifications}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -237,7 +244,7 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
columns={columns}
|
||||
selection={selectionConfig}
|
||||
pagination={pagination}
|
||||
items={usersToShow}
|
||||
items={this.state.visibleUsers}
|
||||
loading={users.length === 0}
|
||||
search={search}
|
||||
sorting={sorting}
|
||||
|
@ -262,10 +269,34 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
private async loadUsers() {
|
||||
private getVisibleUsers = (users: User[], filter: string, includeReservedUsers: boolean) => {
|
||||
return users.filter(
|
||||
({ username, roles: userRoles, full_name: fullName = '', email = '', metadata = {} }) => {
|
||||
const normalized = `${username} ${userRoles.join(' ')} ${fullName} ${email}`.toLowerCase();
|
||||
const normalizedQuery = filter.toLowerCase();
|
||||
return (
|
||||
normalized.indexOf(normalizedQuery) !== -1 &&
|
||||
(includeReservedUsers || !metadata._reserved)
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
private async loadUsersAndRoles() {
|
||||
try {
|
||||
const users = await this.props.apiClient.getUsers();
|
||||
this.setState({ users });
|
||||
const [users, roles] = await Promise.all([
|
||||
this.props.userAPIClient.getUsers(),
|
||||
this.props.rolesAPIClient.getRoles(),
|
||||
]);
|
||||
this.setState({
|
||||
users,
|
||||
roles,
|
||||
visibleUsers: this.getVisibleUsers(
|
||||
users,
|
||||
this.state.filter,
|
||||
this.state.includeReservedUsers
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.body.statusCode === 403) {
|
||||
this.setState({ permissionDenied: true });
|
||||
|
@ -303,6 +334,62 @@ export class UsersGridPage extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
private onIncludeReservedUsersChange = (e: EuiSwitchEvent) => {
|
||||
this.setState({
|
||||
includeReservedUsers: e.target.checked,
|
||||
visibleUsers: this.getVisibleUsers(this.state.users, this.state.filter, e.target.checked),
|
||||
});
|
||||
};
|
||||
|
||||
private renderToolsRight() {
|
||||
return (
|
||||
<EuiSwitch
|
||||
data-test-subj="showReservedUsersSwitch"
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.users.showReservedUsersLabel"
|
||||
defaultMessage="Show reserved users"
|
||||
/>
|
||||
}
|
||||
checked={this.state.includeReservedUsers}
|
||||
onChange={this.onIncludeReservedUsersChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private getUserStatusBadges = (user: User) => {
|
||||
const enabled = user.enabled;
|
||||
const reserved = isUserReserved(user);
|
||||
|
||||
const badges = [];
|
||||
if (!enabled) {
|
||||
badges.push(<DisabledBadge data-test-subj="userDisabled" />);
|
||||
}
|
||||
if (reserved) {
|
||||
badges.push(
|
||||
<ReservedBadge
|
||||
data-test-subj="userReserved"
|
||||
tooltipContent={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.users.reservedUserBadgeTooltip"
|
||||
defaultMessage="Reserved users are built-in and cannot be edited or removed."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
{badges.map((badge, index) => (
|
||||
<EuiFlexItem key={index} grow={false}>
|
||||
{badge}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
private onCancelDelete = () => {
|
||||
this.setState({ showDeleteConfirmation: false });
|
||||
};
|
||||
|
|
|
@ -58,7 +58,7 @@ describe('usersManagementApp', () => {
|
|||
expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Users' }]);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
Users Page: {"notifications":{"toasts":{}},"apiClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}}}
|
||||
Users Page: {"notifications":{"toasts":{}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}}}
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
@ -80,7 +80,7 @@ describe('usersManagementApp', () => {
|
|||
]);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}}}
|
||||
User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}}}
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
@ -103,7 +103,7 @@ describe('usersManagementApp', () => {
|
|||
]);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName"}
|
||||
User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName"}
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
|
|
@ -39,9 +39,16 @@ export const usersManagementApp = Object.freeze({
|
|||
];
|
||||
|
||||
const userAPIClient = new UserAPIClient(http);
|
||||
const rolesAPIClient = new RolesAPIClient(http);
|
||||
const UsersGridPageWithBreadcrumbs = () => {
|
||||
setBreadcrumbs(usersBreadcrumbs);
|
||||
return <UsersGridPage notifications={notifications} apiClient={userAPIClient} />;
|
||||
return (
|
||||
<UsersGridPage
|
||||
notifications={notifications}
|
||||
userAPIClient={userAPIClient}
|
||||
rolesAPIClient={rolesAPIClient}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const EditUserPageWithBreadcrumbs = () => {
|
||||
|
@ -61,7 +68,7 @@ export const usersManagementApp = Object.freeze({
|
|||
return (
|
||||
<EditUserPage
|
||||
authc={authc}
|
||||
apiClient={userAPIClient}
|
||||
userAPIClient={userAPIClient}
|
||||
rolesAPIClient={new RolesAPIClient(http)}
|
||||
notifications={notifications}
|
||||
username={username}
|
||||
|
|
|
@ -128,7 +128,7 @@ export class SecurityPlugin
|
|||
<AccountManagementPage
|
||||
authc={this.authc}
|
||||
notifications={core.notifications}
|
||||
apiClient={new UserAPIClient(core.http)}
|
||||
userAPIClient={new UserAPIClient(core.http)}
|
||||
/>
|
||||
</core.i18n.Context>
|
||||
),
|
||||
|
|
|
@ -10662,14 +10662,10 @@
|
|||
"xpack.security.management.roles.deleteSelectedRolesButtonLabel": "ロール {numSelected} {numSelected, plural, one { } other {}} を削除しました",
|
||||
"xpack.security.management.roles.deletingRolesWarningMessage": "この操作は元に戻すことができません。",
|
||||
"xpack.security.management.roles.deniedPermissionTitle": "ロールを管理するにはパーミッションが必要です",
|
||||
"xpack.security.management.roles.disabledTooltip": " (無効)",
|
||||
"xpack.security.management.roles.editRoleActionName": "{roleName} を編集",
|
||||
"xpack.security.management.roles.fetchingRolesErrorMessage": "ロールの取得中にエラーが発生: {message}",
|
||||
"xpack.security.management.roles.nameColumnName": "ロール",
|
||||
"xpack.security.management.roles.noPermissionToManageRolesDescription": "システム管理者にお問い合わせください。",
|
||||
"xpack.security.management.roles.reservedColumnDescription": "リザーブされたロールはビルトインのため削除または変更できません。",
|
||||
"xpack.security.management.roles.reservedColumnName": "リザーブ",
|
||||
"xpack.security.management.roles.reservedRoleIconLabel": "指定済みロール",
|
||||
"xpack.security.management.roles.roleNotFound": "「{roleName}」ロールが見つかりません。",
|
||||
"xpack.security.management.roles.roleTitle": "ロール",
|
||||
"xpack.security.management.roles.subtitle": "ユーザーのグループにロールを適用してスタック全体のパーミッションを管理",
|
||||
|
@ -10720,7 +10716,6 @@
|
|||
"xpack.security.management.users.fullNameColumnName": "フルネーム",
|
||||
"xpack.security.management.users.permissionDeniedToManageUsersDescription": "システム管理者にお問い合わせください。",
|
||||
"xpack.security.management.users.reservedColumnDescription": "リザーブされたユーザーはビルトインのため削除できません。パスワードのみ変更できます。",
|
||||
"xpack.security.management.users.reservedColumnName": "リザーブ",
|
||||
"xpack.security.management.users.rolesColumnName": "ロール",
|
||||
"xpack.security.management.users.userNameColumnName": "ユーザー名",
|
||||
"xpack.security.management.users.usersTitle": "ユーザー",
|
||||
|
|
|
@ -10662,14 +10662,10 @@
|
|||
"xpack.security.management.roles.deleteSelectedRolesButtonLabel": "删除 {numSelected} 个角色{numSelected, plural, one {} other {}}",
|
||||
"xpack.security.management.roles.deletingRolesWarningMessage": "此操作无法撤消。",
|
||||
"xpack.security.management.roles.deniedPermissionTitle": "您需要用于管理角色的权限",
|
||||
"xpack.security.management.roles.disabledTooltip": " (已禁用)",
|
||||
"xpack.security.management.roles.editRoleActionName": "编辑 {roleName}",
|
||||
"xpack.security.management.roles.fetchingRolesErrorMessage": "获取用户时出错:{message}",
|
||||
"xpack.security.management.roles.nameColumnName": "角色",
|
||||
"xpack.security.management.roles.noPermissionToManageRolesDescription": "请联系您的管理员。",
|
||||
"xpack.security.management.roles.reservedColumnDescription": "保留角色为内置角色,不能编辑或移除。",
|
||||
"xpack.security.management.roles.reservedColumnName": "保留",
|
||||
"xpack.security.management.roles.reservedRoleIconLabel": "保留角色",
|
||||
"xpack.security.management.roles.roleNotFound": "未找到任何“{roleName}”。",
|
||||
"xpack.security.management.roles.roleTitle": "角色",
|
||||
"xpack.security.management.roles.subtitle": "将角色应用到用户组并管理整个堆栈的权限。",
|
||||
|
@ -10720,7 +10716,6 @@
|
|||
"xpack.security.management.users.fullNameColumnName": "全名",
|
||||
"xpack.security.management.users.permissionDeniedToManageUsersDescription": "请联系您的管理员。",
|
||||
"xpack.security.management.users.reservedColumnDescription": "保留的用户是内置的,无法删除。只能更改密码。",
|
||||
"xpack.security.management.users.reservedColumnName": "保留",
|
||||
"xpack.security.management.users.rolesColumnName": "角色",
|
||||
"xpack.security.management.users.userNameColumnName": "用户名",
|
||||
"xpack.security.management.users.usersTitle": "用户",
|
||||
|
|
|
@ -28,7 +28,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
it('allows a role mapping to be created', async () => {
|
||||
await testSubjects.click('createRoleMappingButton');
|
||||
await testSubjects.setValue('roleMappingFormNameInput', 'new_role_mapping');
|
||||
await testSubjects.setValue('roleMappingFormRoleComboBox', 'superuser');
|
||||
await testSubjects.setValue('rolesDropdown', 'superuser');
|
||||
await browser.pressKeys(browser.keys.ENTER);
|
||||
|
||||
await testSubjects.click('roleMappingsAddRuleButton');
|
||||
|
|
|
@ -82,13 +82,34 @@ export default function({ getService, getPageObjects }) {
|
|||
log.debug('actualRoles = %j', roles);
|
||||
// This only contains the first page of alphabetically sorted results, so the assertions are only for the first handful of expected roles.
|
||||
expect(roles.apm_system.reserved).to.be(true);
|
||||
expect(roles.apm_system.deprecated).to.be(false);
|
||||
|
||||
expect(roles.apm_user.reserved).to.be(true);
|
||||
expect(roles.apm_user.deprecated).to.be(false);
|
||||
|
||||
expect(roles.beats_admin.reserved).to.be(true);
|
||||
expect(roles.beats_admin.deprecated).to.be(false);
|
||||
|
||||
expect(roles.beats_system.reserved).to.be(true);
|
||||
expect(roles.beats_system.deprecated).to.be(false);
|
||||
|
||||
expect(roles.kibana_admin.reserved).to.be(true);
|
||||
expect(roles.kibana_admin.deprecated).to.be(false);
|
||||
|
||||
expect(roles.kibana_user.reserved).to.be(true);
|
||||
expect(roles.kibana_user.deprecated).to.be(true);
|
||||
|
||||
expect(roles.kibana_dashboard_only_user.reserved).to.be(true);
|
||||
expect(roles.kibana_dashboard_only_user.deprecated).to.be(true);
|
||||
|
||||
expect(roles.kibana_system.reserved).to.be(true);
|
||||
expect(roles.kibana_system.deprecated).to.be(false);
|
||||
|
||||
expect(roles.logstash_system.reserved).to.be(true);
|
||||
expect(roles.logstash_system.deprecated).to.be(false);
|
||||
|
||||
expect(roles.monitoring_user.reserved).to.be(true);
|
||||
expect(roles.monitoring_user.deprecated).to.be(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -232,16 +232,16 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
|
|||
const usernameElement = await user.findByCssSelector('[data-test-subj="userRowUserName"]');
|
||||
const emailElement = await user.findByCssSelector('[data-test-subj="userRowEmail"]');
|
||||
const rolesElement = await user.findByCssSelector('[data-test-subj="userRowRoles"]');
|
||||
const isReservedElementVisible = await user.findByCssSelector('td:last-child');
|
||||
// findAllByCssSelector is substantially faster than `find.descendantExistsByCssSelector for negative cases
|
||||
const isUserReserved =
|
||||
(await user.findAllByCssSelector('span[data-test-subj="userReserved"]', 1)).length > 0;
|
||||
|
||||
return {
|
||||
username: await usernameElement.getVisibleText(),
|
||||
fullname: await fullnameElement.getVisibleText(),
|
||||
email: await emailElement.getVisibleText(),
|
||||
roles: (await rolesElement.getVisibleText()).split(',').map(role => role.trim()),
|
||||
reserved: (await isReservedElementVisible.getAttribute('innerHTML')).includes(
|
||||
'reservedUser'
|
||||
),
|
||||
roles: (await rolesElement.getVisibleText()).split('\n').map(role => role.trim()),
|
||||
reserved: isUserReserved,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -249,15 +249,22 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
|
|||
async getElasticsearchRoles() {
|
||||
const users = await testSubjects.findAll('roleRow');
|
||||
return mapAsync(users, async role => {
|
||||
const rolenameElement = await role.findByCssSelector('[data-test-subj="roleRowName"]');
|
||||
const reservedRoleRow = await role.findByCssSelector('td:nth-last-child(2)');
|
||||
const [rolename, reserved, deprecated] = await Promise.all([
|
||||
role.findByCssSelector('[data-test-subj="roleRowName"]').then(el => el.getVisibleText()),
|
||||
// findAllByCssSelector is substantially faster than `find.descendantExistsByCssSelector for negative cases
|
||||
role
|
||||
.findAllByCssSelector('span[data-test-subj="roleReserved"]', 1)
|
||||
.then(el => el.length > 0),
|
||||
// findAllByCssSelector is substantially faster than `find.descendantExistsByCssSelector for negative cases
|
||||
role
|
||||
.findAllByCssSelector('span[data-test-subj="roleDeprecated"]', 1)
|
||||
.then(el => el.length > 0),
|
||||
]);
|
||||
|
||||
return {
|
||||
rolename: await rolenameElement.getVisibleText(),
|
||||
reserved: await find.descendantExistsByCssSelector(
|
||||
'[data-test-subj="reservedRole"]',
|
||||
reservedRoleRow
|
||||
),
|
||||
rolename,
|
||||
reserved,
|
||||
deprecated,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -400,7 +407,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
|
|||
}
|
||||
|
||||
async selectRole(role) {
|
||||
const dropdown = await testSubjects.find('userFormRolesDropdown');
|
||||
const dropdown = await testSubjects.find('rolesDropdown');
|
||||
const input = await dropdown.findByCssSelector('input');
|
||||
await input.type(role);
|
||||
await testSubjects.click(`roleOption-${role}`);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue