Skip to content

Commit

Permalink
Add new reauthorize transforms tests
Browse files Browse the repository at this point in the history
  • Loading branch information
qn895 committed Apr 17, 2023
1 parent 3f64b1f commit e8d16e7
Show file tree
Hide file tree
Showing 4 changed files with 354 additions and 0 deletions.
1 change: 1 addition & 0 deletions x-pack/test/api_integration/apis/transform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
285 changes: 285 additions & 0 deletions x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts
Original file line number Diff line number Diff line change
@@ -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<USER, SecurityCreateApiKeyResponse>();

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');
});
});
});
};
21 changes: 21 additions & 0 deletions x-pack/test/functional/services/transform/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
47 changes: 47 additions & 0 deletions x-pack/test/functional/services/transform/security_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { ProvidedType } from '@kbn/test';

import { Client } from '@elastic/elasticsearch';
import { FtrProviderContext } from '../../ftr_provider_context';

export type TransformSecurityCommon = ProvidedType<typeof TransformSecurityCommonProvider>;
Expand All @@ -19,6 +20,7 @@ export enum USER {

export function TransformSecurityCommonProvider({ getService }: FtrProviderContext) {
const security = getService('security');
const esClient: Client = getService('es');

const roles = [
{
Expand Down Expand Up @@ -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<Record<string, object>>((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);
Expand Down

0 comments on commit e8d16e7

Please sign in to comment.