mirror of
https://github.com/elastic/kibana.git
synced 2025-04-18 23:21:39 -04:00
[FTR] support "deployment agnostic" api-integration tests (#189853)
## Summary
### This PR introduces a new type of API integration tests in FTR:
deployment-agnostic

#### 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:
parent
f3aeb81fd6
commit
7df01e99c1
72 changed files with 1758 additions and 422 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
1
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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'
|
||||
|
|
130
packages/kbn-es/src/stateful_resources/roles.yml
Normal file
130
packages/kbn-es/src/stateful_resources/roles.yml
Normal 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: ['*']
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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":[
|
||||
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 } },
|
||||
});
|
||||
}
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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]>;
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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('.')],
|
||||
};
|
||||
}
|
183
x-pack/test/api_integration/deployment_agnostic/README.md
Normal file
183
x-pack/test/api_integration/deployment_agnostic/README.md
Normal 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/`
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
|
@ -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);
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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}`,
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
12
x-pack/test/api_integration/deployment_agnostic/ftr_provider_context.d.ts
vendored
Normal file
12
x-pack/test/api_integration/deployment_agnostic/ftr_provider_context.d.ts
vendored
Normal 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, {}>;
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
});
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
});
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
]);
|
|
@ -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
|
||||
};
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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: [],
|
||||
});
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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) =>
|
||||
|
|
|
@ -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> {}
|
||||
|
|
17
x-pack/test/scalability/services.ts
Normal file
17
x-pack/test/scalability/services.ts
Normal 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,
|
||||
};
|
|
@ -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>
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -42,5 +42,6 @@
|
|||
"@kbn/actions-plugin",
|
||||
"@kbn/alerts-ui-shared",
|
||||
"@kbn/securitysolution-endpoint-exceptions-common",
|
||||
"@kbn/repo-info",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -27,5 +27,6 @@
|
|||
"@kbn/ftr-common-functional-ui-services",
|
||||
"@kbn/test",
|
||||
"@kbn/test-subj-selector",
|
||||
"@kbn/ftr-common-functional-services",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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'];
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue