diff --git a/x-pack/test/api_integration/apis/transform/index.ts b/x-pack/test/api_integration/apis/transform/index.ts index c665665b527d3..ad44dc1249e8e 100644 --- a/x-pack/test/api_integration/apis/transform/index.ts +++ b/x-pack/test/api_integration/apis/transform/index.ts @@ -29,6 +29,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); loadTestFile(require.resolve('./delete_transforms')); + loadTestFile(require.resolve('./reauthorize_transforms')); loadTestFile(require.resolve('./reset_transforms')); loadTestFile(require.resolve('./start_transforms')); loadTestFile(require.resolve('./stop_transforms')); diff --git a/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts b/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts new file mode 100644 index 0000000000000..9520f02a03280 --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts @@ -0,0 +1,285 @@ +/* + * 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 { ReauthorizeTransformsRequestSchema } from '@kbn/transform-plugin/common/api_schemas/reauthorize_transforms'; +import expect from '@kbn/expect'; +import { TRANSFORM_STATE } from '@kbn/transform-plugin/common/constants'; +import type { SecurityCreateApiKeyResponse } from '@elastic/elasticsearch/lib/api/types'; +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { asyncForEach, generateDestIndex, generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + // If transform was created with sufficient -> should still authorize and start + // If transform was created with insufficient -> should still authorize and start + + function getTransformIdByUser(username: USER) { + return `transform-by-${username}`; + } + + function generateHeaders(apiKey: SecurityCreateApiKeyResponse) { + return { + ...COMMON_REQUEST_HEADERS, + 'es-secondary-authorization': `ApiKey ${apiKey.encoded}`, + }; + } + + async function createTransform(transformId: string, headers: object) { + const config = generateTransformConfig(transformId, true); + await transform.api.createTransformWithHeaders(transformId, config, headers, true); + } + + async function cleanUpTransform(transformId: string) { + const destinationIndex = generateDestIndex(transformId); + + await transform.api.stopTransform(transformId); + await transform.api.cleanTransformIndices(); + await transform.api.deleteIndices(destinationIndex); + } + + // @todo: mark this back from only + describe.only('/api/transform/reauthorize_transforms', function () { + const apiKeysForTransformUsers = new Map(); + + async function expectUnauthorizedTransform(transformId: string, createdByUser: USER) { + const user = createdByUser; + const { body: getTransformBody } = await transform.api.getTransform(transformId); + const transformInfo = getTransformBody.transforms[0]; + const expectedApiKeyId = apiKeysForTransformUsers?.get(user)?.id; + expect(transformInfo.authorization.api_key.id).to.eql( + expectedApiKeyId, + `Expected authorization api_key for ${transformId} to be ${expectedApiKeyId} (got ${JSON.stringify( + transformInfo.authorization.api_key + )})` + ); + + const stats = await transform.api.getTransformStats(transformId); + expect(stats.state).to.eql( + TRANSFORM_STATE.STOPPED, + `Expected transform state of ${transformId} to be '${TRANSFORM_STATE.STOPPED}' (got ${stats.state})` + ); + expect(stats.health.status).to.eql( + 'red', + `Expected transform health status of ${transformId} to be 'red' (got ${stats.health.status})` + ); + expect(stats.health.issues![0].type).to.eql( + 'privileges_check_failed', + `Expected transform health issue of ${transformId} to be 'privileges_check_failed' (got ${stats.health.status})` + ); + } + + async function expectAuthorizedTransform( + transformId: string, + createdByUser: USER, + expectedApiKey?: SecurityCreateApiKeyResponse + ) { + const { body: getTransformBody } = await transform.api.getTransform(transformId); + const transformInfo = getTransformBody.transforms[0]; + + if (expectedApiKey) { + const expectedApiKeyId = expectedApiKey?.id; + + expect(transformInfo.authorization.api_key.id).to.eql( + expectedApiKeyId, + `Expected authorization api_key for ${transformId} to be ${expectedApiKeyId} (got ${JSON.stringify( + transformInfo.authorization.api_key + )})` + ); + } else { + const expectedApiKeyId = apiKeysForTransformUsers?.get(createdByUser)?.id; + expect(transformInfo.authorization.api_key.id).to.not.eql( + expectedApiKeyId, + `Expected authorization api_key for ${transformId} to not be ${expectedApiKeyId} (got ${JSON.stringify( + transformInfo.authorization.api_key + )})` + ); + } + const stats = await transform.api.getTransformStats(transformId); + expect(stats.health.status).to.eql( + 'green', + `Expected transform health status of ${transformId} to be 'green' (got ${stats.health.status})` + ); + } + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + + const apiKeyForTransformUsers = + await transform.securityCommon.createApiKeyForTransformUsers(); + + apiKeyForTransformUsers.forEach(({ user, apiKey }) => + apiKeysForTransformUsers.set(user.name as USER, apiKey) + ); + }); + + after(async () => { + await transform.securityCommon.clearAllTransformApiKeys(); + }); + + describe('single transform reauthorize_transforms', function () { + const transformCreatedByViewerId = getTransformIdByUser(USER.TRANSFORM_VIEWER); + + beforeEach(async () => { + await createTransform( + transformCreatedByViewerId, + generateHeaders(apiKeysForTransformUsers.get(USER.TRANSFORM_VIEWER)!) + ); + }); + afterEach(async () => { + await cleanUpTransform(transformCreatedByViewerId); + }); + + it('should not reauthorize transform created by transform_viewer for transform_unauthorized', async () => { + const reqBody: ReauthorizeTransformsRequestSchema = [{ id: transformCreatedByViewerId }]; + const { body, status } = await supertest + .post(`/api/transform/reauthorize_transforms`) + .auth( + USER.TRANSFORM_UNAUTHORIZED, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_UNAUTHORIZED) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody); + transform.api.assertResponseStatusCode(200, status, body); + expect(body[transformCreatedByViewerId].success).to.eql( + false, + `Expected ${transformCreatedByViewerId} not to be authorized` + ); + expect(typeof body[transformCreatedByViewerId].error).to.be('object'); + + await expectUnauthorizedTransform(transformCreatedByViewerId, USER.TRANSFORM_VIEWER); + }); + + it('should not reauthorize transform created by transform_viewer for transform_viewer', async () => { + const reqBody: ReauthorizeTransformsRequestSchema = [{ id: transformCreatedByViewerId }]; + const { body, status } = await supertest + .post(`/api/transform/reauthorize_transforms`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody); + transform.api.assertResponseStatusCode(200, status, body); + expect(body[transformCreatedByViewerId].success).to.eql( + false, + `Expected ${transformCreatedByViewerId} not to be rauthorized` + ); + expect(typeof body[transformCreatedByViewerId].error).to.be('object'); + + await expectUnauthorizedTransform(transformCreatedByViewerId, USER.TRANSFORM_VIEWER); + }); + + it('should reauthorize transform created by transform_viewer with new api key of poweruser and start the transform', async () => { + const reqBody: ReauthorizeTransformsRequestSchema = [{ id: transformCreatedByViewerId }]; + const { body, status } = await supertest + .post(`/api/transform/reauthorize_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody); + transform.api.assertResponseStatusCode(200, status, body); + expect(body[transformCreatedByViewerId].success).to.eql( + true, + `Expected ${transformCreatedByViewerId} to be reauthorized` + ); + expect(typeof body[transformCreatedByViewerId].error).to.eql( + 'undefined', + `Expected ${transformCreatedByViewerId} to be reauthorized without error` + ); + await transform.api.waitForTransformState( + transformCreatedByViewerId, + TRANSFORM_STATE.STARTED + ); + + await expectAuthorizedTransform(transformCreatedByViewerId, USER.TRANSFORM_VIEWER); + }); + }); + + describe('single transform reauthorize_transforms with invalid transformId', function () { + it('should return 200 with error in response if invalid transformId', async () => { + const reqBody: ReauthorizeTransformsRequestSchema = [{ id: 'invalid_transform_id' }]; + const { body, status } = await supertest + .post(`/api/transform/reauthorize_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody); + transform.api.assertResponseStatusCode(200, status, body); + + expect(body.invalid_transform_id.success).to.eql(false); + expect(body.invalid_transform_id).to.have.property('error'); + }); + }); + + describe('bulk reauthorize_transforms', function () { + const reqBody: ReauthorizeTransformsRequestSchema = [ + USER.TRANSFORM_VIEWER, + USER.TRANSFORM_POWERUSER, + ].map((user) => ({ id: getTransformIdByUser(user) })); + const destinationIndices = reqBody.map((d) => generateDestIndex(d.id)); + + beforeEach(async () => { + await Promise.all( + [USER.TRANSFORM_VIEWER, USER.TRANSFORM_POWERUSER].map((user) => + createTransform( + getTransformIdByUser(user), + generateHeaders(apiKeysForTransformUsers.get(user)!) + ) + ) + ); + }); + + afterEach(async () => { + await asyncForEach(reqBody, async ({ id }: { id: string }, idx: number) => { + await transform.api.stopTransform(id); + }); + await transform.api.cleanTransformIndices(); + await asyncForEach(destinationIndices, async (destinationIndex: string) => { + await transform.api.deleteIndices(destinationIndex); + }); + }); + + it('should reauthorize multiple transforms for transform_poweruser, even if one of the transformIds is invalid', async () => { + const invalidTransformId = 'invalid_transform_id'; + + const { body, status } = await supertest + .post(`/api/transform/reauthorize_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send([...reqBody, { id: invalidTransformId }]); + transform.api.assertResponseStatusCode(200, status, body); + + await expectAuthorizedTransform( + getTransformIdByUser(USER.TRANSFORM_VIEWER), + USER.TRANSFORM_VIEWER + ); + await expectAuthorizedTransform( + getTransformIdByUser(USER.TRANSFORM_POWERUSER), + USER.TRANSFORM_POWERUSER + ); + + expect(body[invalidTransformId].success).to.eql(false); + expect(body[invalidTransformId]).to.have.property('error'); + }); + }); + }); +}; diff --git a/x-pack/test/functional/services/transform/api.ts b/x-pack/test/functional/services/transform/api.ts index dcb76bb23eaf0..20f6472d41ad0 100644 --- a/x-pack/test/functional/services/transform/api.ts +++ b/x-pack/test/functional/services/transform/api.ts @@ -232,6 +232,27 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { ); }, + async createTransformWithHeaders( + transformId: string, + transformConfig: PutTransformsRequestSchema, + headers: object, + deferValidation?: boolean + ) { + log.debug( + `Creating transform with id '${transformId}' with headers ${JSON.stringify(headers)}...` + ); + const { body, status } = await esSupertest + .put(`/_transform/${transformId}${deferValidation ? '?defer_validation=true' : ''}`) + .set(headers) + .send(transformConfig); + this.assertResponseStatusCode(200, status, body); + + await this.waitForTransformToExist( + transformId, + `expected transform '${transformId}' to be created` + ); + }, + async waitForTransformToExist(transformId: string, errorMsg?: string) { await retry.waitForWithTimeout(`'${transformId}' to exist`, 5 * 1000, async () => { if (await this.getTransform(transformId, 200)) { diff --git a/x-pack/test/functional/services/transform/security_common.ts b/x-pack/test/functional/services/transform/security_common.ts index 36670a65211b3..94c059be0fae6 100644 --- a/x-pack/test/functional/services/transform/security_common.ts +++ b/x-pack/test/functional/services/transform/security_common.ts @@ -7,6 +7,7 @@ import { ProvidedType } from '@kbn/test'; +import { Client } from '@elastic/elasticsearch'; import { FtrProviderContext } from '../../ftr_provider_context'; export type TransformSecurityCommon = ProvidedType; @@ -19,6 +20,7 @@ export enum USER { export function TransformSecurityCommonProvider({ getService }: FtrProviderContext) { const security = getService('security'); + const esClient: Client = getService('es'); const roles = [ { @@ -98,6 +100,51 @@ export function TransformSecurityCommonProvider({ getService }: FtrProviderConte } }, + async createApiKeyForTransformUser(username: string) { + const user = users.find((u) => u.name === username); + + if (user === undefined) { + throw new Error(`Can't create api key for user ${user} - user not defined`); + } + + const roleDescriptors = user.roles.reduce>((map, roleName) => { + const userRole = roles.find((r) => r.name === roleName); + + if (userRole) { + map[roleName] = userRole.elasticsearch; + } + return map; + }, {}); + const apiKey = await esClient.security.createApiKey({ + body: { + name: `Transform API Key ${user.full_name}`, + role_descriptors: roleDescriptors, + metadata: user, + }, + }); + return { user: { name: user.name as USER, roles: user.roles }, apiKey }; + }, + + async createApiKeyForTransformUsers() { + const apiKeyForTransformUsers = await Promise.all( + users.map((user) => this.createApiKeyForTransformUser(user.name)) + ); + + return apiKeyForTransformUsers; + }, + + async clearAllTransformApiKeys() { + const existingKeys = await esClient.security.queryApiKeys(); + + if (existingKeys.count > 0) { + await Promise.all( + existingKeys.api_keys.map(async (key) => { + esClient.security.invalidateApiKey({ ids: [key.id] }); + }) + ); + } + }, + async cleanTransformRoles() { for (const role of roles) { await security.role.delete(role.name);