Skip to content

Commit

Permalink
[ftr] add roleScopedSupertest service for deployment-agnostic tests (#…
Browse files Browse the repository at this point in the history
…190279)

## Summary

Adding new service that acts as a wrapper of `supertestWithoutAuth`
service authenticated with role-based API key and enriched with request
headers.

The proposed change streamlines test design by centralizing the
management of API key and internal/common headers, eliminating the need
to pass these arguments individually in every API call. This approach
reduces code duplication and enhances maintainability

Before:

```ts
const samlAuth = getService('samlAuth');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
const internalHeaders = samlAuth.getInternalRequestHeader();

await supertestWithoutAuth
  .get('/api/console/api_server')
  .set(roleAuthc.apiKeyHeader)
  .set(internalHeaders)
  .set('kbn-xsrf', 'true')
  .expect(200);
```

After: 
```ts
const roleScopedSupertest = getService('roleScopedSupertest');
const supertestWithAdminScope = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
  withInternalHeaders: true,
  withCustomHeaders: {'kbn-xsrf': 'true'},
});

await supertestWithAdminScope
  .get('/api/console/api_server')
  .expect(200);
```

Use this service to easily test API endpoints with role-specific
authorization and custom headers, both in serverless and stateful
environments.

closes #190228

---------

Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
  • Loading branch information
dmlemeshko and azasypkin committed Aug 14, 2024
1 parent a13f8d9 commit 96d3325
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 60 deletions.
3 changes: 3 additions & 0 deletions packages/kbn-ftr-common-functional-services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ export type SupertestWithoutAuthProviderType = ProvidedType<typeof SupertestWith

export type { InternalRequestHeader, RoleCredentials } from './services/saml_auth';

import { SamlAuthProvider } from './services/saml_auth/saml_auth_provider';
export type SamlAuthProviderType = ProvidedType<typeof SamlAuthProvider>;

export type { FtrProviderContext } from './services/ftr_provider_context';
21 changes: 9 additions & 12 deletions x-pack/test/api_integration/deployment_agnostic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,26 +94,23 @@ Add test files to `x-pack/test/<my_own_api_integration_folder>/deployment_agnost
test example
```ts
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const samlAuth = getService('samlAuth');
const supertestWithoutAuth = getService('supertestWithoutAuth');
let roleAuthc: RoleCredentials;
let internalHeaders: InternalRequestHeader;
const roleScopedSupertest = getService('roleScopedSupertest');
let supertestWithAdminScope: SupertestWithRoleScopeType;

describe('compression', () => {
before(async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
internalHeaders = samlAuth.getInternalRequestHeader();
supertestWithAdminScope = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withInternalHeaders: true,
withCustomHeaders: { 'accept-encoding': 'gzip' },
});
});
after(async () => {
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
// always invalidate API key for the scoped role in the end
await supertestWithAdminScope.destroy();
});
describe('against an application page', () => {
it(`uses compression when there isn't a referer`, async () => {
const response = await supertestWithoutAuth
.get('/app/kibana')
.set('accept-encoding', 'gzip')
.set(internalHeaders)
.set(roleAuthc.apiKeyHeader);
const response = await supertestWithAdminScope.get('/app/kibana');
expect(response.header).to.have.property('content-encoding', 'gzip');
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,25 @@
*/

import expect from '@kbn/expect';
import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services';
import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
import { SupertestWithRoleScopeType } from '../../services';

export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const samlAuth = getService('samlAuth');
const supertestWithoutAuth = getService('supertestWithoutAuth');
let roleAuthc: RoleCredentials;
let internalHeaders: InternalRequestHeader;
const roleScopedSupertest = getService('roleScopedSupertest');
let supertestWithAdminScope: SupertestWithRoleScopeType;

describe('GET /api/console/api_server', () => {
before(async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
internalHeaders = samlAuth.getInternalRequestHeader();
supertestWithAdminScope = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withInternalHeaders: true,
withCustomHeaders: { 'kbn-xsrf': 'true' },
});
});
after(async () => {
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
await supertestWithAdminScope.destroy();
});
it('returns autocomplete definitions', async () => {
const { body } = await supertestWithoutAuth
.get('/api/console/api_server')
.set(roleAuthc.apiKeyHeader)
.set(internalHeaders)
.set('kbn-xsrf', 'true')
.expect(200);
const { body } = await supertestWithAdminScope.get('/api/console/api_server').expect(200);
expect(body.es).to.be.ok();
const {
es: { name, globals, endpoints },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,32 @@
*/

import expect from '@kbn/expect';
import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services';
import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
import { SupertestWithRoleScopeType } from '../../services';

export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const samlAuth = getService('samlAuth');
const supertestWithoutAuth = getService('supertestWithoutAuth');
let roleAuthc: RoleCredentials;
let internalHeaders: InternalRequestHeader;
const roleScopedSupertest = getService('roleScopedSupertest');
let supertestWithAdminScope: SupertestWithRoleScopeType;

describe('compression', () => {
before(async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
internalHeaders = samlAuth.getInternalRequestHeader();
supertestWithAdminScope = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withCustomHeaders: { 'accept-encoding': 'gzip' },
});
});
after(async () => {
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
await supertestWithAdminScope.destroy();
});
describe('against an application page', () => {
it(`uses compression when there isn't a referer`, async () => {
const response = await supertestWithoutAuth
.get('/app/kibana')
.set('accept-encoding', 'gzip')
.set(internalHeaders)
.set(roleAuthc.apiKeyHeader);
const response = await supertestWithAdminScope.get('/app/kibana');
expect(response.header).to.have.property('content-encoding', 'gzip');
});

it(`uses compression when there is a whitelisted referer`, async () => {
const response = await supertestWithoutAuth
const response = await supertestWithAdminScope
.get('/app/kibana')
.set('accept-encoding', 'gzip')
.set(internalHeaders)
.set('referer', 'https://some-host.com')
.set(roleAuthc.apiKeyHeader);
.set('referer', 'https://some-host.com');
expect(response.header).to.have.property('content-encoding', 'gzip');
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,32 @@
*/

import expect from '@kbn/expect';
import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services';
import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
import { SupertestWithRoleScopeType } from '../../services';

const API_BASE_PATH = '/api/painless_lab';

export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const samlAuth = getService('samlAuth');
const supertestWithoutAuth = getService('supertestWithoutAuth');
let roleAuthc: RoleCredentials;
let internalHeaders: InternalRequestHeader;
const roleScopedSupertest = getService('roleScopedSupertest');
let supertestWithAdminScope: SupertestWithRoleScopeType;

describe('Painless Lab Routes', function () {
before(async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
internalHeaders = samlAuth.getInternalRequestHeader();
supertestWithAdminScope = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withInternalHeaders: true,
withCustomHeaders: { 'Content-Type': 'application/json;charset=UTF-8' },
});
});
after(async () => {
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
await supertestWithAdminScope.destroy();
});
describe('Execute', () => {
it('should execute a valid painless script', async () => {
const script =
'"{\\n \\"script\\": {\\n \\"source\\": \\"return true;\\",\\n \\"params\\": {\\n \\"string_parameter\\": \\"string value\\",\\n \\"number_parameter\\": 1.5,\\n \\"boolean_parameter\\": true\\n}\\n }\\n}"';

const { body } = await supertestWithoutAuth
const { body } = await supertestWithAdminScope
.post(`${API_BASE_PATH}/execute`)
.set(internalHeaders)
.set(roleAuthc.apiKeyHeader)
.set('Content-Type', 'application/json;charset=UTF-8')
.send(script)
.expect(200);

Expand All @@ -47,11 +44,8 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const invalidScript =
'"{\\n \\"script\\": {\\n \\"source\\": \\"foobar\\",\\n \\"params\\": {\\n \\"string_parameter\\": \\"string value\\",\\n \\"number_parameter\\": 1.5,\\n \\"boolean_parameter\\": true\\n}\\n }\\n}"';

const { body } = await supertestWithoutAuth
const { body } = await supertestWithAdminScope
.post(`${API_BASE_PATH}/execute`)
.set(internalHeaders)
.set('Content-Type', 'application/json;charset=UTF-8')
.set(roleAuthc.apiKeyHeader)
.send(invalidScript)
.expect(200);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { deploymentAgnosticServices } from './deployment_agnostic_services';
import { DataViewApiProvider } from './data_view_api';
import { SloApiProvider } from './slo_api';
import { RoleScopedSupertestProvider, SupertestWithRoleScope } from './role_scoped_supertest';

export type {
InternalRequestHeader,
Expand All @@ -22,7 +23,9 @@ export const services = {
samlAuth: commonFunctionalServices.samlAuth,
dataViewApi: DataViewApiProvider,
sloApi: SloApiProvider,
roleScopedSupertest: RoleScopedSupertestProvider,
// create a new deployment-agnostic service and load here
};

export type SupertestWithRoleScopeType = SupertestWithRoleScope;
export type DeploymentAgnosticCommonServices = typeof services;
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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 {
RoleCredentials,
SupertestWithoutAuthProviderType,
SamlAuthProviderType,
} from '@kbn/ftr-common-functional-services';
import { Test } from 'supertest';
import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context';

export interface RequestHeadersOptions {
withInternalHeaders?: boolean;
withCommonHeaders?: boolean;
withCustomHeaders?: Record<string, string>;
}

export class SupertestWithRoleScope {
private roleAuthc: RoleCredentials | null;
private readonly supertestWithoutAuth: SupertestWithoutAuthProviderType;
private samlAuth: SamlAuthProviderType;
private readonly options: RequestHeadersOptions;

constructor(
roleAuthc: RoleCredentials,
supertestWithoutAuth: SupertestWithoutAuthProviderType,
samlAuth: SamlAuthProviderType,
options: RequestHeadersOptions
) {
this.roleAuthc = roleAuthc;
this.supertestWithoutAuth = supertestWithoutAuth;
this.samlAuth = samlAuth;
this.options = options;
}

async destroy() {
if (this.roleAuthc) {
await this.samlAuth.invalidateM2mApiKeyWithRoleScope(this.roleAuthc);
this.roleAuthc = null;
}
}

private addHeaders(agent: Test): Test {
const { withInternalHeaders, withCommonHeaders, withCustomHeaders } = this.options;

if (!this.roleAuthc) {
throw new Error('The instance has already been destroyed.');
}
// set role-based API key by default
agent.set(this.roleAuthc.apiKeyHeader);

if (withInternalHeaders) {
agent.set(this.samlAuth.getInternalRequestHeader());
}

if (withCommonHeaders) {
agent.set(this.samlAuth.getCommonRequestHeader());
}

if (withCustomHeaders) {
agent.set(withCustomHeaders);
}

return agent;
}

private request(method: 'post' | 'get' | 'put' | 'delete', url: string): Test {
if (!this.roleAuthc) {
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);
}
}

/**
* 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.
*/
export function RoleScopedSupertestProvider({ getService }: DeploymentAgnosticFtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
const samlAuth = getService('samlAuth');

return {
async getSupertestWithRoleScope(
role: string,
options: RequestHeadersOptions = {
withCommonHeaders: false,
withInternalHeaders: false,
}
) {
const roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope(role);
return new SupertestWithRoleScope(roleAuthc, supertestWithoutAuth, samlAuth, options);
},
};
}

0 comments on commit 96d3325

Please sign in to comment.