Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FTR] allow to call roleScopedSupertest service with Cookie header #192727

Merged
merged 39 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
95e7451
do not return admin cookie with api_key
dmlemeshko Sep 12, 2024
2bcec5c
update SupertestWithRoleScope to support both API key and cookie header
dmlemeshko Sep 12, 2024
5d5e449
revert requestBody for API key invalidation
dmlemeshko Sep 12, 2024
4aa29ce
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 13, 2024
21a8fa0
fix types in tests
dmlemeshko Sep 13, 2024
1fd3ead
update samlUath service
dmlemeshko Sep 13, 2024
2ed4929
Merge branch 'ftr/improve-test-auth-options' of github.com:dmlemeshko…
dmlemeshko Sep 13, 2024
bbfef2f
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 13, 2024
a7874f6
fix type export
dmlemeshko Sep 13, 2024
45988fb
Merge branch 'ftr/improve-test-auth-options' of github.com:dmlemeshko…
dmlemeshko Sep 13, 2024
ae1c516
fix typo
dmlemeshko Sep 13, 2024
3dddbb7
migrate tests to use cookieHeader
dmlemeshko Sep 13, 2024
166eacf
update more tests
dmlemeshko Sep 13, 2024
733503d
Merge branch 'ftr/improve-test-auth-options' of github.com:dmlemeshko…
dmlemeshko Sep 13, 2024
d353784
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 13, 2024
d89d468
update more platform_security tests
dmlemeshko Sep 13, 2024
267168a
Merge branch 'ftr/improve-test-auth-options' of github.com:dmlemeshko…
dmlemeshko Sep 13, 2024
ad9d618
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 13, 2024
fe64f44
convert more tests to use cookie credentials
dmlemeshko Sep 16, 2024
e7d6c9f
update docs
dmlemeshko Sep 16, 2024
32a9892
Merge branch 'ftr/improve-test-auth-options' of github.com:dmlemeshko…
dmlemeshko Sep 16, 2024
91048df
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 16, 2024
51b7339
fix role typo
dmlemeshko Sep 16, 2024
7f29c2e
Merge branch 'ftr/improve-test-auth-options' of github.com:dmlemeshko…
dmlemeshko Sep 16, 2024
16225eb
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 16, 2024
bfcc489
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 17, 2024
5e18cd9
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 18, 2024
e592be9
Update x-pack/test_serverless/README.md
dmlemeshko Sep 18, 2024
f6d935e
rename function to getM2MApiCookieCredentialsWithRoleScope
dmlemeshko Sep 18, 2024
192fea2
Update x-pack/test_serverless/README.md
dmlemeshko Sep 18, 2024
e031d9c
update tests
dmlemeshko Sep 18, 2024
8040bdf
Merge branch 'ftr/improve-test-auth-options' of github.com:dmlemeshko…
dmlemeshko Sep 18, 2024
8c25dc1
fix type check failure
dmlemeshko Sep 18, 2024
27153d3
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 18, 2024
a762679
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 18, 2024
16cf8e7
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 19, 2024
4966103
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 19, 2024
bf921ee
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 19, 2024
2899971
Merge branch 'main' into ftr/improve-test-auth-options
dmlemeshko Sep 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/kbn-ftr-common-functional-services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ export type Es = ProvidedType<typeof EsProvider>;
import { SupertestWithoutAuthProvider } from './services/supertest_without_auth';
export type SupertestWithoutAuthProviderType = ProvidedType<typeof SupertestWithoutAuthProvider>;

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

import { SamlAuthProvider } from './services/saml_auth/saml_auth_provider';
export type SamlAuthProviderType = ProvidedType<typeof SamlAuthProvider>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
*/

export { SamlAuthProvider } from './saml_auth_provider';
export type { RoleCredentials } from './saml_auth_provider';
export type { RoleCredentials, CookieCredentials } from './saml_auth_provider';
export type { InternalRequestHeader } from './default_request_headers';
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import { InternalRequestHeader } from './default_request_headers';
export interface RoleCredentials {
apiKey: { id: string; name: string };
apiKeyHeader: { Authorization: string };
cookieHeader: { Cookie: string };
}

export interface CookieCredentials {
Cookie: string;
// supertest.set() expects an object that matches IncomingHttpHeaders type, that needs to accept arbitrary key-value pairs as headers
// We extend the interface with an index signature to resolve this.
[header: string]: string;
}

export function SamlAuthProvider({ getService }: FtrProviderContext) {
Expand Down Expand Up @@ -60,7 +66,7 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
async getInteractiveUserSessionCookieWithRoleScope(role: string) {
return sessionManager.getInteractiveUserSessionCookieWithRoleScope(role);
},
async getM2MApiCredentialsWithRoleScope(role: string) {
async getM2MApiCookieCredentialsWithRoleScope(role: string): Promise<CookieCredentials> {
return sessionManager.getApiCredentialsForRole(role);
},
async getEmail(role: string) {
Expand All @@ -76,7 +82,7 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
},
async createM2mApiKeyWithRoleScope(role: string): Promise<RoleCredentials> {
// Get admin credentials in order to create the API key
const adminCookieHeader = await this.getM2MApiCredentialsWithRoleScope('admin');
const adminCookieHeader = await this.getM2MApiCookieCredentialsWithRoleScope('admin');

// Get the role descrtiptor for the role
let roleDescriptors = {};
Expand Down Expand Up @@ -110,9 +116,12 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
const apiKeyHeader = { Authorization: 'ApiKey ' + apiKey.encoded };

log.debug(`Created api key for role: [${role}]`);
return { apiKey, apiKeyHeader, cookieHeader: adminCookieHeader };
Copy link
Member Author

@dmlemeshko dmlemeshko Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to avoid misuse: this function returns only API key + header with role-scoped privileges. To get Cookie header for specific role, use the getM2MApiCredentialsWithRoleScope function

return { apiKey, apiKeyHeader };
},
async invalidateM2mApiKeyWithRoleScope(roleCredentials: RoleCredentials) {
// Get admin credentials in order to invalidate the API key
const adminCookieHeader = await this.getM2MApiCookieCredentialsWithRoleScope('admin');

const requestBody = {
apiKeys: [
{
Expand All @@ -126,7 +135,7 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) {
const { status } = await supertestWithoutAuth
.post('/internal/security/api_key/invalidate')
.set(INTERNAL_REQUEST_HEADERS)
.set(roleCredentials.cookieHeader)
.set(adminCookieHeader)
.send(requestBody);

expect(status).to.be(200);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@ import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { IndexDetails } from '@kbn/cloud-security-posture-common';
import { CLOUD_SECURITY_PLUGIN_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants';
import { SecurityService } from '@kbn/ftr-common-functional-ui-services';

export interface RoleCredentials {
apiKey: { id: string; name: string };
apiKeyHeader: { Authorization: string };
cookieHeader: { Cookie: string };
}
import { RoleCredentials } from '@kbn/ftr-common-functional-services';

export const deleteIndex = async (es: Client, indexToBeDeleted: string[]) => {
return Promise.all([
Expand Down
14 changes: 14 additions & 0 deletions x-pack/test/api_integration/deployment_agnostic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@ export type DeploymentAgnosticFtrProviderContext = GenericFtrProviderContext<typ

3. Add Tests

API Authentication in Kibana: Public vs. Internal APIs

Kibana provides both public and internal APIs, each requiring authentication with the correct privileges. However, the method of testing these APIs varies, depending on how they are untilized by end users.

- Public APIs: When testing HTTP requests to public APIs, API key-based authentication should be used. It reflect how end user call these APIs. Due to existing restrictions, we utilize `Admin` user credentials to generate API keys for various roles. While the API key permissions are correctly scoped according to the assigned role, the user will internally be recognized as `Admin` during authentication.

- Internal APIs: Direct HTTP requests to internal APIs are generally not expected. However, for testing purposes, authentication should be performed using the Cookie header. This approach simulates client-side behavior during browser interactions, mirroring how internal APIs are indirectly invoked.

Recommendations:
- use `roleScopedSupertest` service to create supertest instance scoped to specific role and pre-defined request headers
- `roleScopedSupertest.getSupertestWithRoleScope(<role>)` authenticate requests with API key by default
- pass `withCookieHeader: true` to use Cookie header for requests authentication
- don't forget to invalidate API key using `destroy()` on supertest scoped instance in `after` hook

Add test files to `x-pack/test/<my_own_api_integration_folder>/deployment_agnostic/apis/<my_api>`:

test example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,66 @@

import {
RoleCredentials,
CookieCredentials,
SupertestWithoutAuthProviderType,
SamlAuthProviderType,
} from '@kbn/ftr-common-functional-services';
import { Test } from 'supertest';
import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context';

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

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

constructor(
roleAuthc: RoleCredentials,
authValue: RoleCredentials | CookieCredentials | null,
supertestWithoutAuth: SupertestWithoutAuthProviderType,
samlAuth: SamlAuthProviderType,
options: RequestHeadersOptions
) {
this.roleAuthc = roleAuthc;
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.roleAuthc) {
await this.samlAuth.invalidateM2mApiKeyWithRoleScope(this.roleAuthc);
this.roleAuthc = null;
if (this.isRoleCredentials(this.authValue)) {
await this.samlAuth.invalidateM2mApiKeyWithRoleScope(this.authValue);
this.authValue = null;
Comment on lines -41 to +49
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only delete API keys in test suites.

Cookie headers persist within FTR config run: we generate one per role on the first call and cache it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could return the destroy fn from the create functions to make it obvious that it needs to be called.
But I would only add this if we do indeed begin to discover failures due to not calling destroy.
Most likely we won't need it
🤞🏾

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about it initially and decided to not change the existing interfaces. In the end, leaving API key unvalidated shouldn't lead to any failures.

}
}

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

if (!this.roleAuthc) {
throw new Error('The instance has already been destroyed.');
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);
}
// set role-based API key by default
void agent.set(this.roleAuthc.apiKeyHeader);

if (withInternalHeaders) {
void agent.set(this.samlAuth.getInternalRequestHeader());
Expand All @@ -69,7 +84,7 @@ export class SupertestWithRoleScope {
}

private request(method: 'post' | 'get' | 'put' | 'delete', url: string): Test {
if (!this.roleAuthc) {
if (!this.authValue) {
throw new Error('Instance has been destroyed and cannot be used for making requests.');
}
const agent = this.supertestWithoutAuth[method](url);
Expand Down Expand Up @@ -101,6 +116,9 @@ export class SupertestWithRoleScope {
*
* 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');
Expand All @@ -110,10 +128,18 @@ export function RoleScopedSupertestProvider({ getService }: DeploymentAgnosticFt
async getSupertestWithRoleScope(
role: string,
options: RequestHeadersOptions = {
useCookieHeader: false,
withCommonHeaders: false,
withInternalHeaders: false,
}
) {
// if 'useCookieHeader' set to 'true', HTTP requests will be called with cookie Header (like in browser)
if (options.useCookieHeader) {
const cookieHeader = await samlAuth.getM2MApiCookieCredentialsWithRoleScope(role);
return new SupertestWithRoleScope(cookieHeader, supertestWithoutAuth, samlAuth, options);
}

// HTTP requests will be called with API key in header by default
const roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope(role);
return new SupertestWithRoleScope(roleAuthc, supertestWithoutAuth, samlAuth, options);
},
Expand Down
54 changes: 43 additions & 11 deletions x-pack/test_serverless/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,28 +143,60 @@ describe("my test suite", async function() {

#### API integration test example

API Authentication in Kibana: Public vs. Internal APIs

Kibana provides both public and internal APIs, each requiring authentication with the correct privileges. However, the method of testing these APIs varies, depending on how they are untilized by end users.

- Public APIs: When testing HTTP requests to public APIs, API key-based authentication should be used. It reflects how an end user calls these APIs. Due to existing restrictions, we utilize `Admin` user credentials to generate API keys for various roles. While the API key permissions are correctly scoped according to the assigned role, the user will internally be recognized as `Admin` during authentication.

- Internal APIs: Direct HTTP requests to internal APIs are generally not expected. However, for testing purposes, authentication should be performed using the Cookie header. This approach simulates client-side behavior during browser interactions, mirroring how internal APIs are indirectly invoked.

Recommendations:
- in each test file top level `describe` suite should start with `createM2mApiKeyWithRoleScope` call in `before` hook
- don't forget to invalidate api key using `invalidateApiKeyWithRoleScope` in `after` hook
- make api calls using `supertestWithoutAuth` with generated api key header
- use `roleScopedSupertest` service to create a supertest instance scoped to a specific role and predefined request headers
- `roleScopedSupertest.getSupertestWithRoleScope(<role>)` authenticates requests with an API key by default
- pass `withCookieHeader: true` to use Cookie header for request authentication
- don't forget to invalidate API keys by using `destroy()` on the supertest scoped instance in the `after` hook

```
describe("my test suite", async function() {
describe("my public APIs test suite", async function() {
before(async () => {
roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('viewer');
commonRequestHeader = svlCommonApi.getCommonRequestHeader();
internalRequestHeader = svlCommonApi.getInternalRequestHeader();
supertestViewerWithApiKey =
await roleScopedSupertest.getSupertestWithRoleScope('viewer', {
withInternalHeaders: true,
dmlemeshko marked this conversation as resolved.
Show resolved Hide resolved
});
});

after(async () => {
await svlUserManager.invalidateApiKeyWithRoleScope(roleAuthc);
await supertestViewerWithApiKey.destroy();
});

it(''test step', async () => {
const { body, status } = await supertestWithoutAuth
const { body, status } = await supertestViewerWithApiKey
.delete('/api/spaces/space/default')
.set(commonRequestHeader)
.set(roleAuthc.apiKeyHeader);
...
});
});
```

```
describe("my internal APIs test suite", async function() {
before(async () => {
supertestViewerWithCookieCredentials =
await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withCookieHeader: true, // to avoid generating API key and use Cookie header instead
withInternalHeaders: true,
});
});

after(async () => {
// no need to call '.destroy' since we didn't create API key and Cookie persist for the role within FTR run
});

it(''test step', async () => {
await supertestAdminWithCookieCredentials
.post(`/internal/kibana/settings`)
.send({ changes: { [TEST_SETTING]: 500 } })
.expect(200);
...
});
});
Expand Down
Loading