mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Roles] Use Query Roles API for Role Management grid screen (#194630)
Closes https://github.com/elastic/kibana/issues/186266 ## Release notes Enhanced Role management to manage larger number of roles by adding server side filtering, pagination and querying. ## Summary - Replaced the usage of Get Roles API with Query Role API - Added server side pagination and filtering with a maximum limit of 10000 keys (default for max results on index). Added new label to indicate that we show only 10k results. - Search box replicates client side implementation by only filtering on Role names. ### Run locally Start ES ~with the JVM option to enable this feature~: ``` yarn es snapshot --license=trial ``` Start Kibana normally ``` yarn start --no-base-path ``` Navigate to Stack Management > Roles and verify the same behavior as the screen recording below ### Screen recording https://github.com/user-attachments/assets/a447e7df-8aa1-4044-a6b2-0aafe56844a9 ## Technical notes - Client side EuiInMemory table has been replaced by EuiSearchBar, EuiBasicTable and Filters - One new Kibana endpoint added - `roles/_query` - Replicates existing get_role endpoint by being public and added to Open API spec - Extra logic to handle previously UI only filter to show/hide reserved roles - Parse the query to construct the correct DSL if the filter is present - Update Get All Roles by Space internal API to use the Query Role and filter by space id using query DSL. ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4da814d138
commit
66dab0ae0e
21 changed files with 1374 additions and 478 deletions
|
@ -40769,6 +40769,80 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/security/role/_query": {
|
||||
"post": {
|
||||
"operationId": "post-security-role-query",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A required header to protect against CSRF attacks",
|
||||
"in": "header",
|
||||
"name": "kbn-xsrf",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"example": "true",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"filters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"showReservedRoles": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"from": {
|
||||
"type": "number"
|
||||
},
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "number"
|
||||
},
|
||||
"sort": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"direction": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"field",
|
||||
"direction"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Indicates a successful call."
|
||||
}
|
||||
},
|
||||
"summary": "Query roles",
|
||||
"tags": []
|
||||
}
|
||||
},
|
||||
"/api/security/role/{name}": {
|
||||
"delete": {
|
||||
"operationId": "delete-security-role-name",
|
||||
|
|
|
@ -40769,6 +40769,80 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/security/role/_query": {
|
||||
"post": {
|
||||
"operationId": "post-security-role-query",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A required header to protect against CSRF attacks",
|
||||
"in": "header",
|
||||
"name": "kbn-xsrf",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"example": "true",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"filters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"showReservedRoles": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"from": {
|
||||
"type": "number"
|
||||
},
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "number"
|
||||
},
|
||||
"sort": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"direction": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"field",
|
||||
"direction"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Indicates a successful call."
|
||||
}
|
||||
},
|
||||
"summary": "Query roles",
|
||||
"tags": []
|
||||
}
|
||||
},
|
||||
"/api/security/role/{name}": {
|
||||
"delete": {
|
||||
"operationId": "delete-security-role-name",
|
||||
|
|
|
@ -37827,6 +37827,56 @@ paths:
|
|||
tags:
|
||||
- roles
|
||||
x-beta: true
|
||||
/api/security/role/_query:
|
||||
post:
|
||||
operationId: post-security-role-query
|
||||
parameters:
|
||||
- description: A required header to protect against CSRF attacks
|
||||
in: header
|
||||
name: kbn-xsrf
|
||||
required: true
|
||||
schema:
|
||||
example: 'true'
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
filters:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
showReservedRoles:
|
||||
type: boolean
|
||||
from:
|
||||
type: number
|
||||
query:
|
||||
type: string
|
||||
size:
|
||||
type: number
|
||||
sort:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
direction:
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
type: string
|
||||
field:
|
||||
type: string
|
||||
required:
|
||||
- field
|
||||
- direction
|
||||
responses:
|
||||
'200':
|
||||
description: Indicates a successful call.
|
||||
summary: Query roles
|
||||
tags: []
|
||||
x-beta: true
|
||||
/api/security/role/{name}:
|
||||
delete:
|
||||
operationId: delete-security-role-name
|
||||
|
|
|
@ -40299,6 +40299,55 @@ paths:
|
|||
summary: Get all roles
|
||||
tags:
|
||||
- roles
|
||||
/api/security/role/_query:
|
||||
post:
|
||||
operationId: post-security-role-query
|
||||
parameters:
|
||||
- description: A required header to protect against CSRF attacks
|
||||
in: header
|
||||
name: kbn-xsrf
|
||||
required: true
|
||||
schema:
|
||||
example: 'true'
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
filters:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
showReservedRoles:
|
||||
type: boolean
|
||||
from:
|
||||
type: number
|
||||
query:
|
||||
type: string
|
||||
size:
|
||||
type: number
|
||||
sort:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
direction:
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
type: string
|
||||
field:
|
||||
type: string
|
||||
required:
|
||||
- field
|
||||
- direction
|
||||
responses:
|
||||
'200':
|
||||
description: Indicates a successful call.
|
||||
summary: Query roles
|
||||
tags: []
|
||||
/api/security/role/{name}:
|
||||
delete:
|
||||
operationId: delete-security-role-name
|
||||
|
|
|
@ -12,6 +12,8 @@ export type {
|
|||
AuthenticationProvider,
|
||||
} from './src/authentication';
|
||||
export type {
|
||||
QueryRolesRole,
|
||||
QueryRolesResult,
|
||||
RemoteClusterPrivilege,
|
||||
Role,
|
||||
RoleIndexPrivilege,
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
export type { FeaturesPrivileges } from './features_privileges';
|
||||
export type { RawKibanaFeaturePrivileges, RawKibanaPrivileges } from './raw_kibana_privileges';
|
||||
export type {
|
||||
QueryRolesRole,
|
||||
QueryRolesResult,
|
||||
RemoteClusterPrivilege,
|
||||
Role,
|
||||
RoleKibanaPrivilege,
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
|
||||
import type { FeaturesPrivileges } from './features_privileges';
|
||||
|
||||
export interface RoleIndexPrivilege {
|
||||
|
@ -55,3 +57,11 @@ export interface Role {
|
|||
_transform_error?: string[];
|
||||
_unrecognized_applications?: string[];
|
||||
}
|
||||
|
||||
export type QueryRolesRole = estypes.SecurityQueryRoleQueryRole;
|
||||
|
||||
export interface QueryRolesResult {
|
||||
roles: Role[];
|
||||
count: number;
|
||||
total: number;
|
||||
}
|
||||
|
|
|
@ -12,5 +12,6 @@ export const rolesAPIClientMock = {
|
|||
deleteRole: jest.fn(),
|
||||
saveRole: jest.fn(),
|
||||
bulkUpdateRoles: jest.fn(),
|
||||
queryRoles: jest.fn(),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -5,13 +5,26 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Criteria } from '@elastic/eui';
|
||||
|
||||
import type { HttpStart } from '@kbn/core/public';
|
||||
import type { QueryRolesResult } from '@kbn/security-plugin-types-common';
|
||||
import type { BulkUpdatePayload, BulkUpdateRoleResponse } from '@kbn/security-plugin-types-public';
|
||||
|
||||
import type { Role, RoleIndexPrivilege, RoleRemoteIndexPrivilege } from '../../../common';
|
||||
import { API_VERSIONS } from '../../../common/constants';
|
||||
import { copyRole } from '../../../common/model';
|
||||
|
||||
export interface QueryRoleParams {
|
||||
query: string;
|
||||
from: number;
|
||||
size: number;
|
||||
filters?: {
|
||||
showReservedRoles?: boolean;
|
||||
};
|
||||
sort: Criteria<Role>['sort'];
|
||||
}
|
||||
|
||||
const version = API_VERSIONS.roles.public.v1;
|
||||
|
||||
export class RolesAPIClient {
|
||||
|
@ -24,6 +37,13 @@ export class RolesAPIClient {
|
|||
});
|
||||
};
|
||||
|
||||
public queryRoles = async (params?: QueryRoleParams) => {
|
||||
return await this.http.post<QueryRolesResult>(`/api/security/role/_query`, {
|
||||
version,
|
||||
body: JSON.stringify(params || {}),
|
||||
});
|
||||
};
|
||||
|
||||
public getRole = async (roleName: string) => {
|
||||
return await this.http.get<Role>(`/api/security/role/${encodeURIComponent(roleName)}`, {
|
||||
version,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiIcon, EuiInMemoryTable } from '@elastic/eui';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import type { ReactWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
|
@ -51,36 +51,40 @@ describe('<RolesGridPage />', () => {
|
|||
history.createHref.mockImplementation((location) => location.pathname!);
|
||||
|
||||
apiClientMock = rolesAPIClientMock.create();
|
||||
apiClientMock.getRoles.mockResolvedValue([
|
||||
{
|
||||
name: 'test-role-1',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
{
|
||||
name: 'test-role-with-description',
|
||||
description: 'role-description',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
{
|
||||
name: 'reserved-role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
metadata: { _reserved: true },
|
||||
},
|
||||
{
|
||||
name: 'disabled-role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
transient_metadata: { enabled: false },
|
||||
},
|
||||
{
|
||||
name: 'special%chars%role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
]);
|
||||
apiClientMock.queryRoles.mockResolvedValue({
|
||||
total: 5,
|
||||
count: 5,
|
||||
roles: [
|
||||
{
|
||||
name: 'test-role-1',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
{
|
||||
name: 'test-role-with-description',
|
||||
description: 'role-description',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
{
|
||||
name: 'reserved-role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
metadata: { _reserved: true },
|
||||
},
|
||||
{
|
||||
name: 'disabled-role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
transient_metadata: { enabled: false },
|
||||
},
|
||||
{
|
||||
name: 'special%chars%role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it(`renders reserved roles as such`, async () => {
|
||||
|
@ -130,7 +134,7 @@ describe('<RolesGridPage />', () => {
|
|||
});
|
||||
|
||||
it('renders permission denied if required', async () => {
|
||||
apiClientMock.getRoles.mockRejectedValue(mock403());
|
||||
apiClientMock.queryRoles.mockRejectedValue(mock403());
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<RolesGridPage
|
||||
|
@ -195,248 +199,6 @@ describe('<RolesGridPage />', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('hides reserved roles when instructed to', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RolesGridPage
|
||||
rolesAPIClient={apiClientMock}
|
||||
history={history}
|
||||
notifications={notifications}
|
||||
i18n={i18n}
|
||||
buildFlavor={'traditional'}
|
||||
analytics={analytics}
|
||||
theme={theme}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
);
|
||||
const initialIconCount = wrapper.find(EuiIcon).length;
|
||||
|
||||
await waitForRender(wrapper, (updatedWrapper) => {
|
||||
return updatedWrapper.find(EuiIcon).length > initialIconCount;
|
||||
});
|
||||
|
||||
expect(wrapper.find(EuiInMemoryTable).props().items).toEqual([
|
||||
{
|
||||
name: 'test-role-1',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'test-role-with-description',
|
||||
description: 'role-description',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'reserved-role',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'disabled-role',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
transient_metadata: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'special%chars%role',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
findTestSubject(wrapper, 'showReservedRolesSwitch').simulate('click');
|
||||
|
||||
expect(wrapper.find(EuiInMemoryTable).props().items).toEqual([
|
||||
{
|
||||
name: 'test-role-1',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
{
|
||||
name: 'test-role-with-description',
|
||||
description: 'role-description',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
{
|
||||
name: 'disabled-role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
transient_metadata: { enabled: false },
|
||||
},
|
||||
{
|
||||
name: 'special%chars%role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [{ base: [], spaces: [], feature: {} }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts columns on clicking the column header', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RolesGridPage
|
||||
rolesAPIClient={apiClientMock}
|
||||
history={history}
|
||||
notifications={notifications}
|
||||
i18n={i18n}
|
||||
buildFlavor={'traditional'}
|
||||
analytics={analytics}
|
||||
theme={theme}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
);
|
||||
const initialIconCount = wrapper.find(EuiIcon).length;
|
||||
|
||||
await waitForRender(wrapper, (updatedWrapper) => {
|
||||
return updatedWrapper.find(EuiIcon).length > initialIconCount;
|
||||
});
|
||||
|
||||
expect(wrapper.find(EuiInMemoryTable).props().items).toEqual([
|
||||
{
|
||||
name: 'test-role-1',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'test-role-with-description',
|
||||
description: 'role-description',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'reserved-role',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'disabled-role',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
transient_metadata: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'special%chars%role',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
findTestSubject(wrapper, 'tableHeaderCell_name_0').simulate('click');
|
||||
|
||||
const firstRowElement = findTestSubject(wrapper, 'roleRowName').first();
|
||||
expect(firstRowElement.text()).toBe('disabled-role');
|
||||
});
|
||||
|
||||
it('hides controls when `readOnly` is enabled', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RolesGridPage
|
||||
|
|
|
@ -5,13 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiBasicTableColumn, EuiSwitchEvent } from '@elastic/eui';
|
||||
import type {
|
||||
Criteria,
|
||||
CriteriaWithPagination,
|
||||
EuiBasicTableColumn,
|
||||
EuiSearchBarOnChangeArgs,
|
||||
EuiSwitchEvent,
|
||||
Query,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiLink,
|
||||
EuiSearchBar,
|
||||
EuiSpacer,
|
||||
|
@ -28,6 +35,7 @@ import type { NotificationsStart, ScopedHistory } from '@kbn/core/public';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
|
||||
import type { QueryRolesResult } from '@kbn/security-plugin-types-common';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
|
||||
|
@ -54,18 +62,33 @@ export interface Props extends StartServices {
|
|||
cloudOrgUrl?: string;
|
||||
}
|
||||
|
||||
interface RolesTableState {
|
||||
query: Query;
|
||||
sort: Criteria<Role>['sort'];
|
||||
from: number;
|
||||
size: number;
|
||||
filters: {
|
||||
showReservedRoles?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => {
|
||||
return `/${action}${roleName ? `/${encodeURIComponent(roleName)}` : ''}`;
|
||||
};
|
||||
|
||||
const 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))
|
||||
);
|
||||
});
|
||||
const MAX_PAGINATED_ITEMS = 10000;
|
||||
|
||||
const DEFAULT_TABLE_STATE = {
|
||||
query: EuiSearchBar.Query.MATCH_ALL,
|
||||
sort: {
|
||||
field: 'name' as const,
|
||||
direction: 'asc' as const,
|
||||
},
|
||||
from: 0,
|
||||
size: 25,
|
||||
filters: {
|
||||
showReservedRoles: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const RolesGridPage: FC<Props> = ({
|
||||
|
@ -77,25 +100,28 @@ export const RolesGridPage: FC<Props> = ({
|
|||
cloudOrgUrl,
|
||||
...startServices
|
||||
}) => {
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [visibleRoles, setVisibleRoles] = useState<Role[]>([]);
|
||||
const [rolesResponse, setRolesResponse] = useState<QueryRolesResult>({} as QueryRolesResult);
|
||||
|
||||
const [selection, setSelection] = useState<Role[]>([]);
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
|
||||
const [permissionDenied, setPermissionDenied] = useState<boolean>(false);
|
||||
const [includeReservedRoles, setIncludeReservedRoles] = useState<boolean>(true);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadRoles();
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
const [tableState, setTableState] = useState<RolesTableState>(DEFAULT_TABLE_STATE);
|
||||
|
||||
const loadRoles = async (tableStateArgs: RolesTableState) => {
|
||||
const queryText = tableStateArgs.query.text;
|
||||
|
||||
const requestBody = {
|
||||
...tableStateArgs,
|
||||
...(tableStateArgs.sort ? { sort: tableStateArgs.sort } : DEFAULT_TABLE_STATE.sort),
|
||||
query: queryText,
|
||||
};
|
||||
|
||||
const loadRoles = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const rolesFromApi = await rolesAPIClient.getRoles();
|
||||
setRoles(rolesFromApi);
|
||||
setVisibleRoles(getVisibleRoles(rolesFromApi, filter, includeReservedRoles));
|
||||
const rolesFromApi = await rolesAPIClient.queryRoles(requestBody);
|
||||
setRolesResponse(rolesFromApi);
|
||||
} catch (e) {
|
||||
if (_.get(e, 'body.statusCode') === 403) {
|
||||
setPermissionDenied(true);
|
||||
|
@ -112,9 +138,19 @@ export const RolesGridPage: FC<Props> = ({
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadRoles(DEFAULT_TABLE_STATE);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const onIncludeReservedRolesChange = (e: EuiSwitchEvent) => {
|
||||
setIncludeReservedRoles(e.target.checked);
|
||||
setVisibleRoles(getVisibleRoles(roles, filter, e.target.checked));
|
||||
const newTableStateArgs = {
|
||||
...tableState,
|
||||
filters: {
|
||||
showReservedRoles: e.target.checked,
|
||||
},
|
||||
};
|
||||
setTableState(newTableStateArgs);
|
||||
loadRoles(newTableStateArgs);
|
||||
};
|
||||
|
||||
const getRoleStatusBadges = (role: Role) => {
|
||||
|
@ -162,7 +198,7 @@ export const RolesGridPage: FC<Props> = ({
|
|||
const handleDelete = () => {
|
||||
setSelection([]);
|
||||
setShowDeleteConfirmation(false);
|
||||
loadRoles();
|
||||
loadRoles(tableState);
|
||||
};
|
||||
|
||||
const deleteOneRole = (roleToDelete: Role) => {
|
||||
|
@ -203,13 +239,35 @@ export const RolesGridPage: FC<Props> = ({
|
|||
defaultMessage="Show reserved roles"
|
||||
/>
|
||||
}
|
||||
checked={includeReservedRoles}
|
||||
checked={tableState.filters.showReservedRoles ?? true}
|
||||
onChange={onIncludeReservedRolesChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onTableChange = ({ page, sort }: CriteriaWithPagination<Role>) => {
|
||||
const newState = {
|
||||
...tableState,
|
||||
from: page?.index! * page?.size!,
|
||||
size: page?.size!,
|
||||
sort: sort ?? tableState.sort,
|
||||
};
|
||||
setTableState(newState);
|
||||
loadRoles(newState);
|
||||
};
|
||||
|
||||
const onSearchChange = (args: EuiSearchBarOnChangeArgs) => {
|
||||
if (!args.error) {
|
||||
const newState = {
|
||||
...tableState,
|
||||
query: args.query,
|
||||
};
|
||||
setTableState(newState);
|
||||
loadRoles(newState);
|
||||
}
|
||||
};
|
||||
|
||||
const getColumnConfig = (): Array<EuiBasicTableColumn<Role>> => {
|
||||
const config: Array<EuiBasicTableColumn<Role>> = [
|
||||
{
|
||||
|
@ -234,7 +292,7 @@ export const RolesGridPage: FC<Props> = ({
|
|||
name: i18n.translate('xpack.security.management.roles.descriptionColumnName', {
|
||||
defaultMessage: 'Role Description',
|
||||
}),
|
||||
sortable: true,
|
||||
sortable: false,
|
||||
truncateText: { lines: 3 },
|
||||
render: (description: string, record: Role) => (
|
||||
<EuiToolTip position="top" content={description} display="block">
|
||||
|
@ -251,7 +309,7 @@ export const RolesGridPage: FC<Props> = ({
|
|||
name: i18n.translate('xpack.security.management.roles.statusColumnName', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
sortable: (role: Role) => isRoleEnabled(role) && !isRoleDeprecated(role),
|
||||
sortable: false,
|
||||
render: (_metadata: Role['metadata'], record: Role) => getRoleStatusBadges(record),
|
||||
});
|
||||
}
|
||||
|
@ -331,6 +389,19 @@ export const RolesGridPage: FC<Props> = ({
|
|||
setShowDeleteConfirmation(false);
|
||||
};
|
||||
|
||||
const tableItems = rolesResponse.roles ?? [];
|
||||
const totalItemCount = rolesResponse.total ?? 0;
|
||||
|
||||
const displayedItemCount = Math.min(totalItemCount, MAX_PAGINATED_ITEMS);
|
||||
|
||||
const pagination = {
|
||||
pageIndex: tableState.from / tableState.size,
|
||||
pageSize: tableState.size,
|
||||
totalItemCount: displayedItemCount,
|
||||
pageSizeOptions: [25, 50, 100],
|
||||
};
|
||||
const exceededResultCount = totalItemCount > MAX_PAGINATED_ITEMS;
|
||||
|
||||
return permissionDenied ? (
|
||||
<PermissionDenied />
|
||||
) : (
|
||||
|
@ -416,16 +487,25 @@ export const RolesGridPage: FC<Props> = ({
|
|||
incremental: true,
|
||||
'data-test-subj': 'searchRoles',
|
||||
}}
|
||||
onChange={(query: Record<string, any>) => {
|
||||
setFilter(query.queryText);
|
||||
setVisibleRoles(getVisibleRoles(roles, query.queryText, includeReservedRoles));
|
||||
}}
|
||||
onChange={onSearchChange}
|
||||
toolsLeft={renderToolsLeft()}
|
||||
toolsRight={renderToolsRight()}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiInMemoryTable
|
||||
data-test-subj="rolesTable"
|
||||
{exceededResultCount && (
|
||||
<>
|
||||
<EuiText color="subdued" size="s" data-test-subj="rolesTableTooManyResultsLabel">
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.roles.table.tooManyResultsLabel"
|
||||
defaultMessage="Showing {limit} of {totalItemCount, plural, one {# role} other {# roles}}"
|
||||
values={{ totalItemCount, limit: MAX_PAGINATED_ITEMS }}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
<EuiBasicTable
|
||||
data-test-subj={`${!isLoading ? 'rolesTable' : 'rolesTableLoading'}`}
|
||||
itemId="name"
|
||||
columns={getColumnConfig()}
|
||||
selection={
|
||||
|
@ -439,11 +519,9 @@ export const RolesGridPage: FC<Props> = ({
|
|||
selected: selection,
|
||||
}
|
||||
}
|
||||
pagination={{
|
||||
initialPageSize: 20,
|
||||
pageSizeOptions: [10, 20, 30, 50, 100],
|
||||
}}
|
||||
message={
|
||||
onChange={onTableChange}
|
||||
pagination={pagination}
|
||||
noItemsMessage={
|
||||
buildFlavor === 'serverless' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.roles.noCustomRolesFound"
|
||||
|
@ -456,13 +534,10 @@ export const RolesGridPage: FC<Props> = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
items={visibleRoles}
|
||||
items={tableItems}
|
||||
loading={isLoading}
|
||||
sorting={{
|
||||
sort: {
|
||||
field: 'name',
|
||||
direction: 'asc',
|
||||
},
|
||||
sort: tableState.sort,
|
||||
}}
|
||||
rowProps={{ 'data-test-subj': 'roleRow' }}
|
||||
/>
|
||||
|
|
|
@ -179,7 +179,7 @@ describe('GET all roles by space id', () => {
|
|||
});
|
||||
|
||||
if (apiResponse) {
|
||||
mockCoreContext.elasticsearch.client.asCurrentUser.security.getRole.mockResponseImplementation(
|
||||
mockCoreContext.elasticsearch.client.asCurrentUser.security.queryRole.mockResponseImplementation(
|
||||
(() => ({ body: apiResponse() })) as any
|
||||
);
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ describe('GET all roles by space id', () => {
|
|||
|
||||
if (apiResponse) {
|
||||
expect(
|
||||
mockCoreContext.elasticsearch.client.asCurrentUser.security.getRole
|
||||
mockCoreContext.elasticsearch.client.asCurrentUser.security.queryRole
|
||||
).toHaveBeenCalled();
|
||||
}
|
||||
expect(mockLicensingContext.license.check).toHaveBeenCalledWith('security', 'basic');
|
||||
|
@ -226,24 +226,29 @@ describe('GET all roles by space id', () => {
|
|||
|
||||
getRolesTest(`returns error if we have empty resources`, {
|
||||
apiResponse: () => ({
|
||||
first_role: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['read'],
|
||||
resources: [],
|
||||
total: 1,
|
||||
count: 1,
|
||||
roles: [
|
||||
{
|
||||
name: 'first_role',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['read'],
|
||||
resources: [],
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
asserts: {
|
||||
statusCode: 500,
|
||||
|
@ -255,29 +260,34 @@ describe('GET all roles by space id', () => {
|
|||
describe('success', () => {
|
||||
getRolesTest(`returns empty roles list if there is no space match`, {
|
||||
apiResponse: () => ({
|
||||
first_role: {
|
||||
cluster: ['manage_watcher'],
|
||||
indices: [
|
||||
{
|
||||
names: ['.kibana*'],
|
||||
privileges: ['read', 'view_index_metadata'],
|
||||
total: 1,
|
||||
count: 1,
|
||||
roles: [
|
||||
{
|
||||
name: 'first_role',
|
||||
cluster: ['manage_watcher'],
|
||||
indices: [
|
||||
{
|
||||
names: ['.kibana*'],
|
||||
privileges: ['read', 'view_index_metadata'],
|
||||
},
|
||||
],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
},
|
||||
],
|
||||
run_as: ['other_user'],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
run_as: ['other_user'],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
asserts: {
|
||||
statusCode: 200,
|
||||
|
@ -287,48 +297,54 @@ describe('GET all roles by space id', () => {
|
|||
|
||||
getRolesTest(`returns roles for matching space`, {
|
||||
apiResponse: () => ({
|
||||
first_role: {
|
||||
description: 'first role description',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
total: 2,
|
||||
count: 2,
|
||||
roles: [
|
||||
{
|
||||
name: 'first_role',
|
||||
description: 'first role description',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
},
|
||||
{
|
||||
application,
|
||||
privileges: ['space_read'],
|
||||
resources: ['space:engineering'],
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
{
|
||||
application,
|
||||
privileges: ['space_read'],
|
||||
resources: ['space:engineering'],
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
second_role: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
{
|
||||
name: 'second_role',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
spaceId: 'engineering',
|
||||
asserts: {
|
||||
|
@ -369,43 +385,49 @@ describe('GET all roles by space id', () => {
|
|||
|
||||
getRolesTest(`returns roles with access to all spaces`, {
|
||||
apiResponse: () => ({
|
||||
first_role: {
|
||||
description: 'first role description',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['all', 'read'],
|
||||
resources: ['*'],
|
||||
total: 2,
|
||||
count: 2,
|
||||
roles: [
|
||||
{
|
||||
name: 'first_role',
|
||||
description: 'first role description',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['all', 'read'],
|
||||
resources: ['*'],
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
second_role: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
{
|
||||
name: 'second_role',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
asserts: {
|
||||
statusCode: 200,
|
||||
|
@ -440,46 +462,53 @@ describe('GET all roles by space id', () => {
|
|||
|
||||
getRolesTest(`filters roles with reserved only privileges`, {
|
||||
apiResponse: () => ({
|
||||
first_role: {
|
||||
description: 'first role description',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
second_role: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
total: 3,
|
||||
count: 3,
|
||||
roles: [
|
||||
{
|
||||
name: 'first_role',
|
||||
description: 'first role description',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
{
|
||||
name: 'second_role',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
third_role: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [],
|
||||
run_as: [],
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
{
|
||||
name: 'third_role',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [],
|
||||
run_as: [],
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
spaceId: 'marketing',
|
||||
asserts: {
|
||||
|
@ -517,20 +546,25 @@ describe('GET all roles by space id', () => {
|
|||
|
||||
getRolesTest(`replaces privileges of deprecated features by default`, {
|
||||
apiResponse: () => ({
|
||||
first_role: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['feature_alpha.read'],
|
||||
resources: ['*'],
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: { _reserved: true },
|
||||
transient_metadata: { enabled: true },
|
||||
},
|
||||
total: 1,
|
||||
count: 1,
|
||||
roles: [
|
||||
{
|
||||
name: 'first_role',
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['feature_alpha.read'],
|
||||
resources: ['*'],
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: { _reserved: true },
|
||||
transient_metadata: { enabled: true },
|
||||
},
|
||||
],
|
||||
}),
|
||||
asserts: {
|
||||
statusCode: 200,
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { SecurityQueryRoleQueryRole } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import type { RouteDefinitionParams } from '../..';
|
||||
|
@ -38,10 +40,47 @@ export function defineGetAllRolesBySpaceRoutes({
|
|||
const hideReservedRoles = buildFlavor === 'serverless';
|
||||
const esClient = (await context.core).elasticsearch.client;
|
||||
|
||||
const [features, elasticsearchRoles] = await Promise.all([
|
||||
const [features, queryRolesResponse] = await Promise.all([
|
||||
getFeatures(),
|
||||
await esClient.asCurrentUser.security.getRole(),
|
||||
await esClient.asCurrentUser.security.queryRole({
|
||||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
'applications.resources': `space:${request.params.spaceId}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'metadata._reserved': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'metadata._reserved',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
from: 0,
|
||||
size: 1000,
|
||||
}),
|
||||
]);
|
||||
const elasticsearchRoles = (queryRolesResponse.roles || [])?.reduce<
|
||||
Record<string, SecurityQueryRoleQueryRole>
|
||||
>((acc, role) => {
|
||||
return {
|
||||
...acc,
|
||||
[role.name]: role,
|
||||
};
|
||||
}, {});
|
||||
|
||||
// Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name.
|
||||
return response.ok({
|
||||
|
@ -53,7 +92,7 @@ export function defineGetAllRolesBySpaceRoutes({
|
|||
|
||||
const role = transformElasticsearchRoleToRole({
|
||||
features,
|
||||
// @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[]
|
||||
// @ts-expect-error `remote_cluster` is not known in `Role` type
|
||||
elasticsearchRole,
|
||||
name: roleName,
|
||||
application: authz.applicationName,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { defineGetAllRolesRoutes } from './get_all';
|
|||
import { defineGetAllRolesBySpaceRoutes } from './get_all_by_space';
|
||||
import { defineBulkCreateOrUpdateRolesRoutes } from './post';
|
||||
import { definePutRolesRoutes } from './put';
|
||||
import { defineQueryRolesRoutes } from './query';
|
||||
import type { RouteDefinitionParams } from '../..';
|
||||
|
||||
export function defineRolesRoutes(params: RouteDefinitionParams) {
|
||||
|
@ -20,4 +21,5 @@ export function defineRolesRoutes(params: RouteDefinitionParams) {
|
|||
definePutRolesRoutes(params);
|
||||
defineGetAllRolesBySpaceRoutes(params);
|
||||
defineBulkCreateOrUpdateRolesRoutes(params);
|
||||
defineQueryRolesRoutes(params);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,421 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { kibanaResponseFactory } from '@kbn/core/server';
|
||||
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
|
||||
import type { MockedVersionedRouter } from '@kbn/core-http-router-server-mocks';
|
||||
import { KibanaFeature } from '@kbn/features-plugin/common';
|
||||
import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
|
||||
import type { LicenseCheck } from '@kbn/licensing-plugin/server';
|
||||
|
||||
import { defineQueryRolesRoutes } from './query';
|
||||
import { API_VERSIONS } from '../../../../common/constants';
|
||||
import { routeDefinitionParamsMock } from '../../index.mock';
|
||||
|
||||
interface TestOptions {
|
||||
name?: string;
|
||||
licenseCheckResult?: LicenseCheck;
|
||||
apiResponse?: () => unknown;
|
||||
asserts: { statusCode: number; result?: Record<string, any>; calledWith?: Record<string, any> };
|
||||
query?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const application = 'kibana-.kibana';
|
||||
|
||||
const features: KibanaFeature[] = [
|
||||
new KibanaFeature({
|
||||
deprecated: { notice: 'It is deprecated, sorry.' },
|
||||
id: 'alpha',
|
||||
name: 'Feature Alpha',
|
||||
app: [],
|
||||
category: { id: 'alpha', label: 'alpha' },
|
||||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
all: ['all-alpha-all-so'],
|
||||
read: ['all-alpha-read-so'],
|
||||
},
|
||||
ui: ['all-alpha-ui'],
|
||||
app: ['all-alpha-app'],
|
||||
api: ['all-alpha-api'],
|
||||
replacedBy: [{ feature: 'beta', privileges: ['all'] }],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: ['read-alpha-all-so'],
|
||||
read: ['read-alpha-read-so'],
|
||||
},
|
||||
ui: ['read-alpha-ui'],
|
||||
app: ['read-alpha-app'],
|
||||
api: ['read-alpha-api'],
|
||||
replacedBy: {
|
||||
default: [{ feature: 'beta', privileges: ['read', 'sub_beta'] }],
|
||||
minimal: [{ feature: 'beta', privileges: ['minimal_read'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'sub-feature-alpha',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub_alpha',
|
||||
name: 'Sub Feature Alpha',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: ['sub-alpha-all-so'],
|
||||
read: ['sub-alpha-read-so'],
|
||||
},
|
||||
ui: ['sub-alpha-ui'],
|
||||
app: ['sub-alpha-app'],
|
||||
api: ['sub-alpha-api'],
|
||||
replacedBy: [
|
||||
{ feature: 'beta', privileges: ['minimal_read'] },
|
||||
{ feature: 'beta', privileges: ['sub_beta'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
new KibanaFeature({
|
||||
id: 'beta',
|
||||
name: 'Feature Beta',
|
||||
app: [],
|
||||
category: { id: 'beta', label: 'beta' },
|
||||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
all: ['all-beta-all-so'],
|
||||
read: ['all-beta-read-so'],
|
||||
},
|
||||
ui: ['all-beta-ui'],
|
||||
app: ['all-beta-app'],
|
||||
api: ['all-beta-api'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: ['read-beta-all-so'],
|
||||
read: ['read-beta-read-so'],
|
||||
},
|
||||
ui: ['read-beta-ui'],
|
||||
app: ['read-beta-app'],
|
||||
api: ['read-beta-api'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'sub-feature-beta',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub_beta',
|
||||
name: 'Sub Feature Beta',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: ['sub-beta-all-so'],
|
||||
read: ['sub-beta-read-so'],
|
||||
},
|
||||
ui: ['sub-beta-ui'],
|
||||
app: ['sub-beta-app'],
|
||||
api: ['sub-beta-api'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
describe('Query roles', () => {
|
||||
const queryRolesTest = (
|
||||
description: string,
|
||||
{ licenseCheckResult = { state: 'valid' }, apiResponse, asserts, query }: TestOptions
|
||||
) => {
|
||||
test(description, async () => {
|
||||
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
|
||||
const versionedRouterMock = mockRouteDefinitionParams.router
|
||||
.versioned as MockedVersionedRouter;
|
||||
mockRouteDefinitionParams.authz.applicationName = application;
|
||||
mockRouteDefinitionParams.getFeatures = jest.fn().mockResolvedValue(features);
|
||||
mockRouteDefinitionParams.subFeaturePrivilegeIterator =
|
||||
featuresPluginMock.createSetup().subFeaturePrivilegeIterator;
|
||||
|
||||
defineQueryRolesRoutes(mockRouteDefinitionParams);
|
||||
const { handler: routeHandler } = versionedRouterMock.getRoute(
|
||||
'post',
|
||||
'/api/security/role/_query'
|
||||
).versions[API_VERSIONS.roles.public.v1];
|
||||
|
||||
const mockCoreContext = coreMock.createRequestHandlerContext();
|
||||
const mockLicensingContext = {
|
||||
license: { check: jest.fn().mockReturnValue(licenseCheckResult) },
|
||||
} as any;
|
||||
const mockContext = coreMock.createCustomRequestHandlerContext({
|
||||
core: mockCoreContext,
|
||||
licensing: mockLicensingContext,
|
||||
});
|
||||
|
||||
if (apiResponse) {
|
||||
mockCoreContext.elasticsearch.client.asCurrentUser.security.queryRole.mockResponseImplementation(
|
||||
(() => ({ body: apiResponse() })) as any
|
||||
);
|
||||
}
|
||||
|
||||
const headers = { authorization: 'foo' };
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
method: 'post',
|
||||
path: '/api/security/role/_query',
|
||||
headers,
|
||||
query,
|
||||
});
|
||||
|
||||
const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory);
|
||||
expect(response.status).toBe(asserts.statusCode);
|
||||
expect(response.payload).toEqual(asserts.result);
|
||||
|
||||
if (apiResponse) {
|
||||
expect(
|
||||
mockCoreContext.elasticsearch.client.asCurrentUser.security.queryRole
|
||||
).toHaveBeenCalled();
|
||||
}
|
||||
expect(mockLicensingContext.license.check).toHaveBeenCalledWith('security', 'basic');
|
||||
});
|
||||
};
|
||||
|
||||
describe('success', () => {
|
||||
queryRolesTest('query all roles', {
|
||||
apiResponse: () => ({
|
||||
total: 5,
|
||||
count: 2,
|
||||
roles: [
|
||||
{
|
||||
name: 'apm_system',
|
||||
cluster: ['monitor', 'cluster:admin/xpack/monitoring/bulk'],
|
||||
indices: [
|
||||
{
|
||||
names: ['.monitoring-beats-*'],
|
||||
privileges: ['create_index', 'create_doc'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
applications: [],
|
||||
run_as: [],
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
_sort: ['apm_system'],
|
||||
},
|
||||
{
|
||||
name: 'user_role',
|
||||
cluster: [],
|
||||
indices: [
|
||||
{
|
||||
names: ['.management-beats'],
|
||||
privileges: ['all'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
applications: [],
|
||||
run_as: [],
|
||||
metadata: {},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
_sort: ['user_role'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
query: {
|
||||
from: 0,
|
||||
size: 25,
|
||||
},
|
||||
asserts: {
|
||||
statusCode: 200,
|
||||
result: {
|
||||
roles: [
|
||||
{
|
||||
name: 'apm_system',
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
elasticsearch: {
|
||||
cluster: ['monitor', 'cluster:admin/xpack/monitoring/bulk'],
|
||||
indices: [
|
||||
{
|
||||
names: ['.monitoring-beats-*'],
|
||||
privileges: ['create_index', 'create_doc'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [],
|
||||
_transform_error: [],
|
||||
_unrecognized_applications: [],
|
||||
},
|
||||
{
|
||||
name: 'user_role',
|
||||
metadata: {},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [
|
||||
{
|
||||
names: ['.management-beats'],
|
||||
privileges: ['all'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [],
|
||||
_transform_error: [],
|
||||
_unrecognized_applications: [],
|
||||
},
|
||||
],
|
||||
count: 2,
|
||||
total: 5,
|
||||
},
|
||||
calledWith: {
|
||||
from: 0,
|
||||
size: 25,
|
||||
sort: undefined,
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [
|
||||
{ term: { 'metadata._reserved': true } },
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'metadata._reserved',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
queryRolesTest('hide reserved roles', {
|
||||
apiResponse: () => ({
|
||||
total: 1,
|
||||
count: 1,
|
||||
roles: [
|
||||
{
|
||||
name: 'user_role',
|
||||
cluster: [],
|
||||
indices: [
|
||||
{
|
||||
names: ['.management-beats'],
|
||||
privileges: ['all'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
applications: [],
|
||||
run_as: [],
|
||||
metadata: {},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
_sort: ['user_role'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
query: {
|
||||
from: 0,
|
||||
size: 25,
|
||||
},
|
||||
asserts: {
|
||||
statusCode: 200,
|
||||
result: {
|
||||
roles: [
|
||||
{
|
||||
name: 'user_role',
|
||||
metadata: {},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [
|
||||
{
|
||||
names: ['.management-beats'],
|
||||
privileges: ['all'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [],
|
||||
_transform_error: [],
|
||||
_unrecognized_applications: [],
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
total: 1,
|
||||
},
|
||||
calledWith: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [],
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
'metadata._reserved': false,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'metadata._reserved',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not: [],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
from: 0,
|
||||
size: 2,
|
||||
sort: [
|
||||
{
|
||||
name: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { QueryRolesResult } from '@kbn/security-plugin-types-common';
|
||||
|
||||
import type { RouteDefinitionParams } from '../..';
|
||||
import { API_VERSIONS } from '../../../../common/constants';
|
||||
import { transformElasticsearchRoleToRole } from '../../../authorization';
|
||||
import { wrapIntoCustomErrorResponse } from '../../../errors';
|
||||
import { createLicensedRouteHandler } from '../../licensed_route_handler';
|
||||
|
||||
interface QueryClause {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function defineQueryRolesRoutes({
|
||||
router,
|
||||
authz,
|
||||
getFeatures,
|
||||
logger,
|
||||
buildFlavor,
|
||||
}: RouteDefinitionParams) {
|
||||
router.versioned
|
||||
.post({
|
||||
path: '/api/security/role/_query',
|
||||
access: 'public',
|
||||
summary: `Query roles`,
|
||||
options: {
|
||||
tags: ['oas-tags:roles'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.roles.public.v1,
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason: `This route delegates authorization to Core's scoped ES cluster client`,
|
||||
},
|
||||
},
|
||||
validate: {
|
||||
request: {
|
||||
body: schema.object({
|
||||
query: schema.maybe(schema.string()),
|
||||
from: schema.maybe(schema.number()),
|
||||
size: schema.maybe(schema.number()),
|
||||
sort: schema.maybe(
|
||||
schema.object({
|
||||
field: schema.string(),
|
||||
direction: schema.oneOf([schema.literal('asc'), schema.literal('desc')]),
|
||||
})
|
||||
),
|
||||
filters: schema.maybe(
|
||||
schema.object({
|
||||
showReservedRoles: schema.maybe(schema.boolean({ defaultValue: true })),
|
||||
})
|
||||
),
|
||||
}),
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Indicates a successful call.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
try {
|
||||
const esClient = (await context.core).elasticsearch.client;
|
||||
const features = await getFeatures();
|
||||
|
||||
const { query, size, from, sort, filters } = request.body;
|
||||
|
||||
let showReservedRoles = filters?.showReservedRoles;
|
||||
|
||||
if (buildFlavor === 'serverless') {
|
||||
showReservedRoles = false;
|
||||
}
|
||||
|
||||
const queryPayload: {
|
||||
bool: {
|
||||
must: QueryClause[];
|
||||
should: QueryClause[];
|
||||
must_not: QueryClause[];
|
||||
minimum_should_match?: number;
|
||||
};
|
||||
} = { bool: { must: [], should: [], must_not: [] } };
|
||||
|
||||
const nonReservedRolesQuery = [
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'metadata._reserved',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
queryPayload.bool.should.push(...nonReservedRolesQuery);
|
||||
queryPayload.bool.minimum_should_match = 1;
|
||||
|
||||
if (query) {
|
||||
queryPayload.bool.must.push({
|
||||
wildcard: {
|
||||
name: {
|
||||
value: `*${query}*`,
|
||||
case_insensitive: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (showReservedRoles) {
|
||||
queryPayload.bool.should.push({ term: { 'metadata._reserved': true } });
|
||||
}
|
||||
|
||||
const transformedSort = sort && [{ [sort.field]: { order: sort.direction } }];
|
||||
|
||||
const queryRoles = await esClient.asCurrentUser.security.queryRole({
|
||||
query: queryPayload,
|
||||
from,
|
||||
size,
|
||||
sort: transformedSort,
|
||||
});
|
||||
|
||||
const transformedRoles = (queryRoles.roles || []).map((role) =>
|
||||
transformElasticsearchRoleToRole({
|
||||
features,
|
||||
// @ts-expect-error `remote_cluster` is not known in `Role` type
|
||||
elasticsearchRole: role,
|
||||
name: role.name,
|
||||
application: authz.applicationName,
|
||||
logger,
|
||||
})
|
||||
);
|
||||
|
||||
return response.ok<QueryRolesResult>({
|
||||
body: {
|
||||
roles: transformedRoles,
|
||||
count: queryRoles.count,
|
||||
total: queryRoles.total,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return response.customError(wrapIntoCustomErrorResponse(error));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
|
@ -13,5 +13,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
return {
|
||||
...baseIntegrationTestsConfig.getAll(),
|
||||
testFiles: [require.resolve('.')],
|
||||
esTestCluster: {
|
||||
...baseIntegrationTestsConfig.get('esTestCluster'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -519,5 +519,118 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(roleToUpdateWithDlsFls).to.eql({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query Role', () => {
|
||||
it('should query roles by name', async () => {
|
||||
await es.security.putRole({
|
||||
name: 'role_to_query',
|
||||
body: {
|
||||
cluster: ['manage'],
|
||||
indices: [
|
||||
{
|
||||
names: ['logstash-*'],
|
||||
privileges: ['read', 'view_index_metadata'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
applications: [
|
||||
{
|
||||
application: 'kibana-.kibana',
|
||||
privileges: ['read'],
|
||||
resources: ['*'],
|
||||
},
|
||||
{
|
||||
application: 'kibana-.kibana',
|
||||
privileges: ['feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'],
|
||||
resources: ['space:marketing', 'space:sales'],
|
||||
},
|
||||
{
|
||||
application: 'logstash-default',
|
||||
privileges: ['logstash-privilege'],
|
||||
resources: ['*'],
|
||||
},
|
||||
],
|
||||
run_as: ['watcher_user'],
|
||||
metadata: {
|
||||
foo: 'test-metadata',
|
||||
},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await supertest
|
||||
.post('/api/security/role/_query')
|
||||
.send({
|
||||
from: 0,
|
||||
size: 25,
|
||||
query: 'role_to_query',
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200, {
|
||||
total: 1,
|
||||
count: 1,
|
||||
roles: [
|
||||
{
|
||||
name: 'role_to_query',
|
||||
metadata: {
|
||||
foo: 'test-metadata',
|
||||
},
|
||||
transient_metadata: { enabled: true },
|
||||
elasticsearch: {
|
||||
cluster: ['manage'],
|
||||
indices: [
|
||||
{
|
||||
names: ['logstash-*'],
|
||||
privileges: ['read', 'view_index_metadata'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
run_as: ['watcher_user'],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
dashboard: ['read'],
|
||||
discover: ['all'],
|
||||
ml: ['all'],
|
||||
},
|
||||
spaces: ['marketing', 'sales'],
|
||||
},
|
||||
],
|
||||
|
||||
_transform_error: [],
|
||||
_unrecognized_applications: ['logstash-default'],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide reserved roles when filtered', async () => {
|
||||
const response = await supertest
|
||||
.post('/api/security/role/_query')
|
||||
.send({
|
||||
from: 0,
|
||||
size: 100,
|
||||
filters: {
|
||||
showReservedRoles: false,
|
||||
},
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
|
||||
const filteredResults = response.body.roles.filter(
|
||||
(role: any) => role.metadata._reserved === true
|
||||
);
|
||||
expect(filteredResults.length).to.eql(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,5 +13,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
return {
|
||||
...functionalConfig.getAll(),
|
||||
testFiles: [require.resolve('.')],
|
||||
esTestCluster: {
|
||||
...functionalConfig.get('esTestCluster'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,5 +13,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
return {
|
||||
...functionalConfig.getAll(),
|
||||
testFiles: [require.resolve('.')],
|
||||
esTestCluster: {
|
||||
...functionalConfig.get('esTestCluster'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -458,8 +458,12 @@ export class SecurityPageObject extends FtrService {
|
|||
|
||||
async getElasticsearchRoles() {
|
||||
const roles = [];
|
||||
await this.testSubjects.exists('rolesTable');
|
||||
await this.testSubjects.click('tablePaginationPopoverButton');
|
||||
await this.testSubjects.click('tablePagination-100-rows');
|
||||
await this.testSubjects.exists('rolesTableLoading');
|
||||
await this.testSubjects.exists('rolesTable');
|
||||
|
||||
for (const role of await this.testSubjects.findAll('roleRow')) {
|
||||
const [rolename, reserved, deprecated] = await Promise.all([
|
||||
role.findByTestSubject('roleRowName').then((el) => el.getVisibleText()),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue