From 559acad89e0b344b3f05fe5d634b14041d933ead Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 1 Oct 2024 20:46:25 +0200 Subject: [PATCH 01/45] [Roles] Use Query Roles API for Role Management grid screen --- .../management/roles/roles_api_client.ts | 31 ++-- .../roles/roles_grid/roles_grid_page.tsx | 142 ++++++++++++------ .../routes/authorization/roles/index.ts | 2 + .../routes/authorization/roles/query.ts | 109 ++++++++++++++ 4 files changed, 229 insertions(+), 55 deletions(-) create mode 100644 x-pack/plugins/security/server/routes/authorization/roles/query.ts diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts index d6dcab658d21c..32e7529d57363 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -5,12 +5,23 @@ * 2.0. */ +import type { Criteria } from '@elastic/eui'; +import type { QueryContainer } from '@elastic/eui/src/components/search_bar/query/ast_to_es_query_dsl'; + import type { HttpStart } from '@kbn/core/public'; -import type { BulkUpdatePayload, BulkUpdateRoleResponse } from '@kbn/security-plugin-types-public'; import type { Role, RoleIndexPrivilege, RoleRemoteIndexPrivilege } from '../../../common'; import { copyRole } from '../../../common/model'; +export interface QueryRoleParams { + query: QueryContainer; + from: number; + size: number; + filters?: { + showReserved?: boolean; + }; + sort: Required>['sort']; +} export class RolesAPIClient { constructor(private readonly http: HttpStart) {} @@ -18,6 +29,12 @@ export class RolesAPIClient { return await this.http.get('/api/security/role'); }; + public queryRoles = async (params?: QueryRoleParams) => { + return await this.http.post(`/api/security/role/_query`, { + body: JSON.stringify(params || {}), + }); + }; + public getRole = async (roleName: string) => { return await this.http.get(`/api/security/role/${encodeURIComponent(roleName)}`); }; @@ -33,18 +50,6 @@ export class RolesAPIClient { }); }; - public bulkUpdateRoles = async ({ - rolesUpdate, - }: BulkUpdatePayload): Promise => { - return await this.http.post('/api/security/roles', { - body: JSON.stringify({ - roles: Object.fromEntries( - rolesUpdate.map((role) => [role.name, this.transformRoleForSave(copyRole(role))]) - ), - }), - }); - }; - private transformRoleForSave = (role: Role) => { // Remove any placeholder index privileges const isPlaceholderPrivilege = ( diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 457b9053a0ac8..3508e67834b9b 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -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, @@ -22,6 +29,7 @@ import { import _ from 'lodash'; import React, { useEffect, useState } from 'react'; import type { FC } from 'react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; import type { BuildFlavor } from '@kbn/config'; import type { NotificationsStart, ScopedHistory } from '@kbn/core/public'; @@ -54,6 +62,16 @@ export interface Props extends StartServices { cloudOrgUrl?: string; } +interface RolesTableState { + query: Query; + sort: Criteria['sort']; + from: number; + size: number; + filters: { + showReserved?: boolean; + }; +} + const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { return `/${action}${roleName ? `/${encodeURIComponent(roleName)}` : ''}`; }; @@ -68,6 +86,19 @@ const getVisibleRoles = (roles: Role[], filter: string, includeReservedRoles: bo }); }; +const DEFAULT_TABLE_STATE = { + query: EuiSearchBar.Query.MATCH_ALL, + sort: { + field: 'name' as const, + direction: 'asc' as const, + }, + from: 0, + size: 25, + filters: { + showReserved: true, + }, +}; + export const RolesGridPage: FC = ({ notifications, rolesAPIClient, @@ -84,36 +115,24 @@ export const RolesGridPage: FC = ({ const [selection, setSelection] = useState([]); const [filter, setFilter] = useState(''); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); - const [permissionDenied, setPermissionDenied] = useState(false); const [includeReservedRoles, setIncludeReservedRoles] = useState(true); - const [isLoading, setIsLoading] = useState(false); + const [tableState, setTableState] = useState(DEFAULT_TABLE_STATE); + + const [state, queryRoles] = useAsyncFn(async (tableStateArgs: RolesTableState) => { + const queryContainer = EuiSearchBar.Query.toESQuery(tableStateArgs.query); + + const requestBody = { + ...tableStateArgs, + ...(tableStateArgs.sort ? { sort: tableStateArgs.sort } : DEFAULT_TABLE_STATE.sort), + query: queryContainer, + }; + return await rolesAPIClient.queryRoles(requestBody); + }, []); useEffect(() => { - loadRoles(); + queryRoles(DEFAULT_TABLE_STATE); }, []); // eslint-disable-line react-hooks/exhaustive-deps - const loadRoles = async () => { - try { - setIsLoading(true); - const rolesFromApi = await rolesAPIClient.getRoles(); - setRoles(rolesFromApi); - setVisibleRoles(getVisibleRoles(rolesFromApi, filter, includeReservedRoles)); - } catch (e) { - if (_.get(e, 'body.statusCode') === 403) { - setPermissionDenied(true); - } else { - notifications.toasts.addDanger( - i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', { - defaultMessage: 'Error fetching roles: {message}', - values: { message: _.get(e, 'body.message', '') }, - }) - ); - } - } finally { - setIsLoading(false); - } - }; - const onIncludeReservedRolesChange = (e: EuiSwitchEvent) => { setIncludeReservedRoles(e.target.checked); setVisibleRoles(getVisibleRoles(roles, filter, e.target.checked)); @@ -212,6 +231,28 @@ export const RolesGridPage: FC = ({ } }; + const onTableChange = ({ page, sort }: CriteriaWithPagination) => { + const newState = { + ...tableState, + from: page?.index! * page?.size!, + size: page?.size!, + sort: sort ?? tableState.sort, + }; + setTableState(newState); + queryRoles(newState); + }; + + const onSearchChange = (args: EuiSearchBarOnChangeArgs) => { + if (!args.error) { + const newState = { + ...tableState, + query: args.query, + }; + setTableState(newState); + queryRoles(newState); + } + }; + const getColumnConfig = (): Array> => { const config: Array> = [ { @@ -333,6 +374,31 @@ export const RolesGridPage: FC = ({ setShowDeleteConfirmation(false); }; + const isLoading = state.loading; + let permissionDenied = false; + if (state.error) { + if (state.error.message.includes('Forbidden')) { + permissionDenied = true; + } else { + notifications.toasts.addDanger( + i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', { + defaultMessage: 'Error fetching roles: {message}', + values: { message: _.get(state.error, 'body.message', '') }, + }) + ); + } + } + + const tableItems = state.value?.roles ?? []; + const totalItemCount = state.value?.total ?? 0; + + const pagination = { + pageIndex: tableState.from / tableState.size, + pageSize: tableState.size, + totalItemCount, + pageSizeOptions: [25, 50, 100], + }; + return permissionDenied ? ( ) : ( @@ -420,15 +486,12 @@ export const RolesGridPage: FC = ({ incremental: true, 'data-test-subj': 'searchRoles', }} - onChange={(query: Record) => { - setFilter(query.queryText); - setVisibleRoles(getVisibleRoles(roles, query.queryText, includeReservedRoles)); - }} + onChange={onSearchChange} toolsLeft={renderToolsLeft()} toolsRight={renderToolsRight()} /> - = ({ selected: selection, } } - pagination={{ - initialPageSize: 20, - pageSizeOptions: [10, 20, 30, 50, 100], - }} - message={ + onChange={onTableChange} + pagination={pagination} + noItemsMessage={ buildFlavor === 'serverless' ? ( = ({ /> ) } - items={visibleRoles} + items={tableItems} loading={isLoading} sorting={{ - sort: { - field: 'name', - direction: 'asc', - }, + sort: tableState.sort, }} rowProps={{ 'data-test-subj': 'roleRow' }} /> diff --git a/x-pack/plugins/security/server/routes/authorization/roles/index.ts b/x-pack/plugins/security/server/routes/authorization/roles/index.ts index d5af481ca3c11..3207be94b8afe 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/index.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/index.ts @@ -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); } diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts new file mode 100644 index 0000000000000..5ac0439546f78 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -0,0 +1,109 @@ +/* + * 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 { RouteDefinitionParams } from '../..'; +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.post( + { + path: '/api/security/role/_query', + options: { + summary: `Query roles`, + }, + validate: { + body: schema.object({ + query: schema.maybe(schema.object({}, { unknowns: 'allow' })), + 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({ + showReserved: schema.maybe(schema.boolean({ defaultValue: true })), + }) + ), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const hideReservedRoles = + buildFlavor === 'serverless' || request.body.filters?.showReserved === false; + const esClient = (await context.core).elasticsearch.client; + const features = await getFeatures(); + const [elasticsearchRoles] = await Promise.all([ + await esClient.asCurrentUser.security.getRole(), + ]); + + const { query, size, from, sort, filters } = request.body; + + let showReservedRoles = filters?.showReserved; + + if (buildFlavor === 'serverless') { + showReservedRoles = false; + } + + const queryPayload: { + bool: { must: QueryClause[]; should: QueryClause[]; must_not: QueryClause[] }; + } = { bool: { must: [], should: [], must_not: [] } }; + + if (query) { + queryPayload.bool.must.push(query); + } + + queryPayload.bool.should.push({ term: { 'metadata._reserved': showReservedRoles } }); + + const transformedSort = sort && [{ [sort.field]: { order: sort.direction } }]; + const queryRoles = await esClient.asCurrentUser.transport.request({ + path: '/_security/_query/role', + method: 'POST', + body: { + query: queryPayload, + from, + size, + sort: transformedSort, + }, + }); + + console.log( + JSON.stringify({ + query: queryPayload, + from, + size, + sort: transformedSort, + }) + ); + + // Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name. + return response.ok({ + // @ts-expect-error + body: queryRoles, + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} From af5f44e84f26a03eb8b87abc4dd731ca6447107d Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 1 Oct 2024 20:51:03 +0200 Subject: [PATCH 02/45] remove unused function --- .../security/public/management/roles/roles_api_client.ts | 2 +- .../management/roles/roles_grid/roles_grid_page.tsx | 2 +- .../security/server/routes/authorization/roles/query.ts | 9 --------- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts index 32e7529d57363..a8b516f79967e 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -20,7 +20,7 @@ export interface QueryRoleParams { filters?: { showReserved?: boolean; }; - sort: Required>['sort']; + sort: Criteria['sort']; } export class RolesAPIClient { constructor(private readonly http: HttpStart) {} diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 3508e67834b9b..b9c313a18a80d 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -183,7 +183,7 @@ export const RolesGridPage: FC = ({ const handleDelete = () => { setSelection([]); setShowDeleteConfirmation(false); - loadRoles(); + queryRoles(tableState); }; const deleteOneRole = (roleToDelete: Role) => { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 5ac0439546f78..e0b9ddfdad643 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -87,15 +87,6 @@ export function defineQueryRolesRoutes({ }, }); - console.log( - JSON.stringify({ - query: queryPayload, - from, - size, - sort: transformedSort, - }) - ); - // Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name. return response.ok({ // @ts-expect-error From 5c4b8941081f2e97217701b3ebfce56d49f30ea4 Mon Sep 17 00:00:00 2001 From: Sid Date: Wed, 2 Oct 2024 14:16:35 +0200 Subject: [PATCH 03/45] clean up roles grid page, remove useAsyncFn and use regular react hooks to reduce complexity --- .../security/plugin_types_common/index.ts | 2 + .../src/authorization/index.ts | 2 + .../src/authorization/role.ts | 9 ++ .../management/roles/roles_api_client.ts | 3 +- .../roles/roles_grid/roles_grid_page.tsx | 84 +++++++++---------- .../routes/authorization/roles/query.ts | 40 +++++---- 6 files changed, 79 insertions(+), 61 deletions(-) diff --git a/x-pack/packages/security/plugin_types_common/index.ts b/x-pack/packages/security/plugin_types_common/index.ts index 8dd0ff726103a..27a6535f52170 100644 --- a/x-pack/packages/security/plugin_types_common/index.ts +++ b/x-pack/packages/security/plugin_types_common/index.ts @@ -12,6 +12,8 @@ export type { AuthenticationProvider, } from './src/authentication'; export type { + QueryRolesRole, + QueryRolesResult, Role, RoleIndexPrivilege, RoleKibanaPrivilege, diff --git a/x-pack/packages/security/plugin_types_common/src/authorization/index.ts b/x-pack/packages/security/plugin_types_common/src/authorization/index.ts index 89857a18865af..4bcefd4e09681 100644 --- a/x-pack/packages/security/plugin_types_common/src/authorization/index.ts +++ b/x-pack/packages/security/plugin_types_common/src/authorization/index.ts @@ -7,6 +7,8 @@ export type { FeaturesPrivileges } from './features_privileges'; export type { + QueryRolesRole, + QueryRolesResult, Role, RoleKibanaPrivilege, RoleIndexPrivilege, diff --git a/x-pack/packages/security/plugin_types_common/src/authorization/role.ts b/x-pack/packages/security/plugin_types_common/src/authorization/role.ts index 3a20b64d4d06c..2a2034dada666 100644 --- a/x-pack/packages/security/plugin_types_common/src/authorization/role.ts +++ b/x-pack/packages/security/plugin_types_common/src/authorization/role.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import type { FeaturesPrivileges } from './features_privileges'; export interface RoleIndexPrivilege { @@ -53,3 +54,11 @@ export interface Role { _transform_error?: string[]; _unrecognized_applications?: string[]; } + +export type QueryRolesRole = estypes.SecurityQueryRoleQueryRole; + +export interface QueryRolesResult { + roles: Role[]; + count: number; + total: number; +} diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts index a8b516f79967e..d69ac75fb8b67 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -9,6 +9,7 @@ import type { Criteria } from '@elastic/eui'; import type { QueryContainer } from '@elastic/eui/src/components/search_bar/query/ast_to_es_query_dsl'; import type { HttpStart } from '@kbn/core/public'; +import type { QueryRolesResult } from '@kbn/security-plugin-types-common'; import type { Role, RoleIndexPrivilege, RoleRemoteIndexPrivilege } from '../../../common'; import { copyRole } from '../../../common/model'; @@ -30,7 +31,7 @@ export class RolesAPIClient { }; public queryRoles = async (params?: QueryRoleParams) => { - return await this.http.post(`/api/security/role/_query`, { + return await this.http.post(`/api/security/role/_query`, { body: JSON.stringify(params || {}), }); }; diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index b9c313a18a80d..ce3bbf14b24eb 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -29,13 +29,13 @@ import { import _ from 'lodash'; import React, { useEffect, useState } from 'react'; import type { FC } from 'react'; -import useAsyncFn from 'react-use/lib/useAsyncFn'; import type { BuildFlavor } from '@kbn/config'; 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'; @@ -76,16 +76,6 @@ 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 DEFAULT_TABLE_STATE = { query: EuiSearchBar.Query.MATCH_ALL, sort: { @@ -110,15 +100,16 @@ export const RolesGridPage: FC = ({ theme, i18n: i18nStart, }) => { - const [roles, setRoles] = useState([]); - const [visibleRoles, setVisibleRoles] = useState([]); + const [rolesResponse, setRolesResponse] = useState({} as QueryRolesResult); + const [selection, setSelection] = useState([]); - const [filter, setFilter] = useState(''); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); - const [includeReservedRoles, setIncludeReservedRoles] = useState(true); + const [permissionDenied, setPermissionDenied] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [tableState, setTableState] = useState(DEFAULT_TABLE_STATE); - const [state, queryRoles] = useAsyncFn(async (tableStateArgs: RolesTableState) => { + const loadRoles = async (tableStateArgs: RolesTableState) => { const queryContainer = EuiSearchBar.Query.toESQuery(tableStateArgs.query); const requestBody = { @@ -126,16 +117,40 @@ export const RolesGridPage: FC = ({ ...(tableStateArgs.sort ? { sort: tableStateArgs.sort } : DEFAULT_TABLE_STATE.sort), query: queryContainer, }; - return await rolesAPIClient.queryRoles(requestBody); - }, []); + + try { + setIsLoading(true); + const rolesFromApi = await rolesAPIClient.queryRoles(requestBody); + setRolesResponse(rolesFromApi); + } catch (e) { + if (_.get(e, 'body.statusCode') === 403) { + setPermissionDenied(true); + } else { + notifications.toasts.addDanger( + i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', { + defaultMessage: 'Error fetching roles: {message}', + values: { message: _.get(e, 'body.message', '') }, + }) + ); + } + } finally { + setIsLoading(false); + } + }; useEffect(() => { - queryRoles(DEFAULT_TABLE_STATE); + 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: { + showReserved: e.target.checked, + }, + }; + setTableState(newTableStateArgs); + loadRoles(newTableStateArgs); }; const getRoleStatusBadges = (role: Role) => { @@ -183,7 +198,7 @@ export const RolesGridPage: FC = ({ const handleDelete = () => { setSelection([]); setShowDeleteConfirmation(false); - queryRoles(tableState); + loadRoles(tableState); }; const deleteOneRole = (roleToDelete: Role) => { @@ -224,7 +239,7 @@ export const RolesGridPage: FC = ({ defaultMessage="Show reserved roles" /> } - checked={includeReservedRoles} + checked={tableState.filters.showReserved ?? true} onChange={onIncludeReservedRolesChange} /> ); @@ -239,7 +254,7 @@ export const RolesGridPage: FC = ({ sort: sort ?? tableState.sort, }; setTableState(newState); - queryRoles(newState); + loadRoles(newState); }; const onSearchChange = (args: EuiSearchBarOnChangeArgs) => { @@ -249,7 +264,7 @@ export const RolesGridPage: FC = ({ query: args.query, }; setTableState(newState); - queryRoles(newState); + loadRoles(newState); } }; @@ -374,23 +389,8 @@ export const RolesGridPage: FC = ({ setShowDeleteConfirmation(false); }; - const isLoading = state.loading; - let permissionDenied = false; - if (state.error) { - if (state.error.message.includes('Forbidden')) { - permissionDenied = true; - } else { - notifications.toasts.addDanger( - i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', { - defaultMessage: 'Error fetching roles: {message}', - values: { message: _.get(state.error, 'body.message', '') }, - }) - ); - } - } - - const tableItems = state.value?.roles ?? []; - const totalItemCount = state.value?.total ?? 0; + const tableItems = rolesResponse.roles ?? []; + const totalItemCount = rolesResponse.total ?? 0; const pagination = { pageIndex: tableState.from / tableState.size, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index e0b9ddfdad643..0355d397aa6a9 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -6,8 +6,10 @@ */ import { schema } from '@kbn/config-schema'; +import type { QueryRolesResult } from '@kbn/security-plugin-types-common'; import type { RouteDefinitionParams } from '../..'; +import { transformElasticsearchRoleToRole } from '../../../authorization'; import { wrapIntoCustomErrorResponse } from '../../../errors'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; @@ -27,6 +29,7 @@ export function defineQueryRolesRoutes({ path: '/api/security/role/_query', options: { summary: `Query roles`, + access: 'public', }, validate: { body: schema.object({ @@ -49,13 +52,8 @@ export function defineQueryRolesRoutes({ }, createLicensedRouteHandler(async (context, request, response) => { try { - const hideReservedRoles = - buildFlavor === 'serverless' || request.body.filters?.showReserved === false; const esClient = (await context.core).elasticsearch.client; const features = await getFeatures(); - const [elasticsearchRoles] = await Promise.all([ - await esClient.asCurrentUser.security.getRole(), - ]); const { query, size, from, sort, filters } = request.body; @@ -73,24 +71,30 @@ export function defineQueryRolesRoutes({ queryPayload.bool.must.push(query); } - queryPayload.bool.should.push({ term: { 'metadata._reserved': showReservedRoles } }); + if (showReservedRoles) { + queryPayload.bool.should.push({ term: { 'metadata._reserved': showReservedRoles } }); + } const transformedSort = sort && [{ [sort.field]: { order: sort.direction } }]; - const queryRoles = await esClient.asCurrentUser.transport.request({ - path: '/_security/_query/role', - method: 'POST', - body: { - query: queryPayload, - from, - size, - sort: transformedSort, - }, + + const queryRoles = await esClient.asCurrentUser.security.queryRole({ + query: queryPayload, + from, + size, + sort: transformedSort, }); - // Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name. - return response.ok({ + const transformedRoles = queryRoles.roles?.map((role) => // @ts-expect-error - body: queryRoles, + transformElasticsearchRoleToRole(features, role, role.name, authz.applicationName, logger) + ); + + return response.ok({ + body: { + roles: transformedRoles, + count: queryRoles.count, + total: queryRoles.total, + }, }); } catch (error) { return response.customError(wrapIntoCustomErrorResponse(error)); From 73aad8e877e56cefb37e0b7b6e02a68673c52c3b Mon Sep 17 00:00:00 2001 From: Sid Date: Wed, 9 Oct 2024 15:15:20 +0200 Subject: [PATCH 04/45] Add option for filtering by space id --- .../routes/authorization/roles/query.test.ts | 19 +++++++++++++++++++ .../routes/authorization/roles/query.ts | 9 +++++++++ 2 files changed, 28 insertions(+) create mode 100644 x-pack/plugins/security/server/routes/authorization/roles/query.test.ts diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts new file mode 100644 index 0000000000000..737f6247f2cef --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; + +import { kibanaResponseFactory } from '@kbn/core/server'; +import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; +import type { LicenseCheck } from '@kbn/licensing-plugin/server'; + +import { defineQueryRolesRoutes } from './query'; +import { routeDefinitionParamsMock } from '../../index.mock'; + +describe('Query roles', () => { + +}); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 0355d397aa6a9..bbbdd92c931f7 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -45,6 +45,7 @@ export function defineQueryRolesRoutes({ filters: schema.maybe( schema.object({ showReserved: schema.maybe(schema.boolean({ defaultValue: true })), + spaceId: schema.maybe(schema.string({ minLength: 1 })), }) ), }), @@ -75,6 +76,14 @@ export function defineQueryRolesRoutes({ queryPayload.bool.should.push({ term: { 'metadata._reserved': showReservedRoles } }); } + if (filters?.spaceId) { + queryPayload.bool.must.push({ + term: { + 'applications.resources': `space:${filters.spaceId}`, + }, + }); + } + const transformedSort = sort && [{ [sort.field]: { order: sort.direction } }]; const queryRoles = await esClient.asCurrentUser.security.queryRole({ From 5f864f747cae930b61858bf40acb8ffe57a05f9b Mon Sep 17 00:00:00 2001 From: Sid Date: Wed, 9 Oct 2024 16:12:38 +0200 Subject: [PATCH 05/45] replicate TS expect error --- .../security/server/routes/authorization/roles/query.test.ts | 4 +--- .../security/server/routes/authorization/roles/query.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts index 737f6247f2cef..b981c73d859e9 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts @@ -14,6 +14,4 @@ import type { LicenseCheck } from '@kbn/licensing-plugin/server'; import { defineQueryRolesRoutes } from './query'; import { routeDefinitionParamsMock } from '../../index.mock'; -describe('Query roles', () => { - -}); +describe('Query roles', () => {}); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index bbbdd92c931f7..13eb69c053624 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -94,7 +94,7 @@ export function defineQueryRolesRoutes({ }); const transformedRoles = queryRoles.roles?.map((role) => - // @ts-expect-error + // @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[] transformElasticsearchRoleToRole(features, role, role.name, authz.applicationName, logger) ); From 87f299da90f088afd5c4d9f30ac485cda3a5024b Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 10 Dec 2024 12:48:23 +0100 Subject: [PATCH 06/45] remove TS expect error and add todo --- .../server/routes/authorization/roles/query.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 13eb69c053624..5a24ef3629eaf 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -73,7 +73,7 @@ export function defineQueryRolesRoutes({ } if (showReservedRoles) { - queryPayload.bool.should.push({ term: { 'metadata._reserved': showReservedRoles } }); + queryPayload.bool.should.push({ term: { 'metadata._reserved': true } }); } if (filters?.spaceId) { @@ -94,8 +94,13 @@ export function defineQueryRolesRoutes({ }); const transformedRoles = queryRoles.roles?.map((role) => - // @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[] - transformElasticsearchRoleToRole(features, role, role.name, authz.applicationName, logger) + transformElasticsearchRoleToRole({ + features, + elasticsearchRole: role, // TODO: address why the `remote_cluster` field is throwing type errors + name: role.name, + application: authz.applicationName, + logger, + }) ); return response.ok({ From 4af8c05cd82ae9e334ffe96b0af3c2ec83bb64ae Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 10 Dec 2024 13:21:31 +0100 Subject: [PATCH 07/45] update get all by space to use query roles api --- .../authorization/roles/get_all_by_space.ts | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts index a441ba15164c1..b21bdf81047ba 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts @@ -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,11 +40,31 @@ 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.getRole(), + await esClient.asCurrentUser.security.queryRole({ + query: { + bool: { + must: [ + { + term: { + 'applications.resources': `space:${request.params.spaceId}`, + }, + }, + ], + }, + }, + }), ]); - + const elasticsearchRoles = queryRolesResponse.roles?.reduce< + Record + >((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({ body: Object.entries(elasticsearchRoles) @@ -53,8 +75,7 @@ export function defineGetAllRolesBySpaceRoutes({ const role = transformElasticsearchRoleToRole({ features, - // @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[] - elasticsearchRole, + elasticsearchRole, // TODO: address why the `remote_cluster` field is throwing type errors name: roleName, application: authz.applicationName, logger, From f3df93fb50b81565ab1e283cec123d99042aed7a Mon Sep 17 00:00:00 2001 From: SiddharthMantri Date: Wed, 18 Dec 2024 15:14:48 +0530 Subject: [PATCH 08/45] update query endpoint to use metadata reserved roles correctly --- .../roles/roles_grid/roles_grid_page.tsx | 4 +-- .../routes/authorization/roles/query.ts | 35 ++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index ba96821f7aecb..d5e45ef017172 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -108,12 +108,12 @@ export const RolesGridPage: FC = ({ const [tableState, setTableState] = useState(DEFAULT_TABLE_STATE); const loadRoles = async (tableStateArgs: RolesTableState) => { - const queryContainer = EuiSearchBar.Query.toESQuery(tableStateArgs.query); + const queryText = tableStateArgs.query.text; const requestBody = { ...tableStateArgs, ...(tableStateArgs.sort ? { sort: tableStateArgs.sort } : DEFAULT_TABLE_STATE.sort), - query: queryContainer, + query: queryText, }; try { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 5a24ef3629eaf..0b9c26148cdea 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -33,7 +33,7 @@ export function defineQueryRolesRoutes({ }, validate: { body: schema.object({ - query: schema.maybe(schema.object({}, { unknowns: 'allow' })), + query: schema.maybe(schema.string()), from: schema.maybe(schema.number()), size: schema.maybe(schema.number()), sort: schema.maybe( @@ -65,15 +65,42 @@ export function defineQueryRolesRoutes({ } const queryPayload: { - bool: { must: QueryClause[]; should: QueryClause[]; must_not: QueryClause[] }; + bool: { + must: QueryClause[]; + should: QueryClause[]; + must_not: QueryClause[]; + minimum_should_match?: number; + }; } = { bool: { must: [], should: [], must_not: [] } }; if (query) { - queryPayload.bool.must.push(query); + queryPayload.bool.must.push({ + wildcard: { + name: { + value: `*${query}*`, + }, + }, + }); } if (showReservedRoles) { - queryPayload.bool.should.push({ term: { 'metadata._reserved': true } }); + queryPayload.bool.must.push({ term: { 'metadata._reserved': true } }); + } else if (showReservedRoles === false) { + queryPayload.bool.should.push({ + term: { + 'metadata._reserved': false, + }, + }); + queryPayload.bool.should.push({ + bool: { + must_not: { + exists: { + field: 'metadata._reserved', + }, + }, + }, + }); + queryPayload.bool.minimum_should_match = 1; } if (filters?.spaceId) { From 09251e20f8e0a60a11a2a1985f05160b41ae3326 Mon Sep 17 00:00:00 2001 From: SiddharthMantri Date: Wed, 18 Dec 2024 15:31:01 +0530 Subject: [PATCH 09/45] update must to should --- .../plugins/security/server/routes/authorization/roles/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 0b9c26148cdea..995392495b904 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -84,7 +84,7 @@ export function defineQueryRolesRoutes({ } if (showReservedRoles) { - queryPayload.bool.must.push({ term: { 'metadata._reserved': true } }); + queryPayload.bool.should.push({ term: { 'metadata._reserved': true } }); } else if (showReservedRoles === false) { queryPayload.bool.should.push({ term: { From 012deae7b170e3434d987381a93884b043e84def Mon Sep 17 00:00:00 2001 From: SiddharthMantri Date: Sat, 21 Dec 2024 21:16:08 +0530 Subject: [PATCH 10/45] fix tests and query dsl --- .../management/roles/roles_api_client.mock.ts | 1 + .../roles/roles_grid/roles_grid_page.tsx | 2 +- .../routes/authorization/roles/query.test.ts | 46 ++++++++++++++++++- .../routes/authorization/roles/query.ts | 36 ++++++++------- 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts index 5f868fda093a4..015f43af4452c 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts @@ -12,5 +12,6 @@ export const rolesAPIClientMock = { deleteRole: jest.fn(), saveRole: jest.fn(), bulkUpdateRoles: jest.fn(), + queryRoles: jest.fn(), }), }; diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index d5e45ef017172..25959bb62fb1a 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -290,7 +290,7 @@ export const RolesGridPage: FC = ({ name: i18n.translate('xpack.security.management.roles.descriptionColumnName', { defaultMessage: 'Role Description', }), - sortable: true, + sortable: false, truncateText: { lines: 3 }, render: (description: string, record: Role) => ( diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts index b981c73d859e9..b412cea3a03da 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts @@ -8,10 +8,52 @@ import Boom from '@hapi/boom'; import { kibanaResponseFactory } from '@kbn/core/server'; +import type { RequestHandler } from '@kbn/core/server'; +import type { CustomRequestHandlerMock, ScopedClusterClientMock } from '@kbn/core/server/mocks'; import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; -import type { LicenseCheck } from '@kbn/licensing-plugin/server'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { defineQueryRolesRoutes } from './query'; +import type { InternalAuthenticationServiceStart } from '../../../authentication'; +import { authenticationServiceMock } from '../../../authentication/authentication_service.mock'; import { routeDefinitionParamsMock } from '../../index.mock'; -describe('Query roles', () => {}); +describe('Query roles', () => { + let routeHandler: RequestHandler; + let authc: DeeplyMockedKeys; + let esClientMock: ScopedClusterClientMock; + let mockContext: CustomRequestHandlerMock; + + beforeEach(async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + authc = authenticationServiceMock.createStart(); + mockRouteDefinitionParams.getAuthenticationService.mockReturnValue(authc); + defineQueryRolesRoutes(mockRouteDefinitionParams); + [[, routeHandler]] = mockRouteDefinitionParams.router.post.mock.calls; + mockContext = coreMock.createCustomRequestHandlerContext({ + core: coreMock.createRequestHandlerContext(), + licensing: licensingMock.createRequestHandlerContext(), + }); + + esClientMock = (await mockContext.core).elasticsearch.client; + + authc.apiKeys.areAPIKeysEnabled.mockResolvedValue(true); + authc.apiKeys.areCrossClusterAPIKeysEnabled.mockResolvedValue(true); + + esClientMock.asCurrentUser.security.hasPrivileges.mockResponse({ + cluster: { + manage_security: true, + read_security: true, + manage_api_key: true, + manage_own_api_key: true, + }, + } as any); + + esClientMock.asCurrentUser.security.queryRole.mockResponse({ + total: 2, + count: 2, + roles: [{ name: 'role1' }, { name: 'role2' }], + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 995392495b904..1659d773eefaf 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -30,6 +30,7 @@ export function defineQueryRolesRoutes({ options: { summary: `Query roles`, access: 'public', + tags: ['oas-tags:roles'], }, validate: { body: schema.object({ @@ -73,6 +74,25 @@ export function defineQueryRolesRoutes({ }; } = { bool: { must: [], should: [], must_not: [] } }; + const nonReservedRolesQuery = [ + { + term: { + 'metadata._reserved': false, + }, + }, + { + 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: { @@ -85,22 +105,6 @@ export function defineQueryRolesRoutes({ if (showReservedRoles) { queryPayload.bool.should.push({ term: { 'metadata._reserved': true } }); - } else if (showReservedRoles === false) { - queryPayload.bool.should.push({ - term: { - 'metadata._reserved': false, - }, - }); - queryPayload.bool.should.push({ - bool: { - must_not: { - exists: { - field: 'metadata._reserved', - }, - }, - }, - }); - queryPayload.bool.minimum_should_match = 1; } if (filters?.spaceId) { From 3dd451ee309b73f29a068e63f1fb0ac812f5eeea Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Dec 2024 11:18:14 +0100 Subject: [PATCH 11/45] update tests, add check for max items --- .../roles/roles_grid/roles_grid_page.tsx | 19 +- .../routes/authorization/roles/query.test.ts | 433 ++++++++++++++++-- .../routes/authorization/roles/query.ts | 14 + 3 files changed, 428 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 25959bb62fb1a..3f834a98238c5 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -76,6 +76,8 @@ const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { return `/${action}${roleName ? `/${encodeURIComponent(roleName)}` : ''}`; }; +const MAX_PAGINATED_ITEMS = 10000; + const DEFAULT_TABLE_STATE = { query: EuiSearchBar.Query.MATCH_ALL, sort: { @@ -390,12 +392,15 @@ export const RolesGridPage: FC = ({ 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, + totalItemCount: displayedItemCount, pageSizeOptions: [25, 50, 100], }; + const exceededResultCount = totalItemCount > MAX_PAGINATED_ITEMS; return permissionDenied ? ( @@ -487,6 +492,18 @@ export const RolesGridPage: FC = ({ toolsRight={renderToolsRight()} /> + {exceededResultCount && ( + <> + + + + + + )} unknown; + asserts: { statusCode: number; result?: Record; calledWith?: Record }; + query?: Record; +} + +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', () => { - let routeHandler: RequestHandler; - let authc: DeeplyMockedKeys; - let esClientMock: ScopedClusterClientMock; - let mockContext: CustomRequestHandlerMock; - - beforeEach(async () => { - const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - authc = authenticationServiceMock.createStart(); - mockRouteDefinitionParams.getAuthenticationService.mockReturnValue(authc); - defineQueryRolesRoutes(mockRouteDefinitionParams); - [[, routeHandler]] = mockRouteDefinitionParams.router.post.mock.calls; - mockContext = coreMock.createCustomRequestHandlerContext({ - core: coreMock.createRequestHandlerContext(), - licensing: licensingMock.createRequestHandlerContext(), - }); + const queryRolesTest = ( + description: string, + { licenseCheckResult = { state: 'valid' }, apiResponse, asserts, query }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + mockRouteDefinitionParams.authz.applicationName = application; + mockRouteDefinitionParams.getFeatures = jest.fn().mockResolvedValue(features); + mockRouteDefinitionParams.subFeaturePrivilegeIterator = + featuresPluginMock.createSetup().subFeaturePrivilegeIterator; - esClientMock = (await mockContext.core).elasticsearch.client; + defineQueryRolesRoutes(mockRouteDefinitionParams); + const [[, routeHandler]] = mockRouteDefinitionParams.router.post.mock.calls; - authc.apiKeys.areAPIKeysEnabled.mockResolvedValue(true); - authc.apiKeys.areCrossClusterAPIKeysEnabled.mockResolvedValue(true); + const mockCoreContext = coreMock.createRequestHandlerContext(); + const mockLicensingContext = { + license: { check: jest.fn().mockReturnValue(licenseCheckResult) }, + } as any; + const mockContext = coreMock.createCustomRequestHandlerContext({ + core: mockCoreContext, + licensing: mockLicensingContext, + }); - esClientMock.asCurrentUser.security.hasPrivileges.mockResponse({ - cluster: { - manage_security: true, - read_security: true, - manage_api_key: true, - manage_own_api_key: true, + 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, }, - } as any); + 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': false, + }, + }, + { + bool: { + must_not: { + exists: { + field: 'metadata._reserved', + }, + }, + }, + }, + ], + }, + }, + }, + }, + }); - esClientMock.asCurrentUser.security.queryRole.mockResponse({ - total: 2, - count: 2, - roles: [{ name: 'role1' }, { name: 'role2' }], + 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', + }, + }, + ], + }, + }, }); }); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 1659d773eefaf..fe4280955ebef 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -124,6 +124,19 @@ export function defineQueryRolesRoutes({ sort: transformedSort, }); + console.log( + JSON.stringify( + { + query: queryPayload, + from, + size, + sort: transformedSort, + }, + null, + 2 + ) + ); + const transformedRoles = queryRoles.roles?.map((role) => transformElasticsearchRoleToRole({ features, @@ -142,6 +155,7 @@ export function defineQueryRolesRoutes({ }, }); } catch (error) { + console.log(error); return response.customError(wrapIntoCustomErrorResponse(error)); } }) From d921b6ea5b9d2821014728c0d202d40c3b0aad5c Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Dec 2024 11:29:37 +0100 Subject: [PATCH 12/45] remove console log --- .../server/routes/authorization/roles/query.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index fe4280955ebef..09693a6cce720 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -124,19 +124,6 @@ export function defineQueryRolesRoutes({ sort: transformedSort, }); - console.log( - JSON.stringify( - { - query: queryPayload, - from, - size, - sort: transformedSort, - }, - null, - 2 - ) - ); - const transformedRoles = queryRoles.roles?.map((role) => transformElasticsearchRoleToRole({ features, From 607aa7518da1523d9026d62f8293f7d0f9e12a75 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:44:53 +0000 Subject: [PATCH 13/45] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 74 +++++++++++++++++++++++++++++++++ oas_docs/bundle.serverless.json | 74 +++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 3e3d47df01661..77ee71b1275d5 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -39116,6 +39116,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": { + "showReserved": { + "type": "boolean" + }, + "spaceId": { + "minLength": 1, + "type": "string" + } + }, + "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": {}, + "summary": "Query roles", + "tags": [] + } + }, "/api/security/role/{name}": { "delete": { "operationId": "delete-security-role-name", diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index b188ae0999b0d..62d4bce407926 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -39116,6 +39116,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": { + "showReserved": { + "type": "boolean" + }, + "spaceId": { + "minLength": 1, + "type": "string" + } + }, + "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": {}, + "summary": "Query roles", + "tags": [] + } + }, "/api/security/role/{name}": { "delete": { "operationId": "delete-security-role-name", From b1a3e8dc2f588d7a59b6638faee84adab94c6d2f Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Dec 2024 12:01:18 +0100 Subject: [PATCH 14/45] use fallback for no roles --- .../plugins/security/server/routes/authorization/roles/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 09693a6cce720..29710f1199293 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -124,7 +124,7 @@ export function defineQueryRolesRoutes({ sort: transformedSort, }); - const transformedRoles = queryRoles.roles?.map((role) => + const transformedRoles = (queryRoles.roles || []).map((role) => transformElasticsearchRoleToRole({ features, elasticsearchRole: role, // TODO: address why the `remote_cluster` field is throwing type errors From 95653e793005bfbdc51eda729dfc2941e22ab919 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Dec 2024 12:06:19 +0100 Subject: [PATCH 15/45] remove logs --- .../plugins/security/server/routes/authorization/roles/query.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 29710f1199293..78a2a7458247e 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -142,7 +142,6 @@ export function defineQueryRolesRoutes({ }, }); } catch (error) { - console.log(error); return response.customError(wrapIntoCustomErrorResponse(error)); } }) From a528369b78c8ca4c91cd631938e737917eb2f8f6 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:20:19 +0000 Subject: [PATCH 16/45] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 51 ++++++++++++++++++++++++++ oas_docs/output/kibana.yaml | 50 +++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 2a942bc85c3bc..cfc00a5953e9f 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -34344,6 +34344,57 @@ 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: + showReserved: + type: boolean + spaceId: + minLength: 1 + type: string + 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: {} + summary: Query roles + tags: [] + x-beta: true /api/security/role/{name}: delete: operationId: delete-security-role-name diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 5845ba56ae895..a646c905e8025 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -36906,6 +36906,56 @@ 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: + showReserved: + type: boolean + spaceId: + minLength: 1 + type: string + 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: {} + summary: Query roles + tags: [] /api/security/role/{name}: delete: operationId: delete-security-role-name From 2b42662d60113a2b1edcd8fa745285c158d4ea2e Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:40:55 +0000 Subject: [PATCH 17/45] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../security/plugin_types_common/src/authorization/role.ts | 1 + .../plugins/security/public/management/roles/roles_api_client.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/packages/security/plugin_types_common/src/authorization/role.ts b/x-pack/packages/security/plugin_types_common/src/authorization/role.ts index 2a2034dada666..79fe064752e0d 100644 --- a/x-pack/packages/security/plugin_types_common/src/authorization/role.ts +++ b/x-pack/packages/security/plugin_types_common/src/authorization/role.ts @@ -6,6 +6,7 @@ */ import { estypes } from '@elastic/elasticsearch'; + import type { FeaturesPrivileges } from './features_privileges'; export interface RoleIndexPrivilege { diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts index d7e6fd16c74ab..81f86f47fed5c 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -62,7 +62,6 @@ export class RolesAPIClient { }); }; - public bulkUpdateRoles = async ({ rolesUpdate, }: BulkUpdatePayload): Promise => { From 2edd7f2265b3fd7694024080bd6d3e7ab966b237 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Dec 2024 16:13:11 +0100 Subject: [PATCH 18/45] update Ui unit test --- .../roles/roles_grid/roles_grid_page.test.tsx | 210 ++++-------------- 1 file changed, 46 insertions(+), 164 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 1c965551a63dc..e57f68cd9d1b4 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiIcon, EuiInMemoryTable } from '@elastic/eui'; +import { EuiBasicTable, EuiIcon } from '@elastic/eui'; import type { ReactWrapper } from 'enzyme'; import React from 'react'; @@ -51,36 +51,40 @@ describe('', () => { 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('', () => { }); it('renders permission denied if required', async () => { - apiClientMock.getRoles.mockRejectedValue(mock403()); + apiClientMock.queryRoles.mockRejectedValue(mock403()); const wrapper = mountWithIntl( ', () => { return updatedWrapper.find(EuiIcon).length > initialIconCount; }); - expect(wrapper.find(EuiInMemoryTable).props().items).toEqual([ + expect(wrapper.find(EuiBasicTable).props().items).toEqual([ { name: 'test-role-1', elasticsearch: { @@ -301,140 +305,18 @@ describe('', () => { 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: {} }], + expect(apiClientMock.queryRoles).toHaveBeenCalledWith({ + filters: { + showReserved: true, }, - { - 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 }, + from: 0, + size: 25, + query: '', + sort: { + direction: 'asc', + field: 'name', }, - { - 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( - - ); - 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 () => { From 9e59c121594ce5db5013ab3a3dff9880648f4e2a Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Dec 2024 19:35:38 +0100 Subject: [PATCH 19/45] fix tests for get all roles by space --- .../roles/get_all_by_space.test.ts | 350 ++++++++++-------- .../authorization/roles/get_all_by_space.ts | 6 +- .../routes/authorization/roles/query.ts | 3 +- 3 files changed, 198 insertions(+), 161 deletions(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts index 956ced4309304..46b5199bef39c 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts @@ -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, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts index b21bdf81047ba..ab5b5962a1c66 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts @@ -57,7 +57,7 @@ export function defineGetAllRolesBySpaceRoutes({ }, }), ]); - const elasticsearchRoles = queryRolesResponse.roles?.reduce< + const elasticsearchRoles = (queryRolesResponse.roles || [])?.reduce< Record >((acc, role) => { return { @@ -65,6 +65,7 @@ export function defineGetAllRolesBySpaceRoutes({ [role.name]: role, }; }, {}); + // Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name. return response.ok({ body: Object.entries(elasticsearchRoles) @@ -75,7 +76,8 @@ export function defineGetAllRolesBySpaceRoutes({ const role = transformElasticsearchRoleToRole({ features, - elasticsearchRole, // TODO: address why the `remote_cluster` field is throwing type errors + // @ts-expect-error `remote_cluster` is not known in `Role` type + elasticsearchRole, name: roleName, application: authz.applicationName, logger, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 78a2a7458247e..84d611ced4a54 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -127,7 +127,8 @@ export function defineQueryRolesRoutes({ const transformedRoles = (queryRoles.roles || []).map((role) => transformElasticsearchRoleToRole({ features, - elasticsearchRole: role, // TODO: address why the `remote_cluster` field is throwing type errors + // @ts-expect-error `remote_cluster` is not known in `Role` type + elasticsearchRole: role, name: role.name, application: authz.applicationName, logger, From f5f39b5dc9564d93f020279ac5803afcfa5affe5 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Dec 2024 21:33:14 +0100 Subject: [PATCH 20/45] add integration tests --- .../management/roles/roles_api_client.ts | 2 +- .../roles/roles_grid/roles_grid_page.tsx | 8 +- .../routes/authorization/roles/query.ts | 13 +- .../api_integration/apis/security/roles.ts | 113 ++++++++++++++++++ 4 files changed, 120 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts index 81f86f47fed5c..dc2d01200ef84 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -20,7 +20,7 @@ export interface QueryRoleParams { from: number; size: number; filters?: { - showReserved?: boolean; + showReservedRoles?: boolean; }; sort: Criteria['sort']; } diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 3f834a98238c5..121c3e93f420b 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -68,7 +68,7 @@ interface RolesTableState { from: number; size: number; filters: { - showReserved?: boolean; + showReservedRoles?: boolean; }; } @@ -87,7 +87,7 @@ const DEFAULT_TABLE_STATE = { from: 0, size: 25, filters: { - showReserved: true, + showReservedRoles: true, }, }; @@ -146,7 +146,7 @@ export const RolesGridPage: FC = ({ const newTableStateArgs = { ...tableState, filters: { - showReserved: e.target.checked, + showReservedRoles: e.target.checked, }, }; setTableState(newTableStateArgs); @@ -239,7 +239,7 @@ export const RolesGridPage: FC = ({ defaultMessage="Show reserved roles" /> } - checked={tableState.filters.showReserved ?? true} + checked={tableState.filters.showReservedRoles ?? true} onChange={onIncludeReservedRolesChange} /> ); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index 84d611ced4a54..a71e784bc9127 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -45,8 +45,7 @@ export function defineQueryRolesRoutes({ ), filters: schema.maybe( schema.object({ - showReserved: schema.maybe(schema.boolean({ defaultValue: true })), - spaceId: schema.maybe(schema.string({ minLength: 1 })), + showReservedRoles: schema.maybe(schema.boolean({ defaultValue: true })), }) ), }), @@ -59,7 +58,7 @@ export function defineQueryRolesRoutes({ const { query, size, from, sort, filters } = request.body; - let showReservedRoles = filters?.showReserved; + let showReservedRoles = filters?.showReservedRoles; if (buildFlavor === 'serverless') { showReservedRoles = false; @@ -107,14 +106,6 @@ export function defineQueryRolesRoutes({ queryPayload.bool.should.push({ term: { 'metadata._reserved': true } }); } - if (filters?.spaceId) { - queryPayload.bool.must.push({ - term: { - 'applications.resources': `space:${filters.spaceId}`, - }, - }); - } - const transformedSort = sort && [{ [sort.field]: { order: sort.direction } }]; const queryRoles = await esClient.asCurrentUser.security.queryRole({ diff --git a/x-pack/test/api_integration/apis/security/roles.ts b/x-pack/test/api_integration/apis/security/roles.ts index f6cf615d0f71b..3f06721561094 100644 --- a/x-pack/test/api_integration/apis/security/roles.ts +++ b/x-pack/test/api_integration/apis/security/roles.ts @@ -519,5 +519,118 @@ export default function ({ getService }: FtrProviderContext) { expect(roleToUpdateWithDlsFls).to.eql({}); }); }); + + describe.only('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); + }); + }); }); } From 6edcbbf0ac96b1fb72a1ad7bc8a97d3cb548f409 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Dec 2024 21:33:26 +0100 Subject: [PATCH 21/45] remove exclusive test --- x-pack/test/api_integration/apis/security/roles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/security/roles.ts b/x-pack/test/api_integration/apis/security/roles.ts index 3f06721561094..e49d19f15afd3 100644 --- a/x-pack/test/api_integration/apis/security/roles.ts +++ b/x-pack/test/api_integration/apis/security/roles.ts @@ -520,7 +520,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - describe.only('Query Role', () => { + describe('Query Role', () => { it('should query roles by name', async () => { await es.security.putRole({ name: 'role_to_query', From d10984a919aade0efad59c3a577de94737a6020a Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 23 Dec 2024 20:46:50 +0000 Subject: [PATCH 22/45] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 6 +----- oas_docs/bundle.serverless.json | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 77ee71b1275d5..3bfb4306bb3b0 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -39140,12 +39140,8 @@ "filters": { "additionalProperties": false, "properties": { - "showReserved": { + "showReservedRoles": { "type": "boolean" - }, - "spaceId": { - "minLength": 1, - "type": "string" } }, "type": "object" diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 62d4bce407926..7e8fba0ee180b 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -39140,12 +39140,8 @@ "filters": { "additionalProperties": false, "properties": { - "showReserved": { + "showReservedRoles": { "type": "boolean" - }, - "spaceId": { - "minLength": 1, - "type": "string" } }, "type": "object" From 4171befc3c54ec500496b61d169b7a37d8e6cba0 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:01:13 +0000 Subject: [PATCH 23/45] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 5 +---- oas_docs/output/kibana.yaml | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index cfc00a5953e9f..4b03c211c55e7 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -34366,11 +34366,8 @@ paths: additionalProperties: false type: object properties: - showReserved: + showReservedRoles: type: boolean - spaceId: - minLength: 1 - type: string from: type: number query: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index a646c905e8025..fdbf2c5049626 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -36928,11 +36928,8 @@ paths: additionalProperties: false type: object properties: - showReserved: + showReservedRoles: type: boolean - spaceId: - minLength: 1 - type: string from: type: number query: From 312f160af763963986c4ea2da3d1d2948fc91a80 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Dec 2024 22:52:45 +0100 Subject: [PATCH 24/45] fix types --- .../plugins/security/public/management/roles/roles_api_client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts index dc2d01200ef84..7449582a70a46 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -10,6 +10,7 @@ import type { QueryContainer } from '@elastic/eui/src/components/search_bar/quer 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'; From 60d77fad47cf05160c6b9a3dd301f561c7e2a5b6 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Dec 2024 22:58:16 +0100 Subject: [PATCH 25/45] use string instead of query container --- .../security/public/management/roles/roles_api_client.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts index 7449582a70a46..ee1d269a98cb4 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -6,7 +6,6 @@ */ import type { Criteria } from '@elastic/eui'; -import type { QueryContainer } from '@elastic/eui/src/components/search_bar/query/ast_to_es_query_dsl'; import type { HttpStart } from '@kbn/core/public'; import type { QueryRolesResult } from '@kbn/security-plugin-types-common'; @@ -17,7 +16,7 @@ import { API_VERSIONS } from '../../../common/constants'; import { copyRole } from '../../../common/model'; export interface QueryRoleParams { - query: QueryContainer; + query: string; from: number; size: number; filters?: { From 2333a1e8130061589eeb003ea5d3190c04501604 Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 24 Dec 2024 13:31:54 +0100 Subject: [PATCH 26/45] fix jest test --- .../management/roles/roles_grid/roles_grid_page.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index e57f68cd9d1b4..8ec03c1a0486e 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -305,9 +305,13 @@ describe('', () => { findTestSubject(wrapper, 'showReservedRolesSwitch').simulate('click'); + await waitForRender(wrapper, (updatedWrapper) => { + return updatedWrapper.find(EuiIcon).length > initialIconCount; + }); + expect(apiClientMock.queryRoles).toHaveBeenCalledWith({ filters: { - showReserved: true, + showReservedRoles: true, }, from: 0, size: 25, From b599bc1a7d5ff8ea7612cd0e5588b6968bea2ba5 Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 24 Dec 2024 14:23:56 +0100 Subject: [PATCH 27/45] remove unneeded test --- .../roles/roles_grid/roles_grid_page.test.tsx | 124 ------------------ 1 file changed, 124 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 8ec03c1a0486e..a891c6262e014 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -199,130 +199,6 @@ describe('', () => { ); }); - it('hides reserved roles when instructed to', async () => { - const wrapper = mountWithIntl( - - ); - const initialIconCount = wrapper.find(EuiIcon).length; - - await waitForRender(wrapper, (updatedWrapper) => { - return updatedWrapper.find(EuiIcon).length > initialIconCount; - }); - - expect(wrapper.find(EuiBasicTable).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'); - - await waitForRender(wrapper, (updatedWrapper) => { - return updatedWrapper.find(EuiIcon).length > initialIconCount; - }); - - expect(apiClientMock.queryRoles).toHaveBeenCalledWith({ - filters: { - showReservedRoles: true, - }, - from: 0, - size: 25, - query: '', - sort: { - direction: 'asc', - field: 'name', - }, - }); - }); - it('hides controls when `readOnly` is enabled', async () => { const wrapper = mountWithIntl( Date: Tue, 24 Dec 2024 14:25:37 +0100 Subject: [PATCH 28/45] remove imports --- .../public/management/roles/roles_grid/roles_grid_page.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index a891c6262e014..f34bd61ab75df 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiBasicTable, EuiIcon } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; import type { ReactWrapper } from 'enzyme'; import React from 'react'; From 45259e5d5094b2cd5a754c97ecb42bea6da6158b Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 27 Dec 2024 14:28:54 +0100 Subject: [PATCH 29/45] add test subject --- .../public/management/roles/roles_grid/roles_grid_page.tsx | 2 +- x-pack/test/functional/page_objects/security_page.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 121c3e93f420b..adbc19baffd82 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -505,7 +505,7 @@ export const RolesGridPage: FC = ({ )} el.getVisibleText()), @@ -728,7 +732,7 @@ export class SecurityPageObject extends FtrService { await this.addRemoteClusterPrivilege(remoteClusterPrivilege, index); } } - + console.log('is create role'); await this.saveRole(); } From ead1937a422cd39d7e3a292fe1c01a297bb24411 Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 27 Dec 2024 14:38:34 +0100 Subject: [PATCH 30/45] remove console log --- x-pack/test/functional/page_objects/security_page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index f50f261ff258b..a2fdfc157ce69 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -732,7 +732,7 @@ export class SecurityPageObject extends FtrService { await this.addRemoteClusterPrivilege(remoteClusterPrivilege, index); } } - console.log('is create role'); + await this.saveRole(); } From bfff37d9f5941315d5a73d0ee47ea28858c68599 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 30 Dec 2024 13:27:30 +0100 Subject: [PATCH 31/45] add esJavaOpts --- x-pack/test/functional/apps/security/config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/test/functional/apps/security/config.ts b/x-pack/test/functional/apps/security/config.ts index d0d07ff200281..2aa6d708f29fb 100644 --- a/x-pack/test/functional/apps/security/config.ts +++ b/x-pack/test/functional/apps/security/config.ts @@ -13,5 +13,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], + esTestCluster: { + serverArgs: { + esJavaOpts: '-Des.queryable_built_in_roles_enabled=true', + }, + }, }; } From eb79bbbccffaa7f2ffc79b592a23e41bf193907f Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 30 Dec 2024 13:27:39 +0100 Subject: [PATCH 32/45] fix comment --- x-pack/test/functional/apps/security/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/functional/apps/security/config.ts b/x-pack/test/functional/apps/security/config.ts index 2aa6d708f29fb..ffb391cebd3da 100644 --- a/x-pack/test/functional/apps/security/config.ts +++ b/x-pack/test/functional/apps/security/config.ts @@ -15,6 +15,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [require.resolve('.')], esTestCluster: { serverArgs: { + // TODO: remove this once ES has built-in roles enabled by default esJavaOpts: '-Des.queryable_built_in_roles_enabled=true', }, }, From 15957366a8e577d5e27dda41466f09659e7fd983 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 30 Dec 2024 13:37:27 +0100 Subject: [PATCH 33/45] fix usage of esTestCluster config --- x-pack/test/functional/apps/security/config.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/security/config.ts b/x-pack/test/functional/apps/security/config.ts index ffb391cebd3da..1f851048ce923 100644 --- a/x-pack/test/functional/apps/security/config.ts +++ b/x-pack/test/functional/apps/security/config.ts @@ -14,10 +14,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], esTestCluster: { - serverArgs: { - // TODO: remove this once ES has built-in roles enabled by default - esJavaOpts: '-Des.queryable_built_in_roles_enabled=true', - }, + ...functionalConfig.get('esTestCluster'), + // TODO: remove this once ES has built-in roles enabled by default + esJavaOpts: '-Des.queryable_built_in_roles_enabled=true', }, }; } From c95ad2408af5f7161e3bccbfec842a3dd69dcece Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 31 Dec 2024 15:31:11 +0100 Subject: [PATCH 34/45] fix spaces tes{ --- .../authorization/roles/get_all_by_space.ts | 25 +++++++++++++++++-- x-pack/test/functional/apps/spaces/config.ts | 5 ++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts index ab5b5962a1c66..e6fbaf76bea9d 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts @@ -42,19 +42,40 @@ export function defineGetAllRolesBySpaceRoutes({ const [features, queryRolesResponse] = await Promise.all([ getFeatures(), - // await esClient.asCurrentUser.security.getRole(), await esClient.asCurrentUser.security.queryRole({ query: { bool: { - must: [ + should: [ { term: { 'applications.resources': `space:${request.params.spaceId}`, }, }, + { + term: { + 'metadata._reserved': true, + }, + }, + { + term: { + 'metadata._reserved': false, + }, + }, + { + bool: { + must_not: { + exists: { + field: 'metadata._reserved', + }, + }, + }, + }, ], + minimum_should_match: 1, }, }, + from: 0, + size: 100, }), ]); const elasticsearchRoles = (queryRolesResponse.roles || [])?.reduce< diff --git a/x-pack/test/functional/apps/spaces/config.ts b/x-pack/test/functional/apps/spaces/config.ts index d0d07ff200281..1f851048ce923 100644 --- a/x-pack/test/functional/apps/spaces/config.ts +++ b/x-pack/test/functional/apps/spaces/config.ts @@ -13,5 +13,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + // TODO: remove this once ES has built-in roles enabled by default + esJavaOpts: '-Des.queryable_built_in_roles_enabled=true', + }, }; } From af84349fe6f716a7e46966d4c2a417775787645a Mon Sep 17 00:00:00 2001 From: Sid Date: Thu, 2 Jan 2025 19:50:12 +0100 Subject: [PATCH 35/45] update page size to 1000 --- .../server/routes/authorization/roles/get_all_by_space.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts index e6fbaf76bea9d..204173e818c0c 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts @@ -75,7 +75,7 @@ export function defineGetAllRolesBySpaceRoutes({ }, }, from: 0, - size: 100, + size: 1000, }), ]); const elasticsearchRoles = (queryRolesResponse.roles || [])?.reduce< From 37107803ac045f342f7baff40c0f18fb5a58bac2 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 2 Jan 2025 18:59:32 +0000 Subject: [PATCH 36/45] [CI] Auto-commit changed files from 'node scripts/yarn_deduplicate' --- NOTICE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOTICE.txt b/NOTICE.txt index 9cd38e6773d88..312326d7e41a9 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ Kibana source code with Kibana X-Pack source code -Copyright 2012-2024 Elasticsearch B.V. +Copyright 2012-2025 Elasticsearch B.V. --- Adapted from remote-web-worker, which was available under a "MIT" license. From 0b5f7957930632b325f7b227ffd533ef21e2d8e4 Mon Sep 17 00:00:00 2001 From: Sid Date: Thu, 9 Jan 2025 10:04:10 +0100 Subject: [PATCH 37/45] fix tests, update authz --- .../server/routes/authorization/roles/query.test.ts | 1 + .../security/server/routes/authorization/roles/query.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts index 843c3b9c7f72c..c9166da4743b7 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.test.ts @@ -299,6 +299,7 @@ describe('Query roles', () => { must: [], must_not: [], should: [ + { term: { 'metadata._reserved': true } }, { term: { 'metadata._reserved': false, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/query.ts b/x-pack/plugins/security/server/routes/authorization/roles/query.ts index a71e784bc9127..1d07b5a1c9521 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/query.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/query.ts @@ -32,6 +32,12 @@ export function defineQueryRolesRoutes({ access: 'public', tags: ['oas-tags:roles'], }, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { body: schema.object({ query: schema.maybe(schema.string()), From 1c542cee23fbf3124a70aa118d6267590130a773 Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 10 Jan 2025 14:40:04 +0100 Subject: [PATCH 38/45] updat config --- x-pack/test/api_integration/apis/security/config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/test/api_integration/apis/security/config.ts b/x-pack/test/api_integration/apis/security/config.ts index 5f335f116fefe..75237366ab10c 100644 --- a/x-pack/test/api_integration/apis/security/config.ts +++ b/x-pack/test/api_integration/apis/security/config.ts @@ -13,5 +13,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...baseIntegrationTestsConfig.getAll(), testFiles: [require.resolve('.')], + esTestCluster: { + ...baseIntegrationTestsConfig.get('esTestCluster'), + // TODO: remove this once ES has built-in roles enabled by default + esJavaOpts: '-Des.queryable_built_in_roles_enabled=true', + }, }; } From 399b4d5f8f348cec10f97a5e346d5bdaee612bd7 Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 10 Jan 2025 15:42:47 +0100 Subject: [PATCH 39/45] remove unused term query --- .../server/routes/authorization/roles/get_all_by_space.ts | 5 ----- .../security/server/routes/authorization/roles/query.ts | 5 ----- 2 files changed, 10 deletions(-) diff --git a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/get_all_by_space.ts b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/get_all_by_space.ts index 204173e818c0c..53419bfe3b07a 100644 --- a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/get_all_by_space.ts +++ b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/get_all_by_space.ts @@ -56,11 +56,6 @@ export function defineGetAllRolesBySpaceRoutes({ 'metadata._reserved': true, }, }, - { - term: { - 'metadata._reserved': false, - }, - }, { bool: { must_not: { diff --git a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts index 1d07b5a1c9521..c78f39beec80c 100644 --- a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts +++ b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts @@ -80,11 +80,6 @@ export function defineQueryRolesRoutes({ } = { bool: { must: [], should: [], must_not: [] } }; const nonReservedRolesQuery = [ - { - term: { - 'metadata._reserved': false, - }, - }, { bool: { must_not: { From 6300b66142437296230657b16ab738551a8bc52b Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 14 Jan 2025 12:40:28 +0100 Subject: [PATCH 40/45] update test --- .../security/server/routes/authorization/roles/query.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.test.ts b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.test.ts index c9166da4743b7..c68930fe29882 100644 --- a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.test.ts +++ b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.test.ts @@ -300,11 +300,6 @@ describe('Query roles', () => { must_not: [], should: [ { term: { 'metadata._reserved': true } }, - { - term: { - 'metadata._reserved': false, - }, - }, { bool: { must_not: { From 989120fb093de30dd4bf25d8e1e5fbb608c513b1 Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 14 Jan 2025 15:10:36 +0100 Subject: [PATCH 41/45] use versioned router, update tests and api client --- .../management/roles/roles_api_client.ts | 1 + .../routes/authorization/roles/query.test.ts | 9 +- .../routes/authorization/roles/query.ts | 200 ++++++++++-------- 3 files changed, 115 insertions(+), 95 deletions(-) diff --git a/x-pack/platform/plugins/shared/security/public/management/roles/roles_api_client.ts b/x-pack/platform/plugins/shared/security/public/management/roles/roles_api_client.ts index ee1d269a98cb4..bbef97370df17 100644 --- a/x-pack/platform/plugins/shared/security/public/management/roles/roles_api_client.ts +++ b/x-pack/platform/plugins/shared/security/public/management/roles/roles_api_client.ts @@ -39,6 +39,7 @@ export class RolesAPIClient { public queryRoles = async (params?: QueryRoleParams) => { return await this.http.post(`/api/security/role/_query`, { + version, body: JSON.stringify(params || {}), }); }; diff --git a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.test.ts b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.test.ts index c68930fe29882..b63aedf147804 100644 --- a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.test.ts +++ b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.test.ts @@ -7,11 +7,13 @@ 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 { @@ -143,13 +145,18 @@ describe('Query roles', () => { ) => { 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 [[, routeHandler]] = mockRouteDefinitionParams.router.post.mock.calls; + const { handler: routeHandler } = versionedRouterMock.getRoute( + 'post', + '/api/security/role/_query' + ).versions[API_VERSIONS.roles.public.v1]; const mockCoreContext = coreMock.createRequestHandlerContext(); const mockLicensingContext = { diff --git a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts index c78f39beec80c..b1d0373387c03 100644 --- a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts +++ b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts @@ -9,6 +9,7 @@ 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'; @@ -24,119 +25,130 @@ export function defineQueryRolesRoutes({ logger, buildFlavor, }: RouteDefinitionParams) { - router.post( - { + router.versioned + .post({ path: '/api/security/role/_query', + access: 'public', + summary: `Query roles`, options: { - summary: `Query roles`, - access: 'public', tags: ['oas-tags:roles'], }, - security: { - authz: { - enabled: false, - reason: `This route delegates authorization to Core's scoped ES cluster client`, + }) + .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.', + }, + }, }, }, - validate: { - 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 })), - }) - ), - }), - }, - }, - 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; + createLicensedRouteHandler(async (context, request, response) => { + try { + const esClient = (await context.core).elasticsearch.client; + const features = await getFeatures(); - let showReservedRoles = filters?.showReservedRoles; + const { query, size, from, sort, filters } = request.body; - if (buildFlavor === 'serverless') { - showReservedRoles = false; - } + let showReservedRoles = filters?.showReservedRoles; - const queryPayload: { - bool: { - must: QueryClause[]; - should: QueryClause[]; - must_not: QueryClause[]; - minimum_should_match?: number; - }; - } = { bool: { must: [], should: [], must_not: [] } }; + if (buildFlavor === 'serverless') { + showReservedRoles = false; + } - const nonReservedRolesQuery = [ - { + const queryPayload: { bool: { - must_not: { - exists: { - field: 'metadata._reserved', + 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; + ]; + queryPayload.bool.should.push(...nonReservedRolesQuery); + queryPayload.bool.minimum_should_match = 1; - if (query) { - queryPayload.bool.must.push({ - wildcard: { - name: { - value: `*${query}*`, + if (query) { + queryPayload.bool.must.push({ + wildcard: { + name: { + value: `*${query}*`, + }, }, - }, - }); - } + }); + } - if (showReservedRoles) { - queryPayload.bool.should.push({ term: { 'metadata._reserved': true } }); - } + if (showReservedRoles) { + queryPayload.bool.should.push({ term: { 'metadata._reserved': true } }); + } - const transformedSort = sort && [{ [sort.field]: { order: sort.direction } }]; + const transformedSort = sort && [{ [sort.field]: { order: sort.direction } }]; - const queryRoles = await esClient.asCurrentUser.security.queryRole({ - query: queryPayload, - from, - size, - sort: transformedSort, - }); + 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, - }) - ); + 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({ - body: { - roles: transformedRoles, - count: queryRoles.count, - total: queryRoles.total, - }, - }); - } catch (error) { - return response.customError(wrapIntoCustomErrorResponse(error)); - } - }) - ); + return response.ok({ + body: { + roles: transformedRoles, + count: queryRoles.count, + total: queryRoles.total, + }, + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); } From eb736dff1076e0e70d75305b6483438f8c5ad296 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:24:08 +0000 Subject: [PATCH 42/45] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 6 +++++- oas_docs/bundle.serverless.json | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 5bf92234a7db1..eafa8288be1fa 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -39250,7 +39250,11 @@ } } }, - "responses": {}, + "responses": { + "200": { + "description": "Indicates a successful call." + } + }, "summary": "Query roles", "tags": [] } diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index d864d04379507..f646c16f18d31 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -39250,7 +39250,11 @@ } } }, - "responses": {}, + "responses": { + "200": { + "description": "Indicates a successful call." + } + }, "summary": "Query roles", "tags": [] } From 39e51d118a2d01967920f738b9f7a7839005562c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:38:52 +0000 Subject: [PATCH 43/45] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 4 +++- oas_docs/output/kibana.yaml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index f3e84ee78049a..6042d45db0715 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -34475,7 +34475,9 @@ paths: required: - field - direction - responses: {} + responses: + '200': + description: Indicates a successful call. summary: Query roles tags: [] x-beta: true diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index a0df8edbbd9b6..2a2e10b704020 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -37036,7 +37036,9 @@ paths: required: - field - direction - responses: {} + responses: + '200': + description: Indicates a successful call. summary: Query roles tags: [] /api/security/role/{name}: From 31cf053062aac62050a741d32ec9032c9ac23bac Mon Sep 17 00:00:00 2001 From: Sid Date: Wed, 15 Jan 2025 07:25:02 +0100 Subject: [PATCH 44/45] remove sortable status column --- .../public/management/roles/roles_grid/roles_grid_page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/platform/plugins/shared/security/public/management/roles/roles_grid/roles_grid_page.tsx index adbc19baffd82..beac90bb8beaa 100644 --- a/x-pack/platform/plugins/shared/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/platform/plugins/shared/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -309,7 +309,7 @@ export const RolesGridPage: FC = ({ 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), }); } From d3c76873d4007695eed63a9627b40663db312bd6 Mon Sep 17 00:00:00 2001 From: Sid Date: Wed, 15 Jan 2025 14:51:22 +0100 Subject: [PATCH 45/45] change wildcard to case insensitive --- .../shared/security/server/routes/authorization/roles/query.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts index b1d0373387c03..684e69634735b 100644 --- a/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts +++ b/x-pack/platform/plugins/shared/security/server/routes/authorization/roles/query.ts @@ -110,6 +110,7 @@ export function defineQueryRolesRoutes({ wildcard: { name: { value: `*${query}*`, + case_insensitive: true, }, }, });