diff --git a/x-pack/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts index 423b2d001381c..84e43f1f632a1 100644 --- a/x-pack/plugins/transform/common/constants.ts +++ b/x-pack/plugins/transform/common/constants.ts @@ -59,6 +59,9 @@ export const APP_CLUSTER_PRIVILEGES = [ 'cluster:admin/transform/stop', ]; +// Minimum privileges required to return transform node count +export const NODES_INFO_PRIVILEGES = ['cluster:monitor/transform/get']; + // Equivalent of capabilities.canGetTransform export const APP_GET_TRANSFORM_CLUSTER_PRIVILEGES = [ 'cluster.cluster:monitor/transform/get', diff --git a/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts b/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts index c9a0795c32210..29a3c50b2eea9 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts @@ -5,6 +5,9 @@ * 2.0. */ +import Boom from '@hapi/boom'; + +import { NODES_INFO_PRIVILEGES } from '../../../common/constants'; import { isPopulatedObject } from '../../../common/shared_imports'; import { RouteDependencies } from '../../types'; @@ -44,6 +47,22 @@ export function registerTransformNodesRoutes({ router, license }: RouteDependenc }, license.guardApiRoute(async (ctx, req, res) => { try { + // If security is enabled, check that the user has at least permission to + // view transforms before calling the _nodes endpoint with the internal user. + if (license.getStatus().isSecurityEnabled === true) { + const { + body: { has_all_requested: hasAllPrivileges }, + } = await ctx.core.elasticsearch.client.asCurrentUser.security.hasPrivileges({ + body: { + cluster: NODES_INFO_PRIVILEGES, + }, + }); + + if (!hasAllPrivileges) { + return res.customError(wrapError(new Boom.Boom('Forbidden', { statusCode: 403 }))); + } + } + const { body: { nodes }, } = await ctx.core.elasticsearch.client.asInternalUser.nodes.info({ diff --git a/x-pack/test/api_integration/apis/transform/transforms_nodes.ts b/x-pack/test/api_integration/apis/transform/transforms_nodes.ts index ca9ab8e8a728d..0fc93289195d9 100644 --- a/x-pack/test/api_integration/apis/transform/transforms_nodes.ts +++ b/x-pack/test/api_integration/apis/transform/transforms_nodes.ts @@ -31,7 +31,7 @@ export default ({ getService }: FtrProviderContext) => { } describe('/api/transform/transforms/_nodes', function () { - it('should return the number of available transform nodes', async () => { + it('should return the number of available transform nodes for a power user', async () => { const { body } = await supertest .get('/api/transform/transforms/_nodes') .auth( @@ -44,5 +44,31 @@ export default ({ getService }: FtrProviderContext) => { assertTransformsNodesResponseBody(body); }); + + it('should return the number of available transform nodes for a viewer user', async () => { + const { body } = await supertest + .get('/api/transform/transforms/_nodes') + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertTransformsNodesResponseBody(body); + }); + + it('should not return the number of available transform nodes for an unauthorized user', async () => { + await supertest + .get('/api/transform/transforms/_nodes') + .auth( + USER.TRANSFORM_UNAUTHORIZED, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_UNAUTHORIZED) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(403); + }); }); }; diff --git a/x-pack/test/functional/services/transform/security_common.ts b/x-pack/test/functional/services/transform/security_common.ts index bae31dffa1412..f27de80d26b2e 100644 --- a/x-pack/test/functional/services/transform/security_common.ts +++ b/x-pack/test/functional/services/transform/security_common.ts @@ -14,6 +14,7 @@ export type TransformSecurityCommon = ProvidedType