From 1464f6648b9d393a6838b3315a543c9836c1a2bf Mon Sep 17 00:00:00 2001 From: Elena Shostak Date: Fri, 27 Dec 2024 13:54:52 +0100 Subject: [PATCH] wip --- .../common/suites/create.agnostic.ts | 27 +-- .../common/suites/create.ts | 198 ----------------- .../common/suites/delete.agnostic.ts | 30 +-- .../common/suites/get.agnostic.ts | 7 +- .../common/suites/get.ts | 149 ------------- .../common/suites/get_all.agnostic.ts | 15 +- .../common/suites/get_all.ts | 204 ------------------ ...esolve_copy_to_space_conflicts.agnostic.ts | 9 +- .../common/suites/update.agnostic.ts | 12 +- .../ftr_provider_context.d.ts | 2 +- .../security_and_spaces/apis/index.ts | 10 +- .../services/basic_auth_supertest.ts | 54 +++++ .../deployment_agnostic/services/index.ts | 4 + .../services/role_scoped_supertest.ts | 174 +++++++++++++++ 14 files changed, 268 insertions(+), 627 deletions(-) delete mode 100644 x-pack/test/spaces_api_integration/common/suites/create.ts delete mode 100644 x-pack/test/spaces_api_integration/common/suites/get.ts delete mode 100644 x-pack/test/spaces_api_integration/common/suites/get_all.ts create mode 100644 x-pack/test/spaces_api_integration/deployment_agnostic/services/basic_auth_supertest.ts create mode 100644 x-pack/test/spaces_api_integration/deployment_agnostic/services/role_scoped_supertest.ts diff --git a/x-pack/test/spaces_api_integration/common/suites/create.agnostic.ts b/x-pack/test/spaces_api_integration/common/suites/create.agnostic.ts index 9e300b40eecd1..745161176a48b 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.agnostic.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.agnostic.ts @@ -5,15 +5,12 @@ * 2.0. */ -import type { Agent as SuperTestAgent } from 'supertest'; - import expect from '@kbn/expect/expect'; import type { DeploymentAgnosticFtrProviderContext, SupertestWithRoleScopeType, } from '../../deployment_agnostic/ftr_provider_context'; -import { getRoleDefinitionForUser, isBuiltInRole } from '../lib/authentication'; import { getTestScenariosForSpace } from '../lib/space_test_utils'; import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; @@ -38,8 +35,6 @@ interface CreateTestDefinition { export function createTestSuiteFactory({ getService }: DeploymentAgnosticFtrProviderContext) { const esArchiver = getService('esArchiver'); const roleScopedSupertest = getService('roleScopedSupertest'); - const samlAuth = getService('samlAuth'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); const expectConflictResponse = (resp: { [key: string]: any }) => { expect(resp.body).to.only.have.keys(['error', 'message', 'statusCode']); @@ -110,30 +105,14 @@ export function createTestSuiteFactory({ getService }: DeploymentAgnosticFtrProv (describeFn: DescribeFn) => (description: string, { user, spaceId, tests }: CreateTestDefinition) => { describeFn(description, () => { - let supertest: SupertestWithRoleScopeType | SuperTestAgent; + let supertest: SupertestWithRoleScopeType; before(async () => { - if (user) { - const isBuiltIn = isBuiltInRole(user.role); - if (!isBuiltIn) { - await samlAuth.setCustomRole(getRoleDefinitionForUser(user)); - } - supertest = await roleScopedSupertest.getSupertestWithRoleScope( - isBuiltIn ? user.role : 'customRole', - { - useCookieHeader: true, - withInternalHeaders: true, - } - ); - } else { - supertest = supertestWithoutAuth; - } + supertest = await roleScopedSupertest.getSupertestWithRoleScope(user!); }); after(async () => { - if (user) { - await (supertest as SupertestWithRoleScopeType).destroy(); - } + await supertest.destroy(); }); beforeEach(() => diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts deleted file mode 100644 index fc6aa2b852ca7..0000000000000 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* - * 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 type { SuperTest } from 'supertest'; - -import expect from '@kbn/expect'; - -import { getTestScenariosForSpace } from '../lib/space_test_utils'; -import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface CreateTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface CreateTests { - newSpace: CreateTest; - alreadyExists: CreateTest; - reservedSpecified: CreateTest; - solutionSpecified: CreateTest; -} - -interface CreateTestDefinition { - user: TestDefinitionAuthentication; - spaceId: string; - tests: CreateTests; -} - -export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const expectConflictResponse = (resp: { [key: string]: any }) => { - expect(resp.body).to.only.have.keys(['error', 'message', 'statusCode']); - expect(resp.body.error).to.equal('Conflict'); - expect(resp.body.statusCode).to.equal(409); - expect(resp.body.message).to.match(new RegExp(`A space with the identifier .*`)); - }; - - const expectNewSpaceResult = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - name: 'marketing', - id: 'marketing', - description: 'a description', - color: '#5c5959', - disabledFeatures: [], - }); - }; - - const expectRbacForbiddenResponse = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: 'Unauthorized to create spaces', - }); - }; - - const expectReservedSpecifiedResult = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - name: 'reserved space', - id: 'reserved', - description: 'a description', - color: '#5c5959', - disabledFeatures: [], - }); - }; - - const expectSolutionSpecifiedResult = (resp: Record) => { - const disabledFeatures = resp.body.disabledFeatures.sort(); - - const expected = { - id: 'solution', - name: 'space with solution', - description: 'a description', - color: '#5c5959', - disabledFeatures: [ - // Disabled features are automatically added to the space when a solution is set - 'apm', - 'infrastructure', - 'inventory', - 'logs', - 'observabilityCases', - 'observabilityCasesV2', - 'securitySolutionAssistant', - 'securitySolutionAttackDiscovery', - 'securitySolutionCases', - 'securitySolutionCasesV2', - 'siem', - 'slo', - 'uptime', - ], - solution: 'es', - }; - - expect({ ...resp.body, disabledFeatures }).to.eql(expected); - }; - - const makeCreateTest = - (describeFn: DescribeFn) => - (description: string, { user, spaceId, tests }: CreateTestDefinition) => { - describeFn(description, () => { - beforeEach(() => - esArchiver.load( - 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' - ) - ); - afterEach(() => - esArchiver.unload( - 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' - ) - ); - - getTestScenariosForSpace(spaceId).forEach(({ urlPrefix, scenario }) => { - it(`should return ${tests.newSpace.statusCode} ${scenario}`, async () => { - return supertest - .post(`${urlPrefix}/api/spaces/space`) - .auth(user.username, user.password) - .send({ - name: 'marketing', - id: 'marketing', - description: 'a description', - color: '#5c5959', - disabledFeatures: [], - }) - .expect(tests.newSpace.statusCode) - .then(tests.newSpace.response); - }); - - describe('when it already exists', () => { - it(`should return ${tests.alreadyExists.statusCode} ${scenario}`, async () => { - return supertest - .post(`${urlPrefix}/api/spaces/space`) - .auth(user.username, user.password) - .send({ - name: 'space_1', - id: 'space_1', - color: '#ffffff', - description: 'a description', - disabledFeatures: [], - }) - .expect(tests.alreadyExists.statusCode) - .then(tests.alreadyExists.response); - }); - }); - - describe('when _reserved is specified', () => { - it(`should return ${tests.reservedSpecified.statusCode} and ignore _reserved ${scenario}`, async () => { - return supertest - .post(`${urlPrefix}/api/spaces/space`) - .auth(user.username, user.password) - .send({ - name: 'reserved space', - id: 'reserved', - description: 'a description', - color: '#5c5959', - _reserved: true, - disabledFeatures: [], - }) - .expect(tests.reservedSpecified.statusCode) - .then(tests.reservedSpecified.response); - }); - }); - - describe('when solution is specified', () => { - it(`should return ${tests.solutionSpecified.statusCode}`, async () => { - return supertest - .post(`${urlPrefix}/api/spaces/space`) - .auth(user.username, user.password) - .send({ - name: 'space with solution', - id: 'solution', - description: 'a description', - color: '#5c5959', - solution: 'es', - disabledFeatures: [], - }) - .expect(tests.solutionSpecified.statusCode) - .then(tests.solutionSpecified.response); - }); - }); - }); - }); - }; - - const createTest = makeCreateTest(describe); - // @ts-ignore - createTest.only = makeCreateTest(describe.only); - - return { - createTest, - expectConflictResponse, - expectNewSpaceResult, - expectRbacForbiddenResponse, - expectReservedSpecifiedResult, - expectSolutionSpecifiedResult, - }; -} diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.agnostic.ts b/x-pack/test/spaces_api_integration/common/suites/delete.agnostic.ts index 87fbfa5722de4..4f591742fbb2a 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.agnostic.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.agnostic.ts @@ -4,16 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Agent as SuperTestAgent } from 'supertest'; import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; -import expect from '@kbn/expect'; +import expect from '@kbn/expect/expect'; import type { DeploymentAgnosticFtrProviderContext, SupertestWithRoleScopeType, } from '../../deployment_agnostic/ftr_provider_context'; -import { getRoleDefinitionForUser, isBuiltInRole } from '../lib/authentication'; import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { getAggregatedSpaceData, getTestScenariosForSpace } from '../lib/space_test_utils'; import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; @@ -36,11 +34,9 @@ interface DeleteTestDefinition { } export function deleteTestSuiteFactory({ getService }: DeploymentAgnosticFtrProviderContext) { - const roleScopedSupertest = getService('roleScopedSupertest'); - const samlAuth = getService('samlAuth'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('es'); + const roleScopedSupertest = getService('roleScopedSupertest'); const createExpectResult = (expectedResult: any) => (resp: { [key: string]: any }) => { expect(resp.body).to.eql(expectedResult); @@ -206,29 +202,13 @@ export function deleteTestSuiteFactory({ getService }: DeploymentAgnosticFtrProv (describeFn: DescribeFn) => (description: string, { user, spaceId, tests }: DeleteTestDefinition) => { describeFn(description, () => { - let supertest: SupertestWithRoleScopeType | SuperTestAgent; + let supertest: SupertestWithRoleScopeType; before(async () => { - if (user) { - const isBuiltIn = isBuiltInRole(user.role); - if (!isBuiltIn) { - await samlAuth.setCustomRole(getRoleDefinitionForUser(user)); - } - supertest = await roleScopedSupertest.getSupertestWithRoleScope( - isBuiltIn ? user.role : 'customRole', - { - useCookieHeader: true, - withInternalHeaders: true, - } - ); - } else { - supertest = supertestWithoutAuth; - } + supertest = await roleScopedSupertest.getSupertestWithRoleScope(user!); }); after(async () => { - if (user) { - await (supertest as SupertestWithRoleScopeType).destroy(); - } + await supertest.destroy(); }); beforeEach(async () => { diff --git a/x-pack/test/spaces_api_integration/common/suites/get.agnostic.ts b/x-pack/test/spaces_api_integration/common/suites/get.agnostic.ts index a0a5f1eeb553a..3f7f3c3836b4e 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get.agnostic.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get.agnostic.ts @@ -117,16 +117,17 @@ export function getTestSuiteFactory(context: DeploymentAgnosticFtrProviderContex (describeFn: DescribeFn) => (description: string, { user, currentSpaceId, spaceId, tests }: GetTestDefinition) => { describeFn(description, () => { - let supertest: SupertestWithRoleScopeType | SuperTestAgent; + const roleScopedSupertest = context.getService('roleScopedSupertest'); + let supertest: SupertestWithRoleScopeType; before(async () => { - supertest = await getSupertest(context, user); + supertest = await roleScopedSupertest.getSupertestWithRoleScope(user!); await esArchiver.load( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' ); }); after(async () => { - await maybeDestroySupertest(supertest); + await supertest.destroy(); await esArchiver.unload( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' ); diff --git a/x-pack/test/spaces_api_integration/common/suites/get.ts b/x-pack/test/spaces_api_integration/common/suites/get.ts deleted file mode 100644 index 2bdfcad8658fc..0000000000000 --- a/x-pack/test/spaces_api_integration/common/suites/get.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 type { SuperAgent } from 'superagent'; - -import expect from '@kbn/expect'; - -import { getTestScenariosForSpace } from '../lib/space_test_utils'; -import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface GetTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface GetTests { - default: GetTest; -} - -interface GetTestDefinition { - user?: TestDefinitionAuthentication; - currentSpaceId: string; - spaceId: string; - tests: GetTests; -} - -const nonExistantSpaceId = 'not-a-space'; - -export function getTestSuiteFactory(esArchiver: any, supertest: SuperAgent) { - const createExpectEmptyResult = () => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql(''); - }; - - const createExpectNotFoundResult = () => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Not Found', - message: 'Not Found', - statusCode: 404, - }); - }; - - const createExpectRbacForbidden = (spaceId: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unauthorized to get ${spaceId} space`, - }); - }; - - const createExpectResults = (spaceId: string) => (resp: { [key: string]: any }) => { - const allSpaces = [ - { - id: 'default', - name: 'Default', - color: '#00bfb3', - description: 'This is your default space!', - _reserved: true, - disabledFeatures: [], - }, - { - id: 'space_1', - name: 'Space 1', - description: 'This is the first test space', - disabledFeatures: [], - }, - { - id: 'space_2', - name: 'Space 2', - description: 'This is the second test space', - disabledFeatures: [], - }, - { - id: 'space_3', - name: 'Space 3', - description: 'This is the third test space', - solution: 'es', - disabledFeatures: [ - // Disabled features are automatically added to the space when a solution is set - 'apm', - 'infrastructure', - 'inventory', - 'logs', - 'observabilityCases', - 'observabilityCasesV2', - 'securitySolutionAssistant', - 'securitySolutionAttackDiscovery', - 'securitySolutionCases', - 'securitySolutionCasesV2', - 'siem', - 'slo', - 'uptime', - ], - }, - ]; - - const disabledFeatures = (resp.body.disabledFeatures ?? []).sort(); - - const expectedSpace = allSpaces.find((space) => space.id === spaceId); - if (expectedSpace) { - expectedSpace.disabledFeatures.sort(); - } - - expect({ ...resp.body, disabledFeatures }).to.eql(expectedSpace); - }; - - const makeGetTest = - (describeFn: DescribeFn) => - (description: string, { user = {}, currentSpaceId, spaceId, tests }: GetTestDefinition) => { - describeFn(description, () => { - before(() => - esArchiver.load( - 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' - ) - ); - after(() => - esArchiver.unload( - 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' - ) - ); - - getTestScenariosForSpace(currentSpaceId).forEach(({ urlPrefix, scenario }) => { - it(`should return ${tests.default.statusCode} ${scenario}`, async () => { - return supertest - .get(`${urlPrefix}/api/spaces/space/${spaceId}`) - .auth(user.username, user.password) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - }); - }); - }; - - const getTest = makeGetTest(describe); - // @ts-ignore - getTest.only = makeGetTest(describe); - - return { - createExpectResults, - createExpectRbacForbidden, - createExpectEmptyResult, - createExpectNotFoundResult, - getTest, - nonExistantSpaceId, - }; -} diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.agnostic.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.agnostic.ts index 2e833b492289c..a07f1459aa0e6 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.agnostic.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.agnostic.ts @@ -5,11 +5,8 @@ * 2.0. */ -import type { Agent as SuperTestAgent } from 'supertest'; +import expect from '@kbn/expect/expect'; -import expect from '@kbn/expect'; - -import { getSupertest, maybeDestroySupertest } from './common'; import type { DeploymentAgnosticFtrProviderContext, SupertestWithRoleScopeType, @@ -84,9 +81,11 @@ const ALL_SPACE_RESULTS: Space[] = [ 'inventory', 'logs', 'observabilityCases', + 'observabilityCasesV2', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', 'securitySolutionCases', + 'securitySolutionCasesV2', 'siem', 'slo', 'uptime', @@ -146,9 +145,10 @@ export function getAllTestSuiteFactory(context: DeploymentAgnosticFtrProviderCon (describeFn: DescribeFn) => (description: string, { user, spaceId, tests }: GetAllTestDefinition) => { describeFn(description, () => { - let supertest: SupertestWithRoleScopeType | SuperTestAgent; + const roleScopedSupertest = context.getService('roleScopedSupertest'); + let supertest: SupertestWithRoleScopeType; before(async () => { - supertest = await getSupertest(context, user); + supertest = await roleScopedSupertest.getSupertestWithRoleScope(user!); await esArchiver.load( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' ); @@ -157,7 +157,8 @@ export function getAllTestSuiteFactory(context: DeploymentAgnosticFtrProviderCon await esArchiver.unload( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' ); - await maybeDestroySupertest(supertest); + + await supertest.destroy(); }); getTestScenariosForSpace(spaceId).forEach(({ scenario, urlPrefix }) => { diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts deleted file mode 100644 index 8de073c0d2abd..0000000000000 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ /dev/null @@ -1,204 +0,0 @@ -/* - * 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 type { SuperTest } from 'supertest'; - -import expect from '@kbn/expect'; - -import { getTestScenariosForSpace } from '../lib/space_test_utils'; -import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface GetAllTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface GetAllTests { - exists: GetAllTest; - copySavedObjectsPurpose: GetAllTest; - shareSavedObjectsPurpose: GetAllTest; - includeAuthorizedPurposes: GetAllTest; -} - -interface GetAllTestDefinition { - user?: Omit; - spaceId: string; - tests: GetAllTests; -} - -interface AuthorizedPurposes { - any: boolean; - copySavedObjectsIntoSpace: boolean; - findSavedObjects: boolean; - shareSavedObjectsIntoSpace: boolean; -} - -interface Space { - id: string; - name: string; - color?: string; - description: string; - solution?: string; - _reserved?: boolean; - disabledFeatures: string[]; -} - -const ALL_SPACE_RESULTS: Space[] = [ - { - id: 'default', - name: 'Default', - color: '#00bfb3', - description: 'This is your default space!', - _reserved: true, - disabledFeatures: [], - }, - { - id: 'space_1', - name: 'Space 1', - description: 'This is the first test space', - disabledFeatures: [], - }, - { - id: 'space_2', - name: 'Space 2', - description: 'This is the second test space', - disabledFeatures: [], - }, - { - id: 'space_3', - name: 'Space 3', - description: 'This is the third test space', - solution: 'es', - disabledFeatures: [ - // Disabled features are automatically added to the space when a solution is set - 'apm', - 'infrastructure', - 'inventory', - 'logs', - 'observabilityCases', - 'observabilityCasesV2', - 'securitySolutionAssistant', - 'securitySolutionAttackDiscovery', - 'securitySolutionCases', - 'securitySolutionCasesV2', - 'siem', - 'slo', - 'uptime', - ], - }, -]; - -const sortDisabledFeatures = (space: Space) => { - return { - ...space, - disabledFeatures: [...space.disabledFeatures].sort(), - }; -}; - -export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectResults = - (...spaceIds: string[]) => - (resp: { [key: string]: any }) => { - const expectedBody = ALL_SPACE_RESULTS.filter((entry) => spaceIds.includes(entry.id)); - expect(resp.body.map(sortDisabledFeatures)).to.eql(expectedBody.map(sortDisabledFeatures)); - }; - - const createExpectAllPurposesResults = - (authorizedPurposes: AuthorizedPurposes, ...spaceIds: string[]) => - (resp: { [key: string]: any }) => { - const expectedBody = ALL_SPACE_RESULTS.filter((entry) => spaceIds.includes(entry.id)).map( - (x) => ({ ...x, authorizedPurposes }) - ); - expect(resp.body.map(sortDisabledFeatures)).to.eql(expectedBody.map(sortDisabledFeatures)); - }; - - const expectEmptyResult = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql(''); - }; - - const expectRbacForbidden = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Forbidden', - message: 'Forbidden', - statusCode: 403, - }); - }; - - const makeGetAllTest = - (describeFn: DescribeFn) => - (description: string, { user = {}, spaceId, tests }: GetAllTestDefinition) => { - describeFn(description, () => { - before(() => - esArchiver.load( - 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' - ) - ); - after(() => - esArchiver.unload( - 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' - ) - ); - - getTestScenariosForSpace(spaceId).forEach(({ scenario, urlPrefix }) => { - describe('undefined purpose', () => { - it(`should return ${tests.exists.statusCode} ${scenario}`, async () => { - return supertest - .get(`${urlPrefix}/api/spaces/space`) - .auth(user.username, user.password) - .expect(tests.exists.statusCode) - .then(tests.exists.response); - }); - }); - - describe('copySavedObjectsIntoSpace purpose', () => { - it(`should return ${tests.copySavedObjectsPurpose.statusCode} ${scenario}`, async () => { - return supertest - .get(`${urlPrefix}/api/spaces/space`) - .query({ purpose: 'copySavedObjectsIntoSpace' }) - .auth(user.username, user.password) - .expect(tests.copySavedObjectsPurpose.statusCode) - .then(tests.copySavedObjectsPurpose.response); - }); - }); - - describe('shareSavedObjectsIntoSpace purpose', () => { - it(`should return ${tests.shareSavedObjectsPurpose.statusCode} ${scenario}`, async () => { - return supertest - .get(`${urlPrefix}/api/spaces/space`) - .query({ purpose: 'shareSavedObjectsIntoSpace' }) - .auth(user.username, user.password) - .expect(tests.copySavedObjectsPurpose.statusCode) - .then(tests.copySavedObjectsPurpose.response); - }); - }); - - describe('include_authorized_purposes=true', () => { - it(`should return ${tests.includeAuthorizedPurposes.statusCode} ${scenario}`, async () => { - return supertest - .get(`${urlPrefix}/api/spaces/space`) - .query({ include_authorized_purposes: true }) - .auth(user.username, user.password) - .expect(tests.includeAuthorizedPurposes.statusCode) - .then(tests.includeAuthorizedPurposes.response); - }); - }); - }); - }); - }; - - const getAllTest = makeGetAllTest(describe); - // @ts-ignore - getAllTest.only = makeGetAllTest(describe.only); - - return { - createExpectResults, - createExpectAllPurposesResults, - expectRbacForbidden, - getAllTest, - expectEmptyResult, - }; -} diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.agnostic.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.agnostic.ts index 2958620d63a16..dae2959714e41 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.agnostic.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.agnostic.ts @@ -8,7 +8,7 @@ import type { Agent as SuperTestAgent } from 'supertest'; import type { SavedObject } from '@kbn/core/server'; -import expect from '@kbn/expect'; +import expect from '@kbn/expect/expect'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; import type { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces'; @@ -76,6 +76,7 @@ const getDestinationSpace = (originSpaceId?: string) => { export function resolveCopyToSpaceConflictsSuite(context: DeploymentAgnosticFtrProviderContext) { const testDataLoader = getTestDataLoader(context); const supertestWithAuth = context.getService('supertest'); + const roleScopedSupertest = context.getService('roleScopedSupertest'); const getVisualizationAtSpace = async (spaceId: string): Promise> => { return supertestWithAuth @@ -517,15 +518,15 @@ export function resolveCopyToSpaceConflictsSuite(context: DeploymentAgnosticFtrP { user, spaceId = DEFAULT_SPACE_ID, tests }: ResolveCopyToSpaceTestDefinition ) => { describeFn(description, () => { - let supertest: SupertestWithRoleScopeType | SuperTestAgent; + let supertest: SupertestWithRoleScopeType; before(async () => { - supertest = await getSupertest(context, user); + supertest = await roleScopedSupertest.getSupertestWithRoleScope(user!); // test data only allows for the following spaces as the copy origin expect(['default', 'space_1']).to.contain(spaceId); }); after(async () => { - await maybeDestroySupertest(supertest); + await supertest.destroy(); }); describe('single-namespace types', () => { diff --git a/x-pack/test/spaces_api_integration/common/suites/update.agnostic.ts b/x-pack/test/spaces_api_integration/common/suites/update.agnostic.ts index 9d7446cebd397..6ab6c3eca60e7 100644 --- a/x-pack/test/spaces_api_integration/common/suites/update.agnostic.ts +++ b/x-pack/test/spaces_api_integration/common/suites/update.agnostic.ts @@ -11,11 +11,8 @@ * 2.0. */ -import type { Agent as SuperTestAgent } from 'supertest'; +import expect from '@kbn/expect/expect'; -import expect from '@kbn/expect'; - -import { getSupertest, maybeDestroySupertest } from './common'; import type { DeploymentAgnosticFtrProviderContext, SupertestWithRoleScopeType, @@ -42,6 +39,7 @@ interface UpdateTestDefinition { export function updateTestSuiteFactory(context: DeploymentAgnosticFtrProviderContext) { const esArchiver = context.getService('esArchiver'); + const roleScopedSupertest = context.getService('roleScopedSupertest'); const expectRbacForbidden = (resp: { [key: string]: any }) => { expect(resp.body).to.eql({ @@ -84,15 +82,15 @@ export function updateTestSuiteFactory(context: DeploymentAgnosticFtrProviderCon (describeFn: DescribeFn) => (description: string, { user, spaceId, tests }: UpdateTestDefinition) => { describeFn(description, () => { - let supertest: SupertestWithRoleScopeType | SuperTestAgent; + let supertest: SupertestWithRoleScopeType; before(async () => { - supertest = await getSupertest(context, user); + supertest = await roleScopedSupertest.getSupertestWithRoleScope(user!); await esArchiver.load( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' ); }); after(async () => { - await maybeDestroySupertest(supertest); + await supertest.destroy(); await esArchiver.unload( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' ); diff --git a/x-pack/test/spaces_api_integration/deployment_agnostic/ftr_provider_context.d.ts b/x-pack/test/spaces_api_integration/deployment_agnostic/ftr_provider_context.d.ts index 3f5e391e0cb91..c8d0b14c5786f 100644 --- a/x-pack/test/spaces_api_integration/deployment_agnostic/ftr_provider_context.d.ts +++ b/x-pack/test/spaces_api_integration/deployment_agnostic/ftr_provider_context.d.ts @@ -10,4 +10,4 @@ import type { GenericFtrProviderContext } from '@kbn/test'; import type { services } from './services'; export type DeploymentAgnosticFtrProviderContext = GenericFtrProviderContext; -export type { SupertestWithRoleScopeType } from '../../api_integration/deployment_agnostic/services'; +export type { SupertestWithRoleScopeType } from './services'; diff --git a/x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/apis/index.ts index c1911110409c7..7652834480220 100644 --- a/x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/apis/index.ts @@ -21,10 +21,10 @@ export default function ({ loadTestFile, getService }: DeploymentAgnosticFtrProv } }); // loadTestFile(require.resolve('./resolve_copy_to_space_conflicts')); - // loadTestFile(require.resolve('./create')); - // loadTestFile(require.resolve('./delete')); - // loadTestFile(require.resolve('./get_all')); - loadTestFile(require.resolve('./get')); - // loadTestFile(require.resolve('./update')); + // loadTestFile(require.resolve('./create')); // PASS + loadTestFile(require.resolve('./delete')); + // loadTestFile(require.resolve('./get_all')); // PASS + // loadTestFile(require.resolve('./get')); // PASS + // loadTestFile(require.resolve('./update')); // PASS }); } diff --git a/x-pack/test/spaces_api_integration/deployment_agnostic/services/basic_auth_supertest.ts b/x-pack/test/spaces_api_integration/deployment_agnostic/services/basic_auth_supertest.ts new file mode 100644 index 0000000000000..02410fa09da0d --- /dev/null +++ b/x-pack/test/spaces_api_integration/deployment_agnostic/services/basic_auth_supertest.ts @@ -0,0 +1,54 @@ +/* + * 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 type { Test } from 'supertest'; + +import type { SupertestWithoutAuthProviderType } from '@kbn/ftr-common-functional-services'; + +import type { TestDefinitionAuthentication as User } from '../../common/lib/types'; + +export class SupertestWithBasicAuth { + private readonly supertestWithoutAuth: SupertestWithoutAuthProviderType; + private readonly user: User; + + constructor(supertestWithoutAuth: SupertestWithoutAuthProviderType, user: User) { + this.supertestWithoutAuth = supertestWithoutAuth; + this.user = user; + } + + async destroy() {} + + private request(method: 'post' | 'get' | 'put' | 'delete' | 'patch', url: string): Test { + const agent = this.supertestWithoutAuth[method](url); + + if (this.user) { + agent.auth(this.user.username!, this.user.password!); + } + + return agent; + } + + post(url: string) { + return this.request('post', url); + } + + get(url: string) { + return this.request('get', url); + } + + put(url: string) { + return this.request('put', url); + } + + delete(url: string) { + return this.request('delete', url); + } + + patch(url: string) { + return this.request('patch', url); + } +} diff --git a/x-pack/test/spaces_api_integration/deployment_agnostic/services/index.ts b/x-pack/test/spaces_api_integration/deployment_agnostic/services/index.ts index 0923a6ae4d2df..b0166ffbd4ed8 100644 --- a/x-pack/test/spaces_api_integration/deployment_agnostic/services/index.ts +++ b/x-pack/test/spaces_api_integration/deployment_agnostic/services/index.ts @@ -5,12 +5,16 @@ * 2.0. */ +import { RoleScopedSupertestProvider } from './role_scoped_supertest'; import { services as deploymentAgnosticServices } from '../../../api_integration/deployment_agnostic/services'; import { services as apiIntegrationServices } from '../../../api_integration/services'; import { services as commonServices } from '../../../common/services'; +export type { SupertestWithRoleScopeType } from './role_scoped_supertest'; + export const services = { ...deploymentAgnosticServices, ...commonServices, usageAPI: apiIntegrationServices.usageAPI, + roleScopedSupertest: RoleScopedSupertestProvider, }; diff --git a/x-pack/test/spaces_api_integration/deployment_agnostic/services/role_scoped_supertest.ts b/x-pack/test/spaces_api_integration/deployment_agnostic/services/role_scoped_supertest.ts new file mode 100644 index 0000000000000..49e16ca1b2b7a --- /dev/null +++ b/x-pack/test/spaces_api_integration/deployment_agnostic/services/role_scoped_supertest.ts @@ -0,0 +1,174 @@ +/* + * 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 type { Test } from 'supertest'; + +import type { + CookieCredentials, + RoleCredentials, + SamlAuthProviderType, + SupertestWithoutAuthProviderType, +} from '@kbn/ftr-common-functional-services'; + +import { SupertestWithBasicAuth } from './basic_auth_supertest'; +import { getRoleDefinitionForUser, isBuiltInRole } from '../../common/lib/authentication'; +import type { TestDefinitionAuthentication as User } from '../../common/lib/types'; +import type { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context'; + +export interface RequestHeadersOptions { + useCookieHeader?: boolean; + withInternalHeaders?: boolean; + withCommonHeaders?: boolean; + withCustomHeaders?: Record; +} + +export type SupertestWithRoleScopeType = SupertestWithBasicAuth | SupertestWithRoleScope; + +export class SupertestWithRoleScope { + private authValue: RoleCredentials | CookieCredentials | null; + private readonly supertestWithoutAuth: SupertestWithoutAuthProviderType; + private samlAuth: SamlAuthProviderType; + private readonly options: RequestHeadersOptions; + + constructor( + authValue: RoleCredentials | CookieCredentials | null, + supertestWithoutAuth: SupertestWithoutAuthProviderType, + samlAuth: SamlAuthProviderType, + options: RequestHeadersOptions + ) { + this.authValue = authValue; + this.supertestWithoutAuth = supertestWithoutAuth; + this.samlAuth = samlAuth; + this.options = options; + } + + private isRoleCredentials(value: any): value is RoleCredentials { + return value && typeof value === 'object' && 'apiKey' in value && 'apiKeyHeader' in value; + } + + async destroy() { + if (this.isRoleCredentials(this.authValue)) { + await this.samlAuth.invalidateM2mApiKeyWithRoleScope(this.authValue); + this.authValue = null; + } + } + + private addHeaders(agent: Test): Test { + const { useCookieHeader, withInternalHeaders, withCommonHeaders, withCustomHeaders } = + this.options; + + if (useCookieHeader) { + if (!this.authValue || !('Cookie' in this.authValue)) { + throw new Error('The instance has already been destroyed or cookieHeader is missing.'); + } + // set cookie header + void agent.set(this.authValue); + } else { + if (!this.authValue || !this.isRoleCredentials(this.authValue)) { + throw new Error('The instance has already been destroyed or roleAuthc is missing.'); + } + // set API key header + void agent.set(this.authValue.apiKeyHeader); + } + + if (withInternalHeaders) { + void agent.set(this.samlAuth.getInternalRequestHeader()); + } + + if (withCommonHeaders) { + void agent.set(this.samlAuth.getCommonRequestHeader()); + } + + if (withCustomHeaders) { + void agent.set(withCustomHeaders); + } + + return agent; + } + + private request(method: 'post' | 'get' | 'put' | 'delete' | 'patch', url: string): Test { + if (!this.authValue) { + throw new Error('Instance has been destroyed and cannot be used for making requests.'); + } + const agent = this.supertestWithoutAuth[method](url); + return this.addHeaders(agent); + } + + post(url: string) { + return this.request('post', url); + } + + get(url: string) { + return this.request('get', url); + } + + put(url: string) { + return this.request('put', url); + } + + delete(url: string) { + return this.request('delete', url); + } + + patch(url: string) { + return this.request('patch', url); + } +} + +/** + * Provides a customized 'supertest' instance that is authenticated using a role-based API key + * and enriched with the appropriate request headers. This service allows you to perform + * HTTP requests with specific authentication and header configurations, ensuring that + * the requests are scoped to the provided role and environment. + * + * Use this service to easily test API endpoints with role-specific authorization and + * custom headers, both in serverless and stateful environments. + * + * Pass '{ useCookieHeader: true }' to use Cookie header for authentication instead of API key. + * It is the correct way to perform HTTP requests for internal end-points. + */ +export function RoleScopedSupertestProvider({ getService }: DeploymentAgnosticFtrProviderContext) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const samlAuth = getService('samlAuth'); + const config = getService('config'); + + return { + async getSupertestWithRoleScope( + user: User, + options: RequestHeadersOptions = { + useCookieHeader: true, + withCommonHeaders: false, + withInternalHeaders: true, + } + ) { + const license = config.get('esTestCluster.license'); + + if (!user || license !== 'trial') { + return new SupertestWithBasicAuth(supertestWithoutAuth, user); + } + + const isBuiltIn = isBuiltInRole(user.role); + + if (!isBuiltIn) { + await samlAuth.setCustomRole(getRoleDefinitionForUser(user)); + } + + if (options.useCookieHeader) { + const cookieHeader = await samlAuth.getM2MApiCookieCredentialsWithRoleScope( + isBuiltIn ? user.role : 'customRole' + ); + return new SupertestWithRoleScope(cookieHeader, supertestWithoutAuth, samlAuth, options); + } + + // HTTP requests will be called with API key in header by default + const roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope( + isBuiltIn ? user.role : 'customRole' + ); + return new SupertestWithRoleScope(roleAuthc, supertestWithoutAuth, samlAuth, options); + }, + }; +}