[FTR] support "deployment agnostic" api-integration tests (#189853)

## Summary

### This PR introduces a new type of API integration tests in FTR:
deployment-agnostic

![8zcgq0
(1)](https://github.com/user-attachments/assets/17c6d4ee-7848-4a4c-a006-7ae54e523243)

#### Test suite is considered deployment-agnostic when it fulfils the
following criteria:

**Functionality**: It tests Kibana APIs that are **logically identical
in both stateful and serverless environments** for the same SAML roles.

**Design**: The test design is **clean and does not require additional
logic** to execute in either stateful or serverless environments.

### How It Works
Most existing stateful tests use basic authentication for API testing.
In contrast, serverless tests use SAML authentication with
project-specific role mapping.

Since stateful deployments also support SAML, deployment-agnostic tests
**configure Elasticsearch and Kibana with SAML authentication in both
cases**. For roles, stateful deployments define 'viewer', 'editor', and
'admin' roles with serverless-alike privileges.

New `samlAuth` service has `AuthProvider` interface with 2 different
implementations: depending on environment context (serverless or
stateful) appropriate implementation is used. But it remains on service
level and hidden in test suite.

test example
```
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
  const samlAuth = getService('samlAuth');
  const supertestWithoutAuth = getService('supertestWithoutAuth');
  let roleAuthc: RoleCredentials;
  let internalHeaders: InternalRequestHeader;

  describe('GET /api/console/api_server', () => {
    before(async () => {
      roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
      internalHeaders = samlAuth.getInternalRequestHeader();
    });
    after(async () => {
      await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
    });
    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);
      expect(body.es).to.be.ok();
      const {
        es: { name, globals, endpoints },
      } = body;
      expect(name).to.be.ok();
      expect(Object.keys(globals).length).to.be.above(0);
      expect(Object.keys(endpoints).length).to.be.above(0);
    });
  });
}
```

Please read
[readme](966822ac87/x-pack/test/api_integration/deployment_agnostic/README.md)
for more details and step-by-step guide. It should help migrating
existing serverless tests to deployment-agnostic, assuming requirements
are met.

### Examples

Deployment-agnostic tests:

```
x-pack/test/api_integration/deployment_agnostic/apis/console/spec_definitions.ts

x-pack/test/api_integration/deployment_agnostic/apis/core/compression.ts

x-pack/test/api_integration/deployment_agnostic/apis/painless_lab/painless_lab.ts
```

Configs to run it:

```
node scripts/functional_tests --config x-pack/test/api_integration/deployment_agnostic/oblt.serverless.config.ts
node scripts/functional_tests --config x-pack/test/api_integration/deployment_agnostic/search.serverless.config.ts
node scripts/functional_tests --config x-pack/test/api_integration/deployment_agnostic/security.serverless.config.ts

node scripts/functional_tests --config x-pack/test/api_integration/deployment_agnostic/stateful.config.ts
```





PR is a compact version of #188737 with reduced changes in existing
serverless tests.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: elena-shostak <165678770+elena-shostak@users.noreply.github.com>
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
This commit is contained in:
Dzmitry Lemechko 2024-08-07 17:34:52 +02:00 committed by GitHub
parent f3aeb81fd6
commit 7df01e99c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 1758 additions and 422 deletions

View file

@ -1,6 +1,8 @@
disabled:
# Base config files, only necessary to inform config finding script
# Serverless deployment-agnostic default config for api-integration tests
- x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts
# Serverless base config files
- x-pack/test_serverless/api_integration/config.base.ts
- x-pack/test_serverless/functional/config.base.ts

View file

@ -26,3 +26,5 @@ enabled:
- x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group5.ts
- x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group6.ts
- x-pack/test_serverless/functional/test_suites/observability/config.screenshots.ts
# serverless config files that run deployment-agnostic tests
- x-pack/test/api_integration/deployment_agnostic/oblt.serverless.config.ts

View file

@ -1,4 +1,6 @@
disabled:
# Stateful base config for deployment-agnostic tests
- x-pack/test/api_integration/deployment_agnostic/default_configs/stateful.config.base.ts
# Base config files, only necessary to inform config finding script
- test/functional/config.base.js
- test/functional/firefox/config.base.ts
@ -155,7 +157,6 @@ enabled:
- x-pack/test/api_integration/apis/monitoring/config.ts
- x-pack/test/api_integration/apis/monitoring_collection/config.ts
- x-pack/test/api_integration/apis/osquery/config.ts
- x-pack/test/api_integration/apis/painless_lab/config.ts
- x-pack/test/api_integration/apis/search/config.ts
- x-pack/test/api_integration/apis/searchprofiler/config.ts
- x-pack/test/api_integration/apis/security/config.ts
@ -359,3 +360,5 @@ enabled:
- x-pack/performance/journeys_e2e/apm_service_inventory.ts
- x-pack/performance/journeys_e2e/infra_hosts_view.ts
- x-pack/test/custom_branding/config.ts
# stateful config files that run deployment-agnostic tests
- x-pack/test/api_integration/deployment_agnostic/stateful.config.ts

View file

@ -16,3 +16,5 @@ enabled:
- x-pack/test_serverless/functional/test_suites/search/common_configs/config.group4.ts
- x-pack/test_serverless/functional/test_suites/search/common_configs/config.group5.ts
- x-pack/test_serverless/functional/test_suites/search/common_configs/config.group6.ts
# serverless config files that run deployment-agnostic tests
- x-pack/test/api_integration/deployment_agnostic/search.serverless.config.ts

View file

@ -97,3 +97,5 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_endpoint/configs/serverless.endpoint.config.ts
- x-pack/test/security_solution_endpoint/configs/serverless.integrations.config.ts
# serverless config files that run deployment-agnostic tests
- x-pack/test/api_integration/deployment_agnostic/security.serverless.config.ts

View file

@ -618,7 +618,7 @@ module.exports = {
'test/*/*.config.ts',
'test/*/{tests,test_suites,apis,apps}/**/*',
'test/server_integration/**/*.ts',
'x-pack/test/*/{tests,test_suites,apis,apps}/**/*',
'x-pack/test/*/{tests,test_suites,apis,apps,deployment_agnostic}/**/*',
'x-pack/test/*/*config.*ts',
'x-pack/test/saved_object_api_integration/*/apis/**/*',
'x-pack/test/ui_capabilities/*/tests/**/*',

1
.github/CODEOWNERS vendored
View file

@ -1274,6 +1274,7 @@ x-pack/test/observability_ai_assistant_functional @elastic/obs-ai-assistant
/x-pack/test_serverless/functional/test_suites/security/ftr/ @elastic/appex-qa
/x-pack/test_serverless/functional/test_suites/common/home_page/ @elastic/appex-qa
/x-pack/test_serverless/**/services/ @elastic/appex-qa
/packages/kbn-es/src/stateful_resources/roles.yml @elastic/appex-qa
# Core
/config/ @elastic/kibana-core

View file

@ -21,4 +21,4 @@ export {
readRolesDescriptorsFromResource,
} from './src/utils';
export type { ArtifactLicense } from './src/artifact';
export { SERVERLESS_ROLES_ROOT_PATH } from './src/paths';
export { SERVERLESS_ROLES_ROOT_PATH, STATEFUL_ROLES_ROOT_PATH } from './src/paths';

View file

@ -25,6 +25,8 @@ export const ES_CONFIG = 'config/elasticsearch.yml';
export const ES_KEYSTORE_BIN = maybeUseBat('./bin/elasticsearch-keystore');
export const STATEFUL_ROLES_ROOT_PATH = resolve(__dirname, './stateful_resources');
export const SERVERLESS_OPERATOR_USERS_PATH = resolve(
__dirname,
'./serverless_resources/operator_users.yml'

View file

@ -0,0 +1,130 @@
# -----
# This file is for information purpose only. 'viewer' and 'editor' roles are defined in stateful Elasticsearch by default
# Source: https://github.com/elastic/elasticsearch/blob/4272164530807787d4d8b991e3095a6e79176dbf/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java#L861-L952
# Note: inconsistency between these roles definition and the same roles of serverless project may break FTR deployment-agnostic tests
# -----
viewer:
cluster: []
indices:
- names:
- '.alerts*'
- '.preview.alerts*'
privileges:
- 'read'
- 'view_index_metadata'
allow_restricted_indices: false
- names:
- '.items-*'
- '.lists-*'
- '.siem-signals*'
privileges:
- 'read'
- 'view_index_metadata'
allow_restricted_indices: false
- names:
- '/~(([.]|ilm-history-).*)/'
privileges:
- 'read'
- 'view_index_metadata'
allow_restricted_indices: false
- names:
- '.profiling-*'
- 'profiling-*'
privileges:
- 'read'
- 'view_index_metadata'
allow_restricted_indices: false
applications:
- application: 'kibana-.kibana'
privileges:
- 'read'
resources:
- '*'
run_as: []
editor:
cluster: []
indices:
- names:
- 'observability-annotations'
privileges:
- 'read'
- 'view_index_metadata'
- 'write'
allow_restricted_indices: false
- names:
- '.items-*'
- '.lists-*'
- '.siem-signals*'
privileges:
- 'maintenance'
- 'read'
- 'view_index_metadata'
- 'write'
allow_restricted_indices: false
- names:
- '/~(([.]|ilm-history-).*)/'
privileges:
- 'read'
- 'view_index_metadata'
allow_restricted_indices: false
- names:
- '.profiling-*'
- 'profiling-*'
privileges:
- 'read'
- 'view_index_metadata'
allow_restricted_indices: false
- names:
- '.alerts*'
- '.internal.alerts*'
- '.internal.preview.alerts*'
- '.preview.alerts*'
privileges:
- 'maintenance'
- 'read'
- 'view_index_metadata'
- 'write'
allow_restricted_indices: false
applications:
- application: 'kibana-.kibana'
privileges:
- 'all'
resources:
- '*'
run_as: []
# Admin role without 'remote_indices' access definition
# There is no such built-in role in stateful, and it's a role "similar" to the built-in 'admin' role in serverless
admin:
# TODO: 'all' should be replaced with explicit list both here and serverless for deployment-agnostic tests with 'admin' role to be compatible
cluster: ['all']
indices:
- names: ['*']
privileges: ['all']
allow_restricted_indices: false
- names: ['*']
privileges:
- 'monitor'
- 'read'
- 'read_cross_cluster'
- 'view_index_metadata'
allow_restricted_indices: true
applications:
- application: '*'
privileges: ['*']
resources: ['*']
run_as: ['*']
# temporarily added for testing purpose
system_indices_superuser:
cluster: ['all']
indices:
- names: ['*']
privileges: ['all']
allow_restricted_indices: true
applications:
- application: '*'
privileges: ['*']
resources: ['*']
run_as: ['*']

View file

@ -23,4 +23,6 @@ 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 { FtrProviderContext } from './services/ftr_provider_context';

View file

@ -11,6 +11,7 @@ import { EsProvider } from './es';
import { KibanaServerProvider } from './kibana_server';
import { RetryService } from './retry';
import { SupertestWithoutAuthProvider } from './supertest_without_auth';
import { SamlAuthProvider } from './saml_auth';
export const services = {
es: EsProvider,
@ -18,4 +19,5 @@ export const services = {
esArchiver: EsArchiverProvider,
retry: RetryService,
supertestWithoutAuth: SupertestWithoutAuthProvider,
samlAuth: SamlAuthProvider,
};

View file

@ -0,0 +1,33 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const COMMON_REQUEST_HEADERS = {
'kbn-xsrf': 'some-xsrf-token',
};
// possible change in 9.0 to match serverless
const STATEFUL_INTERNAL_REQUEST_HEADERS = {
...COMMON_REQUEST_HEADERS,
};
const SERVERLESS_INTERNAL_REQUEST_HEADERS = {
...COMMON_REQUEST_HEADERS,
'x-elastic-internal-origin': 'kibana',
};
export type InternalRequestHeader =
| typeof STATEFUL_INTERNAL_REQUEST_HEADERS
| typeof SERVERLESS_INTERNAL_REQUEST_HEADERS;
export const getServerlessInternalRequestHeaders = (): InternalRequestHeader => {
return SERVERLESS_INTERNAL_REQUEST_HEADERS;
};
export const getStatefulInternalRequestHeaders = (): InternalRequestHeader => {
return STATEFUL_INTERNAL_REQUEST_HEADERS;
};

View file

@ -0,0 +1,53 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import fs from 'fs';
import { type Config } from '@kbn/test';
import { ToolingLog } from '@kbn/tooling-log';
import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils';
import { KibanaServer } from '../..';
import { ServerlessAuthProvider } from './serverless/auth_provider';
import { StatefulAuthProvider } from './stateful/auth_provider';
import { createRole, createRoleMapping } from './stateful/create_role_mapping';
const STATEFUL_ADMIN_ROLE_MAPPING_PATH = './stateful/admin_mapping';
export interface AuthProvider {
getSupportedRoleDescriptors(): any;
getDefaultRole(): string;
getRolesDefinitionPath(): string;
getCommonRequestHeader(): { [key: string]: string };
getInternalRequestHeader(): { [key: string]: string };
}
export interface AuthProviderProps {
config: Config;
kibanaServer: KibanaServer;
log: ToolingLog;
}
export const getAuthProvider = async (props: AuthProviderProps) => {
const { config, log, kibanaServer } = props;
const isServerless = !!props.config.get('serverless');
if (isServerless) {
return new ServerlessAuthProvider(config);
}
const provider = new StatefulAuthProvider();
// TODO: Move it to @kbn-es package, so that roles and its mapping are created before FTR services loading starts.
// 'viewer' and 'editor' roles are available by default, but we have to create 'admin' role
const adminRoleMapping = JSON.parse(
fs.readFileSync(require.resolve(STATEFUL_ADMIN_ROLE_MAPPING_PATH), 'utf8')
);
await createRole({ roleName: 'admin', roleMapping: adminRoleMapping, kibanaServer, log });
const roles = Object.keys(provider.getSupportedRoleDescriptors());
// Creating roles mapping for mock-idp
await createRoleMapping({ name: MOCK_IDP_REALM_NAME, roles, config, log });
return provider;
};

View file

@ -0,0 +1,10 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { SamlAuthProvider } from './saml_auth_provider';
export type { RoleCredentials } from './saml_auth_provider';
export type { InternalRequestHeader } from './default_request_headers';

View file

@ -1,18 +1,18 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ServerlessProjectType, SERVERLESS_ROLES_ROOT_PATH } from '@kbn/es';
import { SamlSessionManager } from '@kbn/test';
import { readRolesDescriptorsFromResource } from '@kbn/es';
import { resolve } from 'path';
import { Role } from '@kbn/test/src/auth/types';
import { isServerlessProjectType } from '@kbn/es/src/utils';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../functional/ftr_provider_context';
import { REPO_ROOT } from '@kbn/repo-info';
import { resolve } from 'path';
import { FtrProviderContext } from '../ftr_provider_context';
import { getAuthProvider } from './get_auth_provider';
import { InternalRequestHeader } from './default_request_headers';
export interface RoleCredentials {
apiKey: { id: string; name: string };
@ -20,63 +20,41 @@ export interface RoleCredentials {
cookieHeader: { Cookie: string };
}
export function SvlUserManagerProvider({ getService }: FtrProviderContext) {
export async function SamlAuthProvider({ getService }: FtrProviderContext) {
const config = getService('config');
const log = getService('log');
const svlCommonApi = getService('svlCommonApi');
const kibanaServer = getService('kibanaServer');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const isCloud = !!process.env.TEST_CLOUD;
const kbnServerArgs = config.get('kbnTestServer.serverArgs') as string[];
const projectType = kbnServerArgs
.filter((arg) => arg.startsWith('--serverless'))
.reduce((acc, arg) => {
const match = arg.match(/--serverless[=\s](\w+)/);
return acc + (match ? match[1] : '');
}, '') as ServerlessProjectType;
if (!isServerlessProjectType(projectType)) {
throw new Error(`Unsupported serverless projectType: ${projectType}`);
}
const supportedRoleDescriptors = readRolesDescriptorsFromResource(
resolve(SERVERLESS_ROLES_ROOT_PATH, projectType, 'roles.yml')
);
const authRoleProvider = await getAuthProvider({ config, kibanaServer, log });
const supportedRoleDescriptors = authRoleProvider.getSupportedRoleDescriptors();
const supportedRoles = Object.keys(supportedRoleDescriptors);
const defaultRolesToMap = new Map<string, Role>([
['es', 'developer'],
['security', 'editor'],
['oblt', 'editor'],
]);
const getDefaultRole = () => {
if (defaultRolesToMap.has(projectType)) {
return defaultRolesToMap.get(projectType)!;
} else {
throw new Error(`Default role is not defined for ${projectType} project`);
}
};
const customRolesFileName: string | undefined = process.env.ROLES_FILENAME_OVERRIDE;
// Sharing the instance within FTR config run means cookies are persistent for each role between tests.
const sessionManager = new SamlSessionManager(
{
hostOptions: {
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
port: isCloud ? undefined : config.get('servers.kibana.port'),
username: config.get('servers.kibana.username'),
password: config.get('servers.kibana.password'),
},
log,
isCloud,
supportedRoles,
},
customRolesFileName
);
const cloudUsersFilePath = resolve(REPO_ROOT, '.ftr', customRolesFileName ?? 'role_users.json');
const DEFAULT_ROLE = getDefaultRole();
// Sharing the instance within FTR config run means cookies are persistent for each role between tests.
const sessionManager = new SamlSessionManager({
hostOptions: {
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
port: isCloud ? undefined : config.get('servers.kibana.port'),
username: config.get('servers.kibana.username'),
password: config.get('servers.kibana.password'),
},
log,
isCloud,
supportedRoles: {
roles: supportedRoles,
sourcePath: authRoleProvider.getRolesDefinitionPath(),
},
cloudUsersFilePath,
});
const DEFAULT_ROLE = authRoleProvider.getDefaultRole();
const COMMON_REQUEST_HEADERS = authRoleProvider.getCommonRequestHeader();
const INTERNAL_REQUEST_HEADERS = authRoleProvider.getInternalRequestHeader();
return {
async getInteractiveUserSessionCookieWithRoleScope(role: string) {
@ -119,7 +97,7 @@ export function SvlUserManagerProvider({ getService }: FtrProviderContext) {
const { body, status } = await supertestWithoutAuth
.post('/internal/security/api_key')
.set(svlCommonApi.getInternalRequestHeader())
.set(INTERNAL_REQUEST_HEADERS)
.set(adminCookieHeader)
.send({
name: 'myTestApiKey',
@ -147,12 +125,19 @@ export function SvlUserManagerProvider({ getService }: FtrProviderContext) {
const { status } = await supertestWithoutAuth
.post('/internal/security/api_key/invalidate')
.set(svlCommonApi.getInternalRequestHeader())
.set(INTERNAL_REQUEST_HEADERS)
.set(roleCredentials.cookieHeader)
.send(requestBody);
expect(status).to.be(200);
},
getCommonRequestHeader() {
return COMMON_REQUEST_HEADERS;
},
getInternalRequestHeader(): InternalRequestHeader {
return INTERNAL_REQUEST_HEADERS;
},
DEFAULT_ROLE,
};
}

View file

@ -0,0 +1,67 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ServerlessProjectType, SERVERLESS_ROLES_ROOT_PATH } from '@kbn/es';
import { type Config } from '@kbn/test';
import { isServerlessProjectType, readRolesDescriptorsFromResource } from '@kbn/es/src/utils';
import { resolve } from 'path';
import { Role } from '@kbn/test/src/auth/types';
import {
getServerlessInternalRequestHeaders,
COMMON_REQUEST_HEADERS,
} from '../default_request_headers';
import { AuthProvider } from '../get_auth_provider';
const projectDefaultRoles = new Map<string, Role>([
['es', 'developer'],
['security', 'editor'],
['oblt', 'editor'],
]);
const getDefaultServerlessRole = (projectType: string) => {
if (projectDefaultRoles.has(projectType)) {
return projectDefaultRoles.get(projectType)!;
} else {
throw new Error(`Default role is not defined for ${projectType} project`);
}
};
export class ServerlessAuthProvider implements AuthProvider {
private readonly projectType: string;
private readonly rolesDefinitionPath: string;
constructor(config: Config) {
const kbnServerArgs = config.get('kbnTestServer.serverArgs') as string[];
this.projectType = kbnServerArgs.reduce((acc, arg) => {
const match = arg.match(/--serverless[=\s](\w+)/);
return acc + (match ? match[1] : '');
}, '') as ServerlessProjectType;
if (!isServerlessProjectType(this.projectType)) {
throw new Error(`Unsupported serverless projectType: ${this.projectType}`);
}
this.rolesDefinitionPath = resolve(SERVERLESS_ROLES_ROOT_PATH, this.projectType, 'roles.yml');
}
getSupportedRoleDescriptors(): any {
return readRolesDescriptorsFromResource(this.rolesDefinitionPath);
}
getDefaultRole(): string {
return getDefaultServerlessRole(this.projectType);
}
getRolesDefinitionPath(): string {
return this.rolesDefinitionPath;
}
getCommonRequestHeader() {
return COMMON_REQUEST_HEADERS;
}
getInternalRequestHeader() {
return getServerlessInternalRequestHeaders();
}
}

View file

@ -0,0 +1,46 @@
{
"kibana":[
{
"base":[
"all"
],
"feature":{
},
"spaces":[
"*"
]
}
],
"elasticsearch":{
"cluster":[
"all"
],
"indices":[
{
"names":[
"*"
],
"privileges":[
"all"
],
"allow_restricted_indices":false
},
{
"names":[
"*"
],
"privileges":[
"monitor",
"read",
"read_cross_cluster",
"view_index_metadata"
],
"allow_restricted_indices":true
}
],
"run_as":[
]
}
}

View file

@ -0,0 +1,37 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { readRolesDescriptorsFromResource, STATEFUL_ROLES_ROOT_PATH } from '@kbn/es';
import { REPO_ROOT } from '@kbn/repo-info';
import { resolve } from 'path';
import { AuthProvider } from '../get_auth_provider';
import {
getStatefulInternalRequestHeaders,
COMMON_REQUEST_HEADERS,
} from '../default_request_headers';
export class StatefulAuthProvider implements AuthProvider {
private readonly rolesDefinitionPath = resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH, 'roles.yml');
getSupportedRoleDescriptors(): any {
return readRolesDescriptorsFromResource(this.rolesDefinitionPath);
}
getDefaultRole() {
return 'editor';
}
getRolesDefinitionPath() {
return this.rolesDefinitionPath;
}
getCommonRequestHeader() {
return COMMON_REQUEST_HEADERS;
}
getInternalRequestHeader() {
return getStatefulInternalRequestHeaders();
}
}

View file

@ -0,0 +1,52 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Config, createEsClientForFtrConfig } from '@kbn/test';
import { ToolingLog } from '@kbn/tooling-log';
import { KibanaServer } from '../../..';
export interface CreateRoleProps {
roleName: string;
roleMapping: string[];
kibanaServer: KibanaServer;
log: ToolingLog;
}
export interface CreateRoleMappingProps {
name: string;
roles: string[];
config: Config;
log: ToolingLog;
}
export async function createRole(props: CreateRoleProps) {
const { roleName, roleMapping, kibanaServer, log } = props;
log.debug(`Adding a role: ${roleName}`);
const { status, statusText } = await kibanaServer.request({
path: `/api/security/role/${roleName}`,
method: 'PUT',
body: roleMapping,
retries: 0,
});
if (status !== 204) {
throw new Error(`Expected status code of 204, received ${status} ${statusText}`);
}
}
export async function createRoleMapping(props: CreateRoleMappingProps) {
const { name, roles, config, log } = props;
log.debug(`Creating a role mapping: {realm.name: ${name}, roles: ${roles}}`);
const esClient = createEsClientForFtrConfig(config);
await esClient.security.putRoleMapping({
name,
roles,
enabled: true,
// @ts-ignore
rules: { field: { 'realm.name': name } },
});
}

View file

@ -14,7 +14,11 @@
"@kbn/core-saved-objects-server",
"@kbn/tooling-log",
"@kbn/es-archiver",
"@kbn/test"
"@kbn/test",
"@kbn/expect",
"@kbn/repo-info",
"@kbn/es",
"@kbn/mock-idp-utils"
],
"exclude": [
"target/**/*",

View file

@ -10,12 +10,13 @@ import * as fs from 'fs';
import { Role, User } from './types';
export const readCloudUsersFromFile = (filePath: string): Array<[Role, User]> => {
const defaultMessage = `Cannot read roles and email/password from ${filePath}`;
if (!fs.existsSync(filePath)) {
throw new Error(`Please define user roles with email/password in ${filePath}`);
throw new Error(`${defaultMessage}: file does not exist`);
}
const data = fs.readFileSync(filePath, 'utf8');
if (data.length === 0) {
throw new Error(`'${filePath}' is empty: no roles are defined`);
throw new Error(`${defaultMessage}: file is empty`);
}
return Object.entries(JSON.parse(data)) as Array<[Role, User]>;

View file

@ -6,10 +6,7 @@
* Side Public License, v 1.
*/
import { SERVERLESS_ROLES_ROOT_PATH } from '@kbn/es';
import { REPO_ROOT } from '@kbn/repo-info';
import { ToolingLog } from '@kbn/tooling-log';
import { resolve } from 'path';
import Url from 'url';
import { KbnClient } from '../kbn_client';
import { readCloudUsersFromFile } from './helper';
@ -32,31 +29,32 @@ export interface HostOptions {
export interface SamlSessionManagerOptions {
hostOptions: HostOptions;
isCloud: boolean;
supportedRoles?: string[];
supportedRoles?: SupportedRoles;
cloudUsersFilePath: string;
log: ToolingLog;
}
export interface SupportedRoles {
sourcePath: string;
roles: string[];
}
/**
* Manages cookies associated with user roles
*/
export class SamlSessionManager {
private readonly DEFAULT_ROLES_FILE_NAME: string = 'role_users.json';
private readonly isCloud: boolean;
private readonly kbnHost: string;
private readonly kbnClient: KbnClient;
private readonly log: ToolingLog;
private readonly roleToUserMap: Map<Role, User>;
private readonly sessionCache: Map<Role, Session>;
private readonly supportedRoles: string[];
private readonly userRoleFilePath: string;
private readonly supportedRoles?: SupportedRoles;
private readonly cloudUsersFilePath: string;
constructor(options: SamlSessionManagerOptions, rolesFilename?: string) {
constructor(options: SamlSessionManagerOptions) {
this.isCloud = options.isCloud;
this.log = options.log;
// if the rolesFilename is provided, respect it. Otherwise use DEFAULT_ROLES_FILE_NAME.
const rolesFile = rolesFilename ? rolesFilename : this.DEFAULT_ROLES_FILE_NAME;
this.log.info(`Using the file ${rolesFile} for the role users`);
this.userRoleFilePath = resolve(REPO_ROOT, '.ftr', rolesFile);
const hostOptionsWithoutAuth = {
protocol: options.hostOptions.protocol,
hostname: options.hostOptions.hostname,
@ -70,9 +68,10 @@ export class SamlSessionManager {
auth: `${options.hostOptions.username}:${options.hostOptions.password}`,
}),
});
this.cloudUsersFilePath = options.cloudUsersFilePath;
this.sessionCache = new Map<Role, Session>();
this.roleToUserMap = new Map<Role, User>();
this.supportedRoles = options.supportedRoles ?? [];
this.supportedRoles = options.supportedRoles;
}
/**
@ -81,7 +80,8 @@ export class SamlSessionManager {
*/
private getCloudUsers = () => {
if (this.roleToUserMap.size === 0) {
const data = readCloudUsersFromFile(this.userRoleFilePath);
this.log.info(`Reading cloud user credentials from ${this.cloudUsersFilePath}`);
const data = readCloudUsersFromFile(this.cloudUsersFilePath);
for (const [roleName, user] of data) {
this.roleToUserMap.set(roleName, user);
}
@ -104,11 +104,11 @@ export class SamlSessionManager {
}
// Validate role before creating SAML session
if (this.supportedRoles.length && !this.supportedRoles.includes(role)) {
if (this.supportedRoles && !this.supportedRoles.roles.includes(role)) {
throw new Error(
`Role '${role}' is not defined in the supported list: ${this.supportedRoles.join(
`Role '${role}' is not in the supported list: ${this.supportedRoles.roles.join(
', '
)}. Update roles resource file in ${SERVERLESS_ROLES_ROOT_PATH} to enable it for testing`
)}. Add role descriptor in ${this.supportedRoles.sourcePath} to enable it for testing`
);
}

View file

@ -9,17 +9,23 @@
import { ToolingLog } from '@kbn/tooling-log';
import { Cookie } from 'tough-cookie';
import { Session } from './saml_auth';
import { SamlSessionManager } from './session_manager';
import { SamlSessionManager, SupportedRoles } from './session_manager';
import * as samlAuth from './saml_auth';
import * as helper from './helper';
import { Role, User, UserProfile } from './types';
import { SERVERLESS_ROLES_ROOT_PATH } from '@kbn/es';
import { resolve } from 'path';
import { REPO_ROOT } from '@kbn/repo-info';
const log = new ToolingLog();
const supportedRoles = ['admin', 'editor', 'viewer'];
const supportedRoles: SupportedRoles = {
roles: ['admin', 'editor', 'viewer'],
sourcePath: 'test/roles.yml',
};
const roleViewer = 'viewer';
const roleEditor = 'editor';
const cloudUsersFilePath = resolve(REPO_ROOT, SERVERLESS_ROLES_ROOT_PATH, 'role_users.json');
const createLocalSAMLSessionMock = jest.spyOn(samlAuth, 'createLocalSAMLSession');
const createCloudSAMLSessionMock = jest.spyOn(samlAuth, 'createCloudSAMLSession');
@ -58,7 +64,7 @@ describe('SamlSessionManager', () => {
hostOptions,
isCloud,
log,
supportedRoles,
cloudUsersFilePath,
};
const testEmail = 'testuser@elastic.com';
const testFullname = 'Test User';
@ -67,7 +73,7 @@ describe('SamlSessionManager', () => {
)!;
test('should create an instance of SamlSessionManager', () => {
const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud });
const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions);
expect(samlSessionManager).toBeInstanceOf(SamlSessionManager);
});
@ -118,10 +124,13 @@ describe('SamlSessionManager', () => {
test(`throws error when role is not in 'supportedRoles'`, async () => {
const nonExistingRole = 'tester';
const expectedErrorMessage = `Role '${nonExistingRole}' is not defined in the supported list: ${supportedRoles.join(
const expectedErrorMessage = `Role '${nonExistingRole}' is not in the supported list: ${supportedRoles.roles.join(
', '
)}. Update roles resource file in ${SERVERLESS_ROLES_ROOT_PATH} to enable it for testing`;
const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions);
)}. Add role descriptor in ${supportedRoles.sourcePath} to enable it for testing`;
const samlSessionManager = new SamlSessionManager({
...samlSessionManagerOptions,
supportedRoles,
});
await expect(
samlSessionManager.getInteractiveUserSessionCookieWithRoleScope(nonExistingRole)
).rejects.toThrow(expectedErrorMessage);
@ -145,11 +154,7 @@ describe('SamlSessionManager', () => {
elastic_cloud_user: false,
};
getSecurityProfileMock.mockResolvedValueOnce(testData);
const samlSessionManager = new SamlSessionManager({
hostOptions,
log,
isCloud,
});
const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions);
await samlSessionManager.getInteractiveUserSessionCookieWithRoleScope(nonExistingRole);
await samlSessionManager.getApiCredentialsForRole(nonExistingRole);
await samlSessionManager.getUserData(nonExistingRole);
@ -171,7 +176,7 @@ describe('SamlSessionManager', () => {
hostOptions,
isCloud,
log,
supportedRoles,
cloudUsersFilePath,
};
const cloudCookieInstance = Cookie.parse(
'sid=cloud_cookie_value; Path=/; Expires=Wed, 01 Oct 2023 07:00:00 GMT'
@ -195,11 +200,7 @@ describe('SamlSessionManager', () => {
test('should throw error if TEST_CLOUD_HOST_NAME is not set', async () => {
isValidHostnameMock.mockReturnValueOnce(false);
const samlSessionManager = new SamlSessionManager({
hostOptions,
log,
isCloud,
});
const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions);
await expect(
samlSessionManager.getInteractiveUserSessionCookieWithRoleScope(roleViewer)
).rejects.toThrow(
@ -220,11 +221,7 @@ describe('SamlSessionManager', () => {
});
test('should create an instance of SamlSessionManager', () => {
const samlSessionManager = new SamlSessionManager({
hostOptions,
log,
isCloud,
});
const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions);
expect(samlSessionManager).toBeInstanceOf(SamlSessionManager);
});
@ -276,10 +273,13 @@ describe('SamlSessionManager', () => {
test(`throws error for non-existing role when 'supportedRoles' is defined`, async () => {
const nonExistingRole = 'tester';
const expectedErrorMessage = `Role '${nonExistingRole}' is not defined in the supported list: ${supportedRoles.join(
const expectedErrorMessage = `Role '${nonExistingRole}' is not in the supported list: ${supportedRoles.roles.join(
', '
)}. Update roles resource file in ${SERVERLESS_ROLES_ROOT_PATH} to enable it for testing`;
const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions);
)}. Add role descriptor in ${supportedRoles.sourcePath} to enable it for testing`;
const samlSessionManager = new SamlSessionManager({
...samlSessionManagerOptions,
supportedRoles,
});
await expect(
samlSessionManager.getInteractiveUserSessionCookieWithRoleScope(nonExistingRole)
).rejects.toThrow(expectedErrorMessage);
@ -294,11 +294,7 @@ describe('SamlSessionManager', () => {
test(`throws error for non-existing role when 'supportedRoles' is not defined`, async () => {
const nonExistingRole = 'tester';
const samlSessionManager = new SamlSessionManager({
hostOptions,
log,
isCloud,
});
const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions);
await expect(
samlSessionManager.getInteractiveUserSessionCookieWithRoleScope(nonExistingRole)
).rejects.toThrow(`User with '${nonExistingRole}' role is not defined`);

View file

@ -125,7 +125,8 @@ export async function runCheckFtrConfigsCli() {
const invalid = possibleConfigs.filter((path) => !allFtrConfigs.includes(path));
if (invalid.length) {
const invalidList = invalid.map((path) => Path.relative(REPO_ROOT, path)).join('\n - ');
const invalidList =
' - ' + invalid.map((path) => Path.relative(REPO_ROOT, path)).join('\n - ');
log.error(
`The following files look like FTR configs which are not listed in one of manifest files:\n${invalidList}\n
Make sure to add your new FTR config to the correct manifest file.\n

View file

@ -13,6 +13,5 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./proxy_route'));
loadTestFile(require.resolve('./autocomplete_entities'));
loadTestFile(require.resolve('./es_config'));
loadTestFile(require.resolve('./spec_definitions'));
});
}

View file

@ -1,30 +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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('GET /api/console/api_server', () => {
it('returns autocomplete definitions', async () => {
const { body } = await supertest
.get('/api/console/api_server')
.set('kbn-xsrf', 'true')
.expect(200);
expect(body.es).to.be.ok();
const {
es: { name, globals, endpoints },
} = body;
expect(name).to.be.ok();
expect(Object.keys(globals).length).to.be.above(0);
expect(Object.keys(endpoints).length).to.be.above(0);
});
});
}

View file

@ -1,60 +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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const compressionSuite = (url: string) => {
it(`uses compression when there isn't a referer`, async () => {
await supertest
.get(url)
.set('accept-encoding', 'gzip')
.then((response) => {
expect(response.header).to.have.property('content-encoding', 'gzip');
});
});
it(`uses compression when there is a whitelisted referer`, async () => {
await supertest
.get(url)
.set('accept-encoding', 'gzip')
.set('referer', 'https://some-host.com')
.then((response) => {
expect(response.header).to.have.property('content-encoding', 'gzip');
});
});
it(`doesn't use compression when there is a non-whitelisted referer`, async () => {
await supertest
.get(url)
.set('accept-encoding', 'gzip')
.set('referer', 'https://other.some-host.com')
.then((response) => {
expect(response.header).not.to.have.property('content-encoding');
});
});
it(`supports brotli compression`, async () => {
await supertest
.get(url)
.set('accept-encoding', 'br')
.then((response) => {
expect(response.header).to.have.property('content-encoding', 'br');
});
});
};
describe('compression', () => {
describe('against an application page', () => {
compressionSuite('/app/kibana');
});
});
}

View file

@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('core', () => {
loadTestFile(require.resolve('./compression'));
loadTestFile(require.resolve('./translations'));
loadTestFile(require.resolve('./capabilities'));
});

View file

@ -16,8 +16,15 @@ import { IndexPatternsService } from './index_patterns';
import { BsearchService } from './bsearch';
import { ConsoleProvider } from './console';
// pick only services that work for any FTR config, e.g. 'samlAuth' requires SAML setup in config file
const { es, esArchiver, kibanaServer, retry, supertestWithoutAuth } = commonFunctionalServices;
export const services = {
...commonFunctionalServices,
es,
esArchiver,
kibanaServer,
retry,
supertestWithoutAuth,
deployment: DeploymentService,
randomness: RandomnessService,
security: SecurityServiceProvider,

View file

@ -10,6 +10,7 @@ import { ToolingLog } from '@kbn/tooling-log';
import type { HostOptions } from '@kbn/test';
import { SamlSessionManager } from '@kbn/test';
import type { SecurityRoleName } from '../../../../common/test';
import { resolveCloudUsersFilePath } from '../../../../scripts/endpoint/common/roles_users/serverless';
export const samlAuthentication = async (
on: Cypress.PluginEvents,
@ -34,15 +35,15 @@ export const samlAuthentication = async (
role: string | SecurityRoleName
): Promise<{ cookie: string; username: string; password: string }> => {
// If config.env.PROXY_ORG is set, it means that proxy service is used to create projects. Define the proxy org filename to override the roles.
const rolesFilename = config.env.PROXY_ORG ? `${config.env.PROXY_ORG}.json` : undefined;
const sessionManager = new SamlSessionManager(
{
hostOptions,
log,
isCloud: config.env.CLOUD_SERVERLESS,
},
rolesFilename
);
const rolesFilename = config.env.PROXY_ORG
? `${config.env.PROXY_ORG}.json`
: 'role_users.json';
const sessionManager = new SamlSessionManager({
hostOptions,
log,
isCloud: config.env.CLOUD_SERVERLESS,
cloudUsersFilePath: resolveCloudUsersFilePath(rolesFilename),
});
return sessionManager.getInteractiveUserSessionCookieWithRoleScope(role).then((cookie) => {
return {
cookie,

View file

@ -7,6 +7,7 @@
import { resolve, join } from 'path';
import { readFileSync } from 'fs';
import { REPO_ROOT } from '@kbn/repo-info';
const ES_RESOURCES_DIR = resolve(__dirname, 'es_serverless_resources');
@ -16,6 +17,8 @@ export const ES_RESOURCES = Object.freeze({
users_roles: join(ES_RESOURCES_DIR, 'users_roles'),
});
export const resolveCloudUsersFilePath = (filename: string) => resolve(REPO_ROOT, '.ftr', filename);
export const ES_LOADED_USERS = readFileSync(ES_RESOURCES.users)
.toString()
.split(/\n/)

View file

@ -1,17 +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 { FtrConfigProviderContext } from '@kbn/test';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts'));
return {
...baseIntegrationTestsConfig.getAll(),
testFiles: [require.resolve('.')],
};
}

View file

@ -0,0 +1,183 @@
# Deployment-Agnostic Tests Guidelines
## Definition
A deployment-agnostic API integration test is a test suite that fulfills the following criteria:
**Functionality**: It tests Kibana APIs that are logically identical in both stateful and serverless environments for the same roles.
**Design**: The test design is clean and does not require additional logic to execute in either stateful or serverless environments.
## Tests Design Requirements
A deployment-agnostic test is contained within a single test file and always utilizes the [DeploymentAgnosticFtrProviderContext](https://github.com/elastic/kibana/blob/main/x-pack/test/api_integration/deployment_agnostic/ftr_provider_context.d.ts) to load compatible FTR services. A compatible FTR service must support:
- **Serverless**: Both local environments and MKI (Managed Kubernetes Infrastructure).
- **Stateful**: Both local environments and Cloud deployments.
To achieve this, services cannot use `supertest`, which employs an operator user for serverless and a system index superuser for stateful setups. Instead, services should use a combination of `supertestWithoutAuth` and `samlAuth` to generate an API key for user roles and make API calls. For example, see the [data_view_api.ts](https://github.com/elastic/kibana/blob/main/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts) service.
### How It Works
Most existing stateful tests use basic authentication for API testing. In contrast, serverless tests use SAML authentication with project-specific role mapping.
Since both Elastic Cloud (ESS) and Serverless rely on SAML authentication by default, and stateful deployments also support SAML, *deployment-agnostic tests configure Elasticsearch and Kibana with SAML authentication to use the same authentication approach in all cases*. For roles, stateful deployments define 'viewer', 'editor', and 'admin' roles with serverless-alike permissions.
### When to Create Separate Tests
While the deployment-agnostic testing approach is beneficial, it should not compromise the quality and simplicity of the tests. Here are some scenarios where separate test files are recommended:
- **Role-Specific Logic**: If API access or logic depends on roles that differ across deployments.
- **Environment Constraints**: If a test can only run locally and not on MKI or Cloud deployments.
- **Complex Logic**: If the test logic requires splitting across multiple locations.
## File Structure
We recommend following this structure to simplify maintenance and allow other teams to reuse code (e.g., FTR services) created by different teams:
```
x-pack/test/<my_own_api_integration_folder>
├─ deployment_agnostic
│ ├─ apis
│ │ ├─ <api_1>
│ │ │ ├─ <test_1_1>
│ │ │ ├─ <test_1_2>
│ │ ├─ <api_2>
│ │ │ ├─ <test_2_1>
│ │ │ ├─ <test_2_2>
│ ├─ services
│ │ ├─ index.ts // only services from 'x-pack/test/api_integration/deployment_agnostic/services'
│ │ ├─ <deployment_agnostic_service_1>.ts
│ │ ├─ <deployment_agnostic_service_2>.ts
│ ├─ ftr_provider_context.d.ts // with types of services from './services'
├─ stateful.index.ts
├─ stateful.config.ts
├─ <serverless_project>.index.ts // e.g., oblt.index.ts
├─ <serverless_project>.serverless.config.ts // e.g., oblt.serverless.config.ts
```
## Step-by-Step Guide
1. Define Deployment-Agnostic Services
Under `x-pack/test/<my_own_api_integration_folder>/deployment_agnostic/services`, create `index.ts` and load default services like `samlAuth` and `superuserWithoutAuth`:
```ts
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { services as deploymentAgnosticServices } from './../../api_integration/deployment_agnostic/services';
export type {
InternalRequestHeader,
RoleCredentials,
SupertestWithoutAuthProviderType,
} from '@kbn/ftr-common-functional-services';
export const services = {
...deploymentAgnosticServices,
// create a new deployment-agnostic service and load here
};
```
We suggest adding new services to `x-pack/test/api_integration/deployment_agnostic/services` so other teams can benefit from them.
2. Create `DeploymentAgnosticFtrProviderContext` with Services Defined in Step 2
Create `ftr_provider_context.d.ts` and export `DeploymentAgnosticFtrProviderContext`:
```ts
import { GenericFtrProviderContext } from '@kbn/test';
import { services } from './services';
export type DeploymentAgnosticFtrProviderContext = GenericFtrProviderContext<typeof services, {}>;
```
3. Add Tests
Add test files to `x-pack/test/<my_own_api_integration_folder>/deployment_agnostic/apis/<my_api>`:
test example
```ts
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const samlAuth = getService('samlAuth');
const supertestWithoutAuth = getService('supertestWithoutAuth');
let roleAuthc: RoleCredentials;
let internalHeaders: InternalRequestHeader;
describe('compression', () => {
before(async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
internalHeaders = samlAuth.getInternalRequestHeader();
});
after(async () => {
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
});
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);
expect(response.header).to.have.property('content-encoding', 'gzip');
});
});
});
}
```
Load all test files in `index.ts` under the same folder.
4. Add Tests Entry File and FTR Config File for **Stateful** Deployment
Create `stateful.index.ts` tests entry file and load tests:
```ts
import { DeploymentAgnosticFtrProviderContext } from './ftr_provider_context';
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
describe('apis', () => {
loadTestFile(require.resolve('./apis/<my_api>'));
});
}
```
Create `stateful.config.ts` and link tests entry file:
```ts
import { createStatefulTestConfig } from './../../api_integration/deployment_agnostic/default_configs/stateful.config.base';
export default createStatefulTestConfig({
testFiles: [require.resolve('./stateful.index.ts')],
junit: {
reportName: 'Stateful - Deployment-agnostic API Integration Tests',
},
// extra arguments
esServerArgs: [],
kbnServerArgs: [],
});
```
5. Add Tests Entry File and FTR Config File for Specific **Serverless** Project
Example for Observability project:
oblt.index.ts
```ts
import { DeploymentAgnosticFtrProviderContext } from './ftr_provider_context';
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
describe('Serverless Observability - Deployment-agnostic api integration tests', () => {
loadTestFile(require.resolve('./apis/<my_api>'));
});
}
```
oblt.serverless.config.ts
```ts
import { createServerlessTestConfig } from './../../api_integration/deployment_agnostic/default_configs/serverless.config.base';
export default createServerlessTestConfig({
serverlessProject: 'oblt',
testFiles: [require.resolve('./oblt.index.ts')],
junit: {
reportName: 'Serverless Observability - Deployment-agnostic API Integration Tests',
},
});
```
ES and Kibana project-specific arguments are defined and loaded from `serverless.config.base`. These arguments are copied from the Elasticsearch and Kibana controller repositories.
Note: The FTR (Functional Test Runner) does not have the capability to provision custom ES/Kibana server arguments into the serverless project. Any custom arguments listed explicitly in this config file will apply **only to a local environment**.
6. Add FTR Configs Path to FTR Manifest Files Located in `.buildkite/`

View file

@ -0,0 +1,14 @@
/*
* 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 { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
describe('console', () => {
loadTestFile(require.resolve('./spec_definitions'));
});
}

View file

@ -0,0 +1,42 @@
/*
* 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 expect from '@kbn/expect';
import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services';
import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const samlAuth = getService('samlAuth');
const supertestWithoutAuth = getService('supertestWithoutAuth');
let roleAuthc: RoleCredentials;
let internalHeaders: InternalRequestHeader;
describe('GET /api/console/api_server', () => {
before(async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
internalHeaders = samlAuth.getInternalRequestHeader();
});
after(async () => {
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
});
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);
expect(body.es).to.be.ok();
const {
es: { name, globals, endpoints },
} = body;
expect(name).to.be.ok();
expect(Object.keys(globals).length).to.be.above(0);
expect(Object.keys(endpoints).length).to.be.above(0);
});
});
}

View file

@ -0,0 +1,47 @@
/*
* 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 expect from '@kbn/expect';
import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services';
import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const samlAuth = getService('samlAuth');
const supertestWithoutAuth = getService('supertestWithoutAuth');
let roleAuthc: RoleCredentials;
let internalHeaders: InternalRequestHeader;
describe('compression', () => {
before(async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
internalHeaders = samlAuth.getInternalRequestHeader();
});
after(async () => {
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
});
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);
expect(response.header).to.have.property('content-encoding', 'gzip');
});
it(`uses compression when there is a whitelisted referer`, async () => {
const response = await supertestWithoutAuth
.get('/app/kibana')
.set('accept-encoding', 'gzip')
.set(internalHeaders)
.set('referer', 'https://some-host.com')
.set(roleAuthc.apiKeyHeader);
expect(response.header).to.have.property('content-encoding', 'gzip');
});
});
});
}

View file

@ -0,0 +1,14 @@
/*
* 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 { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
describe('core', () => {
loadTestFile(require.resolve('./compression'));
});
}

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
describe('Painless Lab', () => {
loadTestFile(require.resolve('./painless_lab'));
});

View file

@ -6,22 +6,34 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services';
import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
const API_BASE_PATH = '/api/painless_lab';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const samlAuth = getService('samlAuth');
const supertestWithoutAuth = getService('supertestWithoutAuth');
let roleAuthc: RoleCredentials;
let internalHeaders: InternalRequestHeader;
describe('Painless Lab', function () {
describe('Painless Lab Routes', function () {
before(async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
internalHeaders = samlAuth.getInternalRequestHeader();
});
after(async () => {
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
});
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 supertest
const { body } = await supertestWithoutAuth
.post(`${API_BASE_PATH}/execute`)
.set('kbn-xsrf', 'xxx')
.set(internalHeaders)
.set(roleAuthc.apiKeyHeader)
.set('Content-Type', 'application/json;charset=UTF-8')
.send(script)
.expect(200);
@ -35,10 +47,11 @@ export default function ({ getService }: FtrProviderContext) {
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 supertest
const { body } = await supertestWithoutAuth
.post(`${API_BASE_PATH}/execute`)
.set('kbn-xsrf', 'xxx')
.set(internalHeaders)
.set('Content-Type', 'application/json;charset=UTF-8')
.set(roleAuthc.apiKeyHeader)
.send(invalidScript)
.expect(200);

View file

@ -0,0 +1,90 @@
/*
* 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 { FtrConfigProviderContext, Config } from '@kbn/test';
import { ServerlessProjectType } from '@kbn/es';
import { services } from '../services';
interface CreateTestConfigOptions {
serverlessProject: ServerlessProjectType;
esServerArgs?: string[];
kbnServerArgs?: string[];
testFiles: string[];
junit: { reportName: string };
suiteTags?: { include?: string[]; exclude?: string[] };
}
// include settings from elasticsearch controller
// https://github.com/elastic/elasticsearch-controller/blob/main/helm/values.yaml
const esServerArgsFromController = {
es: [],
oblt: [
'xpack.apm_data.enabled=true',
// for ML, data frame analytics are not part of this project type
'xpack.ml.dfa.enabled=false',
],
security: [
'xpack.security.authc.api_key.cache.max_keys=70000',
'data_streams.lifecycle.retention.factory_default=365d',
'data_streams.lifecycle.retention.factory_max=365d',
],
};
// include settings from kibana controller
// https://github.com/elastic/kibana-controller/blob/main/internal/controllers/kibana/config/config_settings.go
const kbnServerArgsFromController = {
es: [
// useful for testing (also enabled in MKI QA)
'--coreApp.allowDynamicConfigOverrides=true',
],
oblt: [
'--coreApp.allowDynamicConfigOverrides=true',
// defined in MKI control plane
'--xpack.uptime.service.manifestUrl=mockDevUrl',
],
security: [
'--coreApp.allowDynamicConfigOverrides=true',
// disable fleet task that writes to metrics.fleet_server.* data streams, impacting functional tests
`--xpack.task_manager.unsafe.exclude_task_types=${JSON.stringify(['Fleet-Metrics-Task'])}`,
],
};
export function createServerlessTestConfig(options: CreateTestConfigOptions) {
return async ({ readConfigFile }: FtrConfigProviderContext): Promise<Config> => {
const svlSharedConfig = await readConfigFile(
require.resolve('@kbn/test-suites-serverless/shared/config.base')
);
return {
...svlSharedConfig.getAll(),
services: {
...services,
},
esTestCluster: {
...svlSharedConfig.get('esTestCluster'),
serverArgs: [
...svlSharedConfig.get('esTestCluster.serverArgs'),
...esServerArgsFromController[options.serverlessProject],
...(options.esServerArgs ?? []),
],
},
kbnTestServer: {
...svlSharedConfig.get('kbnTestServer'),
serverArgs: [
...svlSharedConfig.get('kbnTestServer.serverArgs'),
...kbnServerArgsFromController[options.serverlessProject],
`--serverless=${options.serverlessProject}`,
...(options.kbnServerArgs || []),
],
},
testFiles: options.testFiles,
junit: options.junit,
suiteTags: options.suiteTags,
};
};
}

View file

@ -0,0 +1,95 @@
/*
* 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 {
MOCK_IDP_REALM_NAME,
MOCK_IDP_ENTITY_ID,
MOCK_IDP_ATTRIBUTE_PRINCIPAL,
MOCK_IDP_ATTRIBUTE_ROLES,
MOCK_IDP_ATTRIBUTE_EMAIL,
MOCK_IDP_ATTRIBUTE_NAME,
} from '@kbn/mock-idp-utils';
import {
esTestConfig,
kbnTestConfig,
systemIndicesSuperuser,
FtrConfigProviderContext,
} from '@kbn/test';
import { services } from '../services';
interface CreateTestConfigOptions {
esServerArgs?: string[];
kbnServerArgs?: string[];
testFiles: string[];
junit: { reportName: string };
suiteTags?: { include?: string[]; exclude?: string[] };
}
export function createStatefulTestConfig(options: CreateTestConfigOptions) {
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../../config.ts'));
// TODO: move to kbn-es because currently metadata file has hardcoded entityID and Location
const idpPath = require.resolve(
'@kbn/security-api-integration-helpers/saml/idp_metadata_mock_idp.xml'
);
const servers = {
kibana: {
...kbnTestConfig.getUrlParts(systemIndicesSuperuser),
protocol: process.env.TEST_CLOUD ? 'https' : 'http',
},
elasticsearch: {
...esTestConfig.getUrlParts(),
protocol: process.env.TEST_CLOUD ? 'https' : 'http',
},
};
const kbnUrl = `${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`;
return {
servers,
testFiles: options.testFiles,
security: { disableTestUser: true },
services,
junit: options.junit,
suiteTags: options.suiteTags,
esTestCluster: {
...xPackAPITestsConfig.get('esTestCluster'),
serverArgs: [
...xPackAPITestsConfig.get('esTestCluster.serverArgs'),
...(options.esServerArgs ?? []),
'xpack.security.authc.token.enabled=true',
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.order=0`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.metadata.path=${idpPath}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.entity_id=${MOCK_IDP_ENTITY_ID}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.entity_id=${kbnUrl}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.acs=${kbnUrl}/api/security/saml/callback`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.logout=${kbnUrl}/logout`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.principal=${MOCK_IDP_ATTRIBUTE_PRINCIPAL}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.groups=${MOCK_IDP_ATTRIBUTE_ROLES}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.name=${MOCK_IDP_ATTRIBUTE_NAME}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.mail=${MOCK_IDP_ATTRIBUTE_EMAIL}`,
],
},
kbnTestServer: {
...xPackAPITestsConfig.get('kbnTestServer'),
serverArgs: [
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
...(options.kbnServerArgs || []),
'--xpack.security.authc.selector.enabled=false',
`--xpack.security.authc.providers=${JSON.stringify({
saml: { 'cloud-saml-kibana': { order: 0, realm: MOCK_IDP_REALM_NAME } },
basic: { 'cloud-basic': { order: 1 } },
})}`,
`--server.publicBaseUrl=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`,
],
},
};
};
}

View file

@ -0,0 +1,12 @@
/*
* 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 { GenericFtrProviderContext } from '@kbn/test';
import { services } from './services';
export type DeploymentAgnosticFtrProviderContext = GenericFtrProviderContext<typeof services, {}>;

View file

@ -0,0 +1,15 @@
/*
* 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 { DeploymentAgnosticFtrProviderContext } from './ftr_provider_context';
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
describe('Serverless Observability - Deployment-agnostic api integration tests', () => {
loadTestFile(require.resolve('./apis/console'));
loadTestFile(require.resolve('./apis/core'));
loadTestFile(require.resolve('./apis/painless_lab'));
});
}

View file

@ -0,0 +1,16 @@
/*
* 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 { createServerlessTestConfig } from './default_configs/serverless.config.base';
export default createServerlessTestConfig({
serverlessProject: 'oblt',
testFiles: [require.resolve('./oblt.index.ts')],
junit: {
reportName: 'Serverless Observability - Deployment-agnostic API Integration Tests',
},
});

View file

@ -0,0 +1,14 @@
/*
* 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 { DeploymentAgnosticFtrProviderContext } from './ftr_provider_context';
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
describe('Serverless Search - Deployment-agnostic api integration tests', () => {
loadTestFile(require.resolve('./apis/console'));
loadTestFile(require.resolve('./apis/core'));
});
}

View file

@ -0,0 +1,16 @@
/*
* 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 { createServerlessTestConfig } from './default_configs/serverless.config.base';
export default createServerlessTestConfig({
serverlessProject: 'es',
testFiles: [require.resolve('./search.index.ts')],
junit: {
reportName: 'Serverless Search - Deployment-agnostic API Integration Tests',
},
});

View file

@ -0,0 +1,15 @@
/*
* 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 { DeploymentAgnosticFtrProviderContext } from './ftr_provider_context';
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
describe('Security Search - Deployment-agnostic api integration tests', () => {
loadTestFile(require.resolve('./apis/console'));
loadTestFile(require.resolve('./apis/core'));
loadTestFile(require.resolve('./apis/painless_lab'));
});
}

View file

@ -0,0 +1,16 @@
/*
* 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 { createServerlessTestConfig } from './default_configs/serverless.config.base';
export default createServerlessTestConfig({
serverlessProject: 'security',
testFiles: [require.resolve('./security.index.ts')],
junit: {
reportName: 'Serverless Security - Deployment-agnostic API Integration Tests',
},
});

View file

@ -0,0 +1,66 @@
/*
* 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 } from '@kbn/ftr-common-functional-services';
import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context';
export function DataViewApiProvider({ getService }: DeploymentAgnosticFtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
const samlAuth = getService('samlAuth');
return {
async create({
roleAuthc,
id,
name,
title,
}: {
roleAuthc: RoleCredentials;
id: string;
name: string;
title: string;
}) {
const { body } = await supertestWithoutAuth
.post(`/api/content_management/rpc/create`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.set(samlAuth.getCommonRequestHeader())
.send({
contentTypeId: 'index-pattern',
data: {
fieldAttrs: '{}',
title,
timeFieldName: '@timestamp',
sourceFilters: '[]',
fields: '[]',
fieldFormatMap: '{}',
typeMeta: '{}',
runtimeFieldMap: '{}',
name,
},
options: { id },
version: 1,
});
return body;
},
async delete({ roleAuthc, id }: { roleAuthc: RoleCredentials; id: string }) {
const { body } = await supertestWithoutAuth
.post(`/api/content_management/rpc/create`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.set(samlAuth.getCommonRequestHeader())
.send({
contentTypeId: 'index-pattern',
id,
options: { force: true },
version: 1,
});
return body;
},
};
}

View file

@ -0,0 +1,28 @@
/*
* 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 _ from 'lodash';
import { services as apiIntegrationServices } from '../../services';
/**
* Load only services that support both stateful & serverless deployments (including Cloud/MKI),
* e.g. `randomness` or `retry` are deployment agnostic
*/
export const deploymentAgnosticServices = _.pick(apiIntegrationServices, [
'supertest', // TODO: review its behaviour
'es',
'esArchiver',
'esSupertest', // TODO: review its behaviour
'indexPatterns',
'ingestPipelines',
'kibanaServer',
// 'ml', depends on 'esDeleteAllIndices', can we make it deployment agnostic?
'randomness',
'retry',
'security',
'usageAPI',
]);

View file

@ -0,0 +1,26 @@
/*
* 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 { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { deploymentAgnosticServices } from './deployment_agnostic_services';
import { DataViewApiProvider } from './data_view_api';
import { SloApiProvider } from './slo_api';
export type {
InternalRequestHeader,
RoleCredentials,
SupertestWithoutAuthProviderType,
} from '@kbn/ftr-common-functional-services';
export const services = {
...deploymentAgnosticServices,
supertestWithoutAuth: commonFunctionalServices.supertestWithoutAuth,
samlAuth: commonFunctionalServices.samlAuth,
dataViewApi: DataViewApiProvider,
sloApi: SloApiProvider,
// create a new deployment-agnostic service and load here
};

View file

@ -0,0 +1,203 @@
/*
* 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 {
fetchHistoricalSummaryParamsSchema,
FetchHistoricalSummaryResponse,
} from '@kbn/slo-schema';
import * as t from 'io-ts';
import { RoleCredentials } from '@kbn/ftr-common-functional-services';
import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context';
type DurationUnit = 'm' | 'h' | 'd' | 'w' | 'M';
interface Duration {
value: number;
unit: DurationUnit;
}
interface WindowSchema {
id: string;
burnRateThreshold: number;
maxBurnRateThreshold: number;
longWindow: Duration;
shortWindow: Duration;
actionGroup: string;
}
interface Dependency {
ruleId: string;
actionGroupsToSuppressOn: string[];
}
export interface SloBurnRateRuleParams {
sloId: string;
windows: WindowSchema[];
dependencies?: Dependency[];
}
interface SloParams {
id?: string;
name: string;
description: string;
indicator: {
type: 'sli.kql.custom';
params: {
index: string;
good: string;
total: string;
timestampField: string;
};
};
timeWindow: {
duration: string;
type: string;
};
budgetingMethod: string;
objective: {
target: number;
};
groupBy: string;
}
type FetchHistoricalSummaryParams = t.OutputOf<
typeof fetchHistoricalSummaryParamsSchema.props.body
>;
interface SloRequestParams {
id: string;
roleAuthc: RoleCredentials;
}
export function SloApiProvider({ getService }: DeploymentAgnosticFtrProviderContext) {
const es = getService('es');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const samlAuth = getService('samlAuth');
const retry = getService('retry');
const config = getService('config');
const retryTimeout = config.get('timeouts.try');
const requestTimeout = 30 * 1000;
return {
async create(slo: SloParams, roleAuthc: RoleCredentials) {
const { body } = await supertestWithoutAuth
.post(`/api/observability/slos`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.send(slo);
return body;
},
async delete({ id, roleAuthc }: SloRequestParams) {
const response = await supertestWithoutAuth
.delete(`/api/observability/slos/${id}`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader());
return response;
},
async fetchHistoricalSummary(
params: FetchHistoricalSummaryParams,
roleAuthc: RoleCredentials
): Promise<FetchHistoricalSummaryResponse> {
const { body } = await supertestWithoutAuth
.post(`/internal/observability/slos/_historical_summary`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.send(params);
return body;
},
async waitForSloToBeDeleted({ id, roleAuthc }: SloRequestParams) {
return await retry.tryForTime(retryTimeout, async () => {
const response = await supertestWithoutAuth
.delete(`/api/observability/slos/${id}`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.timeout(requestTimeout);
if (!response.ok) {
throw new Error(`SLO with id '${id}' was not deleted`);
}
return response;
});
},
async waitForSloCreated({ id, roleAuthc }: SloRequestParams) {
return await retry.tryForTime(retryTimeout, async () => {
const response = await supertestWithoutAuth
.get(`/api/observability/slos/${id}`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.timeout(requestTimeout);
if (response.body.id === undefined) {
throw new Error(`No SLO with id '${id}' found`);
}
return response.body;
});
},
async waitForSloSummaryTempIndexToExist(index: string) {
return await retry.tryForTime(retryTimeout, async () => {
const indexExists = await es.indices.exists({ index, allow_no_indices: false });
if (!indexExists) {
throw new Error(`SLO summary index '${index}' should exist`);
}
return indexExists;
});
},
async getSloData({ sloId, indexName }: { sloId: string; indexName: string }) {
const response = await es.search({
index: indexName,
body: {
query: {
bool: {
filter: [{ term: { 'slo.id': sloId } }],
},
},
},
});
return response;
},
async waitForSloData({ id, indexName }: { id: string; indexName: string }) {
return await retry.tryForTime(retryTimeout, async () => {
const response = await es.search({
index: indexName,
body: {
query: {
bool: {
filter: [{ term: { 'slo.id': id } }],
},
},
},
});
if (response.hits.hits.length === 0) {
throw new Error(`No hits found at index '${indexName}' for slo id='${id}'`);
}
return response;
});
},
async deleteAllSLOs(roleAuthc: RoleCredentials) {
const response = await supertestWithoutAuth
.get(`/api/observability/slos/_definitions`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.send()
.expect(200);
await Promise.all(
response.body.results.map(({ id }: { id: string }) => {
return supertestWithoutAuth
.delete(`/api/observability/slos/${id}`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.send()
.expect(204);
})
);
},
};
}

View file

@ -0,0 +1,18 @@
/*
* 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 { createStatefulTestConfig } from './default_configs/stateful.config.base';
export default createStatefulTestConfig({
testFiles: [require.resolve('./stateful.index.ts')],
junit: {
reportName: 'Stateful - Deployment-agnostic API Integration Tests',
},
// extra arguments
esServerArgs: [],
kbnServerArgs: [],
});

View file

@ -0,0 +1,16 @@
/*
* 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 { DeploymentAgnosticFtrProviderContext } from './ftr_provider_context';
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
describe('apis', () => {
loadTestFile(require.resolve('./apis/console'));
loadTestFile(require.resolve('./apis/core'));
loadTestFile(require.resolve('./apis/painless_lab'));
});
}

View file

@ -11,8 +11,8 @@ import path from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT } from '@kbn/repo-info';
import { createFlagError } from '@kbn/dev-cli-errors';
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { v4 as uuidV4 } from 'uuid';
import { services } from './services';
import { ScalabilityTestRunner } from './runner';
import { FtrProviderContext } from './ftr_provider_context';
import { ScalabilityJourney } from './types';
@ -49,7 +49,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
return {
...baseConfig,
services: commonFunctionalServices,
services,
pageObjects: {},
testRunner: (context: FtrProviderContext) =>

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { GenericFtrProviderContext, GenericFtrService } from '@kbn/test';
import { services } from './services';
export type FtrProviderContext = GenericFtrProviderContext<typeof commonFunctionalServices, {}>;
export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>;
export class FtrService extends GenericFtrService<FtrProviderContext> {}

View file

@ -0,0 +1,17 @@
/*
* 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 { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
const { es, esArchiver, kibanaServer, retry, supertestWithoutAuth } = commonFunctionalServices;
export const services = {
es,
esArchiver,
kibanaServer,
retry,
supertestWithoutAuth,
};

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
entityID="urn:mock-idp">
<md:IDPSSODescriptor WantAuthnRequestsSigned="false"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<!-- This certificate is extracted from KBN_CERT_PATH in @kbn/dev-utils and should always be in sync with it -->
<ds:X509Certificate>MIIDYjCCAkqgAwIBAgIUZ2p8K7GMXGk6xwCS9S91BUl1JnAwDQYJKoZIhvcNAQEL
BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l
cmF0ZWQgQ0EwIBcNMjMwOTIzMTUyMDE0WhgPMjA3MzA5MTAxNTIwMTRaMBExDzAN
BgNVBAMTBmtpYmFuYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMOU
r52dbZ5dY0BoP2p7CEnOpG+qHTNrOAqZO/OJfniPMtpGmwAMl3WZDca6u2XkV2KE
qQyevQ2ADk6G3o8S2RU8mO/+UweuCDF7LHuSdxEGTpucidZErmVhEGUOFosL5UeB
AtIDWxvWwgK+W9Yzt5IEN2HzNCZ6h0dOSk2r9EjVMG5yF4Q6kuqOYxBT7jxoaOtO
OCrgBRummtUga4T13WZ/ZIyyHpXj2+JD4YEmrDyoTa7NLaphv0hnVhHXYoYBI/c6
2SwwAoBlmtDmlinwSACQ3o/8eLWk0tqkIP14rc3oFh3m7D2c3c2m2HXuyoSDMfGW
beG2IE1Q3idcGmeG3qsCAwEAAaOBjDCBiTAdBgNVHQ4EFgQUMOUM7w5jmIozDvnq
RpM779m5GigwHwYDVR0jBBgwFoAUMEwqwI5b0MYpNxwaHJ9Tw1Lp3p4wPAYDVR0R
BDUwM4IUaG9zdC5kb2NrZXIuaW50ZXJuYWyCCWxvY2FsaG9zdIIEZXMwM4IEZXMw
MoIEZXMwMTAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCxqvQYXSKqgpdl
SP4gXgwipAnYsoW9qkgWQODTvSBEzUdOWme0d3j7i2l6Ur/nVSv5YjkqAv1hf/yJ
Hrk9h+j29ZO/aQ/KDh5i/gTEUnPw3Bxbw47dfn23tjMWO7NCU1fr5HNztRsa/gQr
e9s07g25u/gTfTi9Fyu0lcRe3bXOLS/mFVcuC5oxuS65R9OlbIsiORkZ2EfwuNUf
wAAYOGPIjM2VlQCvBitefsd/SzRKHdxSPy6KSjkO6MGEGo87fr7B7Nx1qp1DVrK7
q9XeP1Cuygjg9WTcnsvWvNw8CssyuFM6X/3tGjpPasXwLvNUoG2AairK2AYTWhvS
foE31cFg</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:5620/mock_idp/logout"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://localhost:5620/mock_idp/logout"/>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:5620/mock_idp/login"/>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://localhost:5620/mock_idp/login"/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { SvlUserManagerProvider } from '@kbn/test-suites-serverless/shared/services/svl_user_manager';
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { SvlCommonApiServiceProvider } from '@kbn/test-suites-serverless/shared/services/svl_common_api';
import { services as essServices } from '../ess/services_edr_workflows';
import { SecuritySolutionServerlessSuperTest } from '../services/security_solution_serverless_supertest';
@ -15,6 +15,6 @@ export const svlServices = {
...essServices,
supertest: SecuritySolutionServerlessSuperTest,
securitySolutionUtils: SecuritySolutionServerlessUtils,
svlUserManager: SvlUserManagerProvider,
svlUserManager: commonFunctionalServices.samlAuth,
svlCommonApi: SvlCommonApiServiceProvider,
};

View file

@ -32,6 +32,8 @@ export function SecuritySolutionServerlessUtils({
});
async function invalidateApiKey(credentials: RoleCredentials) {
// load service to call it outside mocha context
await svlUserManager.init();
await svlUserManager.invalidateM2mApiKeyWithRoleScope(credentials);
}
@ -53,6 +55,8 @@ export function SecuritySolutionServerlessUtils({
const createSuperTest = async (role = 'admin') => {
cleanCredentials(role);
// load service to call it outside mocha context
await svlUserManager.init();
const credentials = await svlUserManager.createM2mApiKeyWithRoleScope(role);
rolesCredentials.set(role, credentials);
@ -62,6 +66,8 @@ export function SecuritySolutionServerlessUtils({
return {
getUsername: async (role = 'admin') => {
// load service to call it outside mocha context
await svlUserManager.init();
const { username } = await svlUserManager.getUserData(role);
return username;

View file

@ -9,6 +9,8 @@ import { ToolingLog } from '@kbn/tooling-log';
import { SecurityRoleName } from '@kbn/security-solution-plugin/common/test';
import { HostOptions, SamlSessionManager } from '@kbn/test';
import { REPO_ROOT } from '@kbn/repo-info';
import { resolve } from 'path';
import { DEFAULT_SERVERLESS_ROLE } from '../env_var_names_constants';
export const samlAuthentication = async (
@ -31,30 +33,27 @@ export const samlAuthentication = async (
// If config.env.PROXY_ORG is set, it means that proxy service is used to create projects. Define the proxy org filename to override the roles.
const rolesFilename = config.env.PROXY_ORG ? `${config.env.PROXY_ORG}.json` : undefined;
const cloudUsersFilePath = resolve(REPO_ROOT, '.ftr', rolesFilename ?? 'role_users.json');
on('task', {
getSessionCookie: async (role: string | SecurityRoleName): Promise<string> => {
const sessionManager = new SamlSessionManager(
{
hostOptions,
log,
isCloud: config.env.CLOUD_SERVERLESS,
},
rolesFilename
);
const sessionManager = new SamlSessionManager({
hostOptions,
log,
isCloud: config.env.CLOUD_SERVERLESS,
cloudUsersFilePath,
});
return sessionManager.getInteractiveUserSessionCookieWithRoleScope(role);
},
getFullname: async (
role: string | SecurityRoleName = DEFAULT_SERVERLESS_ROLE
): Promise<string> => {
const sessionManager = new SamlSessionManager(
{
hostOptions,
log,
isCloud: config.env.CLOUD_SERVERLESS,
},
rolesFilename
);
const sessionManager = new SamlSessionManager({
hostOptions,
log,
isCloud: config.env.CLOUD_SERVERLESS,
cloudUsersFilePath,
});
const { full_name: fullName } = await sessionManager.getUserData(role);
return fullName;
},

View file

@ -42,5 +42,6 @@
"@kbn/actions-plugin",
"@kbn/alerts-ui-shared",
"@kbn/securitysolution-endpoint-exceptions-common",
"@kbn/repo-info",
]
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { SvlUserManagerProvider } from '@kbn/test-suites-serverless/shared/services/svl_user_manager';
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { SvlCommonApiServiceProvider } from '@kbn/test-suites-serverless/shared/services/svl_common_api';
import { services as xPackFunctionalServices } from '../../functional/services';
import { IngestManagerProvider } from '../../common/services/ingest_manager';
@ -43,7 +43,7 @@ export const svlServices = {
supertestWithoutAuth: KibanaSupertestWithCertWithoutAuthProvider,
svlCommonApi: SvlCommonApiServiceProvider,
svlUserManager: SvlUserManagerProvider,
svlUserManager: commonFunctionalServices.samlAuth,
};
export type Services = typeof services | typeof svlServices;

View file

@ -27,5 +27,6 @@
"@kbn/ftr-common-functional-ui-services",
"@kbn/test",
"@kbn/test-subj-selector",
"@kbn/ftr-common-functional-services",
]
}

View file

@ -176,6 +176,7 @@
"@kbn/osquery-plugin",
"@kbn/entities-schema",
"@kbn/actions-simulators-plugin",
"@kbn/cases-api-integration-test-plugin"
"@kbn/cases-api-integration-test-plugin",
"@kbn/mock-idp-utils"
]
}

View file

@ -7,8 +7,7 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { RoleCredentials } from '../../../../shared/services';
import { InternalRequestHeader } from '../../../../shared/services/svl_common_api';
import { InternalRequestHeader, RoleCredentials } from '../../../../shared/services';
export default ({ getService }: FtrProviderContext) => {
const svlCommonApi = getService('svlCommonApi');

View file

@ -42,7 +42,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
it('allows to mutate the objects during an export', async () => {
await supertest
const resp = await supertest
.post('/api/saved_objects/_export')
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
@ -50,24 +50,23 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
type: ['test-export-transform'],
excludeExportDetails: true,
})
.expect(200)
.then((resp) => {
const objects = parseNdJson(resp.text);
expect(objects.map((obj) => ({ id: obj.id, enabled: obj.attributes.enabled }))).to.eql([
{
id: 'type_1-obj_1',
enabled: false,
},
{
id: 'type_1-obj_2',
enabled: false,
},
]);
});
.expect(200);
const objects = parseNdJson(resp.text);
expect(objects.map((obj) => ({ id: obj.id, enabled: obj.attributes.enabled }))).to.eql([
{
id: 'type_1-obj_1',
enabled: false,
},
{
id: 'type_1-obj_2',
enabled: false,
},
]);
});
it('allows to add additional objects to an export', async () => {
await supertest
const resp = await supertest
.post('/api/saved_objects/_export')
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
@ -80,15 +79,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
],
excludeExportDetails: true,
})
.expect(200)
.then((resp) => {
const objects = parseNdJson(resp.text);
expect(objects.map((obj) => obj.id)).to.eql(['type_2-obj_1', 'type_dep-obj_1']);
});
.expect(200);
const objects = parseNdJson(resp.text);
expect(objects.map((obj) => obj.id)).to.eql(['type_2-obj_1', 'type_dep-obj_1']);
});
it('allows to add additional objects to an export when exporting by type', async () => {
await supertest
const resp = await supertest
.post('/api/saved_objects/_export')
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
@ -96,20 +93,18 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
type: ['test-export-add'],
excludeExportDetails: true,
})
.expect(200)
.then((resp) => {
const objects = parseNdJson(resp.text);
expect(objects.map((obj) => obj.id)).to.eql([
'type_2-obj_1',
'type_2-obj_2',
'type_dep-obj_1',
'type_dep-obj_2',
]);
});
.expect(200);
const objects = parseNdJson(resp.text);
expect(objects.map((obj) => obj.id)).to.eql([
'type_2-obj_1',
'type_2-obj_2',
'type_dep-obj_1',
'type_dep-obj_2',
]);
});
it('returns a 400 when the type causes a transform error', async () => {
await supertest
const resp = await supertest
.post('/api/saved_objects/_export')
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
@ -117,21 +112,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
type: ['test-export-transform-error'],
excludeExportDetails: true,
})
.expect(400)
.then((resp) => {
const { attributes, ...error } = resp.body;
expect(error).to.eql({
error: 'Bad Request',
message: 'Error transforming objects to export',
statusCode: 400,
});
expect(attributes.cause).to.eql('Error during transform');
expect(attributes.objects.map((obj: any) => obj.id)).to.eql(['type_4-obj_1']);
});
.expect(400);
const { attributes, ...error } = resp.body;
expect(error).to.eql({
error: 'Bad Request',
message: 'Error transforming objects to export',
statusCode: 400,
});
expect(attributes.cause).to.eql('Error during transform');
expect(attributes.objects.map((obj: any) => obj.id)).to.eql(['type_4-obj_1']);
});
it('returns a 400 when the type causes an invalid transform', async () => {
await supertest
const resp = await supertest
.post('/api/saved_objects/_export')
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
@ -139,17 +132,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
type: ['test-export-invalid-transform'],
excludeExportDetails: true,
})
.expect(400)
.then((resp) => {
expect(resp.body).to.eql({
error: 'Bad Request',
message: 'Invalid transform performed on objects to export',
statusCode: 400,
attributes: {
objectKeys: ['test-export-invalid-transform|type_3-obj_1'],
},
});
});
.expect(400);
expect(resp.body).to.eql({
error: 'Bad Request',
message: 'Invalid transform performed on objects to export',
statusCode: 400,
attributes: {
objectKeys: ['test-export-invalid-transform|type_3-obj_1'],
},
});
});
});
@ -172,7 +163,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
it('execute export transforms for reference objects', async () => {
await supertest
const resp = await supertest
.post('/api/saved_objects/_export')
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
@ -186,21 +177,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
includeReferencesDeep: true,
excludeExportDetails: true,
})
.expect(200)
.then((resp) => {
const objects = parseNdJson(resp.text).sort((obj1, obj2) =>
obj1.id.localeCompare(obj2.id)
);
expect(objects.map((obj) => obj.id)).to.eql([
'type_1-obj_1',
'type_1-obj_2',
'type_2-obj_1',
'type_dep-obj_1',
]);
.expect(200);
const objects = parseNdJson(resp.text).sort((obj1, obj2) => obj1.id.localeCompare(obj2.id));
expect(objects.map((obj) => obj.id)).to.eql([
'type_1-obj_1',
'type_1-obj_2',
'type_2-obj_1',
'type_dep-obj_1',
]);
expect(objects[0].attributes.enabled).to.eql(false);
expect(objects[1].attributes.enabled).to.eql(false);
});
expect(objects[0].attributes.enabled).to.eql(false);
expect(objects[1].attributes.enabled).to.eql(false);
});
});
@ -223,7 +210,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
it('should only export objects returning `true` for `isExportable`', async () => {
await supertest
const resp = await supertest
.post('/api/saved_objects/_export')
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
@ -237,21 +224,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
includeReferencesDeep: true,
excludeExportDetails: true,
})
.expect(200)
.then((resp) => {
const objects = parseNdJson(resp.text).sort((obj1, obj2) =>
obj1.id.localeCompare(obj2.id)
);
expect(objects.map((obj) => `${obj.type}:${obj.id}`)).to.eql([
'test-is-exportable:1',
'test-is-exportable:3',
'test-is-exportable:5',
]);
});
.expect(200);
const objects = parseNdJson(resp.text).sort((obj1, obj2) => obj1.id.localeCompare(obj2.id));
expect(objects.map((obj) => `${obj.type}:${obj.id}`)).to.eql([
'test-is-exportable:1',
'test-is-exportable:3',
'test-is-exportable:5',
]);
});
it('lists objects that got filtered', async () => {
await supertest
const resp = await supertest
.post('/api/saved_objects/_export')
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
@ -265,31 +248,29 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
includeReferencesDeep: true,
excludeExportDetails: false,
})
.expect(200)
.then((resp) => {
const objects = parseNdJson(resp.text);
const exportDetails = objects[
objects.length - 1
] as unknown as SavedObjectsExportResultDetails;
.expect(200);
const objects = parseNdJson(resp.text);
const exportDetails = objects[
objects.length - 1
] as unknown as SavedObjectsExportResultDetails;
expect(exportDetails.excludedObjectsCount).to.eql(2);
expect(exportDetails.excludedObjects).to.eql([
{
type: 'test-is-exportable',
id: '2',
reason: 'excluded',
},
{
type: 'test-is-exportable',
id: '4',
reason: 'excluded',
},
]);
});
expect(exportDetails.excludedObjectsCount).to.eql(2);
expect(exportDetails.excludedObjects).to.eql([
{
type: 'test-is-exportable',
id: '2',
reason: 'excluded',
},
{
type: 'test-is-exportable',
id: '4',
reason: 'excluded',
},
]);
});
it('excludes objects if `isExportable` throws', async () => {
await supertest
const resp = await supertest
.post('/api/saved_objects/_export')
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
@ -307,24 +288,20 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
includeReferencesDeep: true,
excludeExportDetails: false,
})
.expect(200)
.then((resp) => {
const objects = parseNdJson(resp.text);
expect(objects.length).to.eql(2);
expect([objects[0]].map((obj) => `${obj.type}:${obj.id}`)).to.eql([
'test-is-exportable:5',
]);
const exportDetails = objects[
objects.length - 1
] as unknown as SavedObjectsExportResultDetails;
expect(exportDetails.excludedObjects).to.eql([
{
type: 'test-is-exportable',
id: 'error',
reason: 'predicate_error',
},
]);
});
.expect(200);
const objects = parseNdJson(resp.text);
expect(objects.length).to.eql(2);
expect([objects[0]].map((obj) => `${obj.type}:${obj.id}`)).to.eql(['test-is-exportable:5']);
const exportDetails = objects[
objects.length - 1
] as unknown as SavedObjectsExportResultDetails;
expect(exportDetails.excludedObjects).to.eql([
{
type: 'test-is-exportable',
id: 'error',
reason: 'predicate_error',
},
]);
});
});
});

View file

@ -44,37 +44,35 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await kibanaServer.savedObjects.cleanStandardList();
});
it('returns saved objects with importableAndExportable types', async () =>
await supertest
it('returns saved objects with importableAndExportable types', async () => {
const resp = await supertest
.get('/api/kibana/management/saved_objects/_find?type=test-hidden-importable-exportable')
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
.expect(200)
.then((resp) => {
expect(
resp.body.saved_objects.map((so: { id: string; type: string }) => ({
id: so.id,
type: so.type,
}))
).to.eql([
{
type: 'test-hidden-importable-exportable',
id: 'ff3733a0-9fty-11e7-ahb3-3dcb94193fab',
},
]);
}));
.expect(200);
expect(
resp.body.saved_objects.map((so: { id: string; type: string }) => ({
id: so.id,
type: so.type,
}))
).to.eql([
{
type: 'test-hidden-importable-exportable',
id: 'ff3733a0-9fty-11e7-ahb3-3dcb94193fab',
},
]);
});
it('returns empty response for non importableAndExportable types', async () =>
await supertest
it('returns empty response for non importableAndExportable types', async () => {
const resp = await supertest
.get(
'/api/kibana/management/saved_objects/_find?type=test-hidden-non-importable-exportable'
)
.set(svlCommonApi.getCommonRequestHeader())
.set(svlCommonApi.getInternalRequestHeader())
.expect(200)
.then((resp) => {
expect(resp.body.saved_objects).to.eql([]);
}));
.expect(200);
expect(resp.body.saved_objects).to.eql([]);
});
});
});
}

View file

@ -9,12 +9,13 @@ import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { SupertestProvider } from './supertest';
import { SvlCommonApiServiceProvider } from './svl_common_api';
import { SvlReportingServiceProvider } from './svl_reporting';
import { SvlUserManagerProvider } from './svl_user_manager';
import { DataViewApiProvider } from './data_view_api';
export type { RoleCredentials } from './svl_user_manager';
export type { InternalRequestHeader } from './svl_common_api';
export type { SupertestWithoutAuthProviderType } from '@kbn/ftr-common-functional-services';
export type {
InternalRequestHeader,
RoleCredentials,
SupertestWithoutAuthProviderType,
} from '@kbn/ftr-common-functional-services';
const SupertestWithoutAuthProvider = commonFunctionalServices.supertestWithoutAuth;
@ -23,6 +24,6 @@ export const services = {
supertestWithoutAuth: SupertestWithoutAuthProvider,
svlCommonApi: SvlCommonApiServiceProvider,
svlReportingApi: SvlReportingServiceProvider,
svlUserManager: SvlUserManagerProvider,
svlUserManager: commonFunctionalServices.samlAuth,
dataViewApi: DataViewApiProvider,
};

View file

@ -22,10 +22,11 @@ export type InternalRequestHeader = typeof INTERNAL_REQUEST_HEADERS;
export function SvlCommonApiServiceProvider({}: FtrProviderContext) {
return {
// call it from 'samlAuth' service when tests are migrated to deployment-agnostic
getCommonRequestHeader() {
return COMMON_REQUEST_HEADERS;
},
// call it from 'samlAuth' service when tests are migrated to deployment-agnostic
getInternalRequestHeader(): InternalRequestHeader {
return INTERNAL_REQUEST_HEADERS;
},

View file

@ -11,8 +11,8 @@ import type { ReportingJobResponse } from '@kbn/reporting-plugin/server/types';
import { REPORTING_DATA_STREAM_WILDCARD_WITH_LEGACY } from '@kbn/reporting-server';
import rison from '@kbn/rison';
import { FtrProviderContext } from '../../functional/ftr_provider_context';
import { RoleCredentials } from './svl_user_manager';
import { InternalRequestHeader } from './svl_common_api';
import { RoleCredentials } from '.';
import { InternalRequestHeader } from '.';
const API_HEADER: [string, string] = ['kbn-xsrf', 'reporting'];