Serverless custom roles and multiple spaces tests (#191729)

## Summary

Implements serverless tests for custom roles and multiple spaces as part
of the feature flag test configs. We have implemented them here because
the feature flags for custom roles and multiple space are not yet
enabled in production.

These tests are minimal because when custom roles and custom spaces are
eventually be enabled in production, the full stateful test suites
should be adapted to deployment agnostic tests if possible.

### API Integration Tests
-
x-pack/test_serverless/api_integration/test_suites/common/management/multiple_spaces_enabled.ts
-
x-pack/test_serverless/api_integration/test_suites/common/platform_security/roles_routes_feature_flag.ts

## Functional Tests
-
x-pack/test_serverless/functional/test_suites/common/platform_security/roles.ts
-
x-pack/test_serverless/functional/test_suites/common/spaces/multiple_spaces_enabled.ts

## Flaky Test Runners
- API Integration Feature Flag tests:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6866
- Functional Feature Flag tests:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6867

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jeramy Soucy 2024-09-06 12:57:19 +02:00 committed by GitHub
parent d8445ccab6
commit 6bef58096f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1396 additions and 168 deletions

View file

@ -0,0 +1,274 @@
/*
* 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 'expect';
import { asyncForEach } from '@kbn/std';
import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integration/deployment_agnostic/services';
import { FtrProviderContext } from '../../../ftr_provider_context';
// Notes:
// This suite is currently only called from the feature flags test configs, e.g.
// x-pack/test_serverless/api_integration/test_suites/search/config.feature_flags.ts
// Configuration toggle:
// kbnServerArgs: ['--xpack.spaces.maxSpaces=100'],
//
// Initial test coverage limited to CRUD operations and ensuring disabling features/toggling feature visibility is not possible.
// Full coverage of x-pack/test/api_integration/apis/spaces & x-pack/test/spaces_api_integration
// should be converted into a deployment agnostic suite when spaces are
// permanently enabled in serverless.
//
// The route access tests for the spaces APIs in ./spaces.ts should also get updated at that time.
export default function ({ getService }: FtrProviderContext) {
const svlCommonApi = getService('svlCommonApi');
const roleScopedSupertest = getService('roleScopedSupertest');
let supertestWithAdminScope: SupertestWithRoleScopeType;
async function createSpace(id: string) {
await supertestWithAdminScope
.post('/api/spaces/space')
.send({
id,
name: id,
disabledFeatures: [],
})
.expect(200);
}
async function deleteSpace(id: string) {
await supertestWithAdminScope.delete(`/api/spaces/space/${id}`).expect(204);
}
describe('spaces', function () {
before(async () => {
supertestWithAdminScope = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withInternalHeaders: true,
withCustomHeaders: { 'kbn-xsrf': 'true' },
});
});
after(async () => {
// delete any lingering spaces
const { body } = await supertestWithAdminScope.get('/api/spaces/space').send().expect(200);
const toDelete = (body as Array<{ id: string }>).filter((f) => f.id !== 'default');
await asyncForEach(toDelete, async (space) => {
await deleteSpace(space.id);
});
});
describe('Create (POST /api/spaces/space)', () => {
it('should allow us to create a space', async () => {
await supertestWithAdminScope
.post('/api/spaces/space')
.send({
id: 'custom_space_1',
name: 'custom_space_1',
disabledFeatures: [],
})
.expect(200);
});
it('should not allow us to create a space with disabled features', async () => {
await supertestWithAdminScope
.post('/api/spaces/space')
.send({
id: 'custom_space_2',
name: 'custom_space_2',
disabledFeatures: ['discover'],
})
.expect(400);
});
});
describe('Read (GET /api/spaces/space)', () => {
before(async () => {
await createSpace('space_to_get_1');
await createSpace('space_to_get_2');
await createSpace('space_to_get_3');
});
after(async () => {
await deleteSpace('space_to_get_1');
await deleteSpace('space_to_get_2');
await deleteSpace('space_to_get_3');
});
it('should allow us to get a space', async () => {
await supertestWithAdminScope.get('/api/spaces/space/space_to_get_1').send().expect(200, {
id: 'space_to_get_1',
name: 'space_to_get_1',
disabledFeatures: [],
});
});
it('should allow us to get all spaces', async () => {
const { body } = await supertestWithAdminScope.get('/api/spaces/space').send().expect(200);
expect(body).toEqual(
expect.arrayContaining([
{
_reserved: true,
color: '#00bfb3',
description: 'This is your default space!',
disabledFeatures: [],
id: 'default',
name: 'Default',
},
{ id: 'space_to_get_1', name: 'space_to_get_1', disabledFeatures: [] },
{ id: 'space_to_get_2', name: 'space_to_get_2', disabledFeatures: [] },
{ id: 'space_to_get_3', name: 'space_to_get_3', disabledFeatures: [] },
])
);
});
});
describe('Update (PUT /api/spaces/space)', () => {
before(async () => {
await createSpace('space_to_update');
});
after(async () => {
await deleteSpace('space_to_update');
});
it('should allow us to update a space', async () => {
await supertestWithAdminScope
.put('/api/spaces/space/space_to_update')
.send({
id: 'space_to_update',
name: 'some new name',
initials: 'SN',
disabledFeatures: [],
})
.expect(200);
await supertestWithAdminScope.get('/api/spaces/space/space_to_update').send().expect(200, {
id: 'space_to_update',
name: 'some new name',
initials: 'SN',
disabledFeatures: [],
});
});
it('should not allow us to update a space with disabled features', async () => {
await supertestWithAdminScope
.put('/api/spaces/space/space_to_update')
.send({
id: 'space_to_update',
name: 'some new name',
initials: 'SN',
disabledFeatures: ['discover'],
})
.expect(400);
});
});
describe('Delete (DELETE /api/spaces/space)', () => {
it('should allow us to delete a space', async () => {
await createSpace('space_to_delete');
await supertestWithAdminScope.delete(`/api/spaces/space/space_to_delete`).expect(204);
});
});
describe('Get active space (GET /internal/spaces/_active_space)', () => {
before(async () => {
await createSpace('foo-space');
});
after(async () => {
await deleteSpace('foo-space');
});
it('returns the default space', async () => {
const response = await supertestWithAdminScope
.get('/internal/spaces/_active_space')
.expect(200);
const { id, name, _reserved } = response.body;
expect({ id, name, _reserved }).toEqual({
id: 'default',
name: 'Default',
_reserved: true,
});
});
it('returns the default space when explicitly referenced', async () => {
const response = await supertestWithAdminScope
.get('/s/default/internal/spaces/_active_space')
.expect(200);
const { id, name, _reserved } = response.body;
expect({ id, name, _reserved }).toEqual({
id: 'default',
name: 'Default',
_reserved: true,
});
});
it('returns the foo space', async () => {
await supertestWithAdminScope
.get('/s/foo-space/internal/spaces/_active_space')
.expect(200, {
id: 'foo-space',
name: 'foo-space',
disabledFeatures: [],
});
});
it('returns 404 when the space is not found', async () => {
await supertestWithAdminScope
.get('/s/not-found-space/internal/spaces/_active_space')
.expect(404, {
statusCode: 404,
error: 'Not Found',
message: 'Saved object [space/not-found-space] not found',
});
});
});
// These tests just test access to API endpoints
// They will be included in deployment agnostic testing once spaces
// are enabled in production.
describe(`Access`, () => {
it('#copyToSpace', async () => {
const { body, status } = await supertestWithAdminScope.post(
'/api/spaces/_copy_saved_objects'
);
svlCommonApi.assertResponseStatusCode(400, status, body);
});
it('#resolveCopyToSpaceErrors', async () => {
const { body, status } = await supertestWithAdminScope.post(
'/api/spaces/_resolve_copy_saved_objects_errors'
);
svlCommonApi.assertResponseStatusCode(400, status, body);
});
it('#updateObjectsSpaces', async () => {
const { body, status } = await supertestWithAdminScope.post(
'/api/spaces/_update_objects_spaces'
);
svlCommonApi.assertResponseStatusCode(400, status, body);
});
it('#getShareableReferences', async () => {
const { body, status } = await supertestWithAdminScope
.post('/api/spaces/_get_shareable_references')
.send({
objects: [{ type: 'a', id: 'a' }],
});
svlCommonApi.assertResponseStatusCode(200, status, body);
});
it('#disableLegacyUrlAliases', async () => {
const { body, status } = await supertestWithAdminScope.post(
'/api/spaces/_disable_legacy_url_aliases'
);
// without a request body we would normally a 400 bad request if the endpoint was registered
svlCommonApi.assertApiNotFound(body, status);
});
});
});
}

View file

@ -64,7 +64,7 @@ export default function ({ getService }: FtrProviderContext) {
it('create/update roleAuthc', async () => {
const { body, status } = await supertestWithoutAuth
.put('/api/security/roleAuthc/test')
.put('/api/security/role/test')
.set(svlCommonApi.getInternalRequestHeader())
.set(roleAuthc.apiKeyHeader);
svlCommonApi.assertApiNotFound(body, status);
@ -72,7 +72,7 @@ export default function ({ getService }: FtrProviderContext) {
it('get roleAuthc', async () => {
const { body, status } = await supertestWithoutAuth
.get('/api/security/roleAuthc/superuser')
.get('/api/security/role/superuser')
.set(svlCommonApi.getInternalRequestHeader())
.set(roleAuthc.apiKeyHeader);
svlCommonApi.assertApiNotFound(body, status);
@ -80,7 +80,7 @@ export default function ({ getService }: FtrProviderContext) {
it('get all roles', async () => {
const { body, status } = await supertestWithoutAuth
.get('/api/security/roleAuthc')
.get('/api/security/role')
.set(svlCommonApi.getInternalRequestHeader())
.set(roleAuthc.apiKeyHeader);
svlCommonApi.assertApiNotFound(body, status);
@ -88,7 +88,7 @@ export default function ({ getService }: FtrProviderContext) {
it('delete roleAuthc', async () => {
const { body, status } = await supertestWithoutAuth
.delete('/api/security/roleAuthc/superuser')
.delete('/api/security/role/superuser')
.set(svlCommonApi.getInternalRequestHeader())
.set(roleAuthc.apiKeyHeader);
svlCommonApi.assertApiNotFound(body, status);

View file

@ -32,9 +32,7 @@ export default function ({ getService }: FtrProviderContext) {
({ body, status } = await supertestWithoutAuth
.post('/api/encrypted_saved_objects/_rotate_key')
// .set(internalReqHeader)
.set(roleAuthc.apiKeyHeader));
// svlCommonApi.assertApiNotFound(body, status);
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,

View file

@ -5,57 +5,919 @@
* 2.0.
*/
import expect from '@kbn/expect';
import type { Role } from '@kbn/security-plugin-types-common';
import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integration/deployment_agnostic/services';
import { FtrProviderContext } from '../../../ftr_provider_context';
// Notes:
// Test coverage comes from stateful test suite: x-pack/test/api_integration/apis/security/roles.ts
// It has been modified to work for serverless by removing invalid options (run_as, allow_restricted_indices, etc).
//
// Note: this suite is currently only called from the feature flags test configs, e.g.
// x-pack/test_serverless/api_integration/test_suites/search/config.feature_flags.ts
//
// This suite should be converted into a deployment agnostic suite when the native roles
// feature flags are enabled permanently in serverless. Additionally, the route access tests
// for the roles APIs in authorization.ts should also get updated at that time.
// kbnServerArgs: ['--xpack.security.roleManagementEnabled=true'],
// esServerArgs: ['xpack.security.authc.native_roles.enabled=true'],
export default function ({ getService }: FtrProviderContext) {
const svlCommonApi = getService('svlCommonApi');
const supertest = getService('supertest');
const platformSecurityUtils = getService('platformSecurityUtils');
const roleScopedSupertest = getService('roleScopedSupertest');
let supertestWithAdminScope: SupertestWithRoleScopeType;
const es = getService('es');
describe('security', function () {
describe('route access', () => {
describe('roles', () => {
describe('enabled', () => {
it('get role', async () => {
const { body, status } = await supertest
.get('/api/security/role/superuser')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertResponseStatusCode(200, status, body);
});
describe('Roles', () => {
before(async () => {
supertestWithAdminScope = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withInternalHeaders: true,
withCustomHeaders: { 'kbn-xsrf': 'true' },
});
});
after(async () => {
await platformSecurityUtils.clearAllRoles();
});
it('get all roles', async () => {
const { body, status } = await supertest
.get('/api/security/role')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertResponseStatusCode(200, status, body);
describe('Create Role', () => {
it('should allow us to create an empty role', async () => {
await supertestWithAdminScope.put('/api/security/role/empty_role').send({}).expect(204);
});
it('should create a role with kibana and elasticsearch privileges', async () => {
await supertestWithAdminScope
.put('/api/security/role/role_with_privileges')
.send({
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
base: ['read'],
},
{
feature: {
dashboard: ['read'],
discover: ['all'],
ml: ['all'],
},
spaces: ['marketing', 'sales'],
},
],
})
.expect(204);
const role = await es.security.getRole({ name: 'role_with_privileges' });
expect(role).to.eql({
role_with_privileges: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
allow_restricted_indices: false,
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'kibana-.kibana',
privileges: ['feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'],
resources: ['space:marketing', 'space:sales'],
},
],
metadata: {
foo: 'test-metadata',
},
run_as: [],
transient_metadata: {
enabled: true,
},
},
});
});
describe('moved', () => {
it('delete role', async () => {
const { body, status } = await supertest
.delete('/api/security/role/superuser')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertResponseStatusCode(410, status, body);
});
it('create/update role', async () => {
const role = {
elasticsearch: {
cluster: [],
indices: [{ names: ['test'], privileges: ['read'] }],
run_as: [],
it(`should create a role with kibana and FLS/DLS elasticsearch privileges`, async () => {
await supertestWithAdminScope
.put('/api/security/role/role_with_privileges_dls_fls')
.send({
metadata: {
foo: 'test-metadata',
},
kibana: [],
};
elasticsearch: {
cluster: ['manage'],
indices: [
{
field_security: {
grant: ['*'],
except: ['geo.*'],
},
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
query: `{ "match": { "geo.src": "CN" } }`,
},
],
},
})
.expect(204);
});
const { body, status } = await supertest
.put('/api/security/role/myRole')
.send(role)
.set(svlCommonApi.getInternalRequestHeader());
// serverless only (stateful will allow)
it(`should not create a role with 'run as' privileges`, async () => {
await supertestWithAdminScope
.put('/api/security/role/role_with_privileges')
.send({
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
run_as: ['admin'],
},
kibana: [
{
base: ['read'],
},
{
feature: {
dashboard: ['read'],
discover: ['all'],
ml: ['all'],
},
spaces: ['marketing', 'sales'],
},
],
})
.expect(400);
});
svlCommonApi.assertResponseStatusCode(410, status, body);
// serverless only (stateful will allow)
it(`should not create a role with remote cluster privileges`, async () => {
await supertestWithAdminScope
.put('/api/security/role/role_with_privileges')
.send({
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
remote_cluster: [
{
clusters: ['remote_cluster1'],
privileges: ['monitor_enrich'],
},
],
},
kibana: [
{
base: ['read'],
},
{
feature: {
dashboard: ['read'],
discover: ['all'],
ml: ['all'],
},
spaces: ['marketing', 'sales'],
},
],
})
.expect(400);
});
// serverless only (stateful will allow)
it(`should not create a role with remote index privileges`, async () => {
await supertestWithAdminScope
.put('/api/security/role/role_with_privileges')
.send({
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
remote_indices: [
{
clusters: ['remote_cluster1'],
names: ['remote_index1', 'remote_index2'],
privileges: ['all'],
},
],
},
kibana: [
{
base: ['read'],
},
{
feature: {
dashboard: ['read'],
discover: ['all'],
ml: ['all'],
},
spaces: ['marketing', 'sales'],
},
],
})
.expect(400);
});
describe('with the createOnly option enabled', () => {
it('should fail when role already exists', async () => {
await es.security.putRole({
name: 'test_role',
body: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
},
],
},
});
await supertestWithAdminScope
.put('/api/security/role/test_role?createOnly=true')
.send({})
.expect(409);
});
it('should succeed when role does not exist', async () => {
await supertestWithAdminScope
.put('/api/security/role/new_role?createOnly=true')
.send({})
.expect(204);
});
});
});
describe('Read Role', () => {
it('should get roles', async () => {
await es.security.putRole({
name: 'role_to_get',
body: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'kibana-.kibana',
privileges: ['feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'],
resources: ['space:marketing', 'space:sales'],
},
{
application: 'apm',
privileges: ['apm-privilege'],
resources: ['*'],
},
],
metadata: {
foo: 'test-metadata',
},
transient_metadata: {
enabled: true,
},
},
});
await supertestWithAdminScope.get('/api/security/role/role_to_get').expect(200, {
name: 'role_to_get',
metadata: {
foo: 'test-metadata',
},
transient_metadata: { enabled: true },
elasticsearch: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
allow_restricted_indices: false,
},
],
run_as: [],
},
kibana: [
{
base: ['read'],
feature: {},
spaces: ['*'],
},
{
base: [],
feature: {
dashboard: ['read'],
discover: ['all'],
ml: ['all'],
},
spaces: ['marketing', 'sales'],
},
],
_transform_error: [],
_unrecognized_applications: ['apm'],
});
});
it('should get roles by space id', async () => {
await es.security.putRole({
name: 'space_role_not_to_get',
body: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'],
resources: ['space:marketing', 'space:sales'],
},
],
metadata: {
foo: 'test-metadata',
},
transient_metadata: {
enabled: true,
},
},
});
await es.security.putRole({
name: 'space_role_to_get',
body: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'],
resources: ['space:engineering', 'space:sales'],
},
],
metadata: {
foo: 'test-metadata',
},
transient_metadata: {
enabled: true,
},
},
});
await supertestWithAdminScope
.get('/internal/security/roles/engineering')
.expect(200)
.expect((res: { body: Role[] }) => {
const roles = res.body;
expect(roles).to.be.an('array');
const success = roles.every((role) => {
return (
role.name !== 'space_role_not_to_get' &&
role.kibana.some((privilege) => {
return (
privilege.spaces.includes('*') || privilege.spaces.includes('engineering')
);
})
);
});
const expectedRole = roles.find((role) => role.name === 'space_role_to_get');
expect(success).to.be(true);
expect(expectedRole).to.be.an('object');
});
});
});
describe('Update Role', () => {
it('should update a role with elasticsearch, kibana and other applications privileges', async () => {
await es.security.putRole({
name: 'role_to_update',
body: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'apm',
privileges: ['apm-privilege'],
resources: ['*'],
},
],
metadata: {
bar: 'old-metadata',
},
},
});
await supertestWithAdminScope
.put('/api/security/role/role_to_update')
.send({
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
feature: {
dashboard: ['read'],
dev_tools: ['all'],
},
spaces: ['*'],
},
{
base: ['all'],
spaces: ['marketing', 'sales'],
},
],
})
.expect(204);
const role = await es.security.getRole({ name: 'role_to_update' });
expect(role).to.eql({
role_to_update: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
allow_restricted_indices: false,
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['feature_dashboard.read', 'feature_dev_tools.all'],
resources: ['*'],
},
{
application: 'kibana-.kibana',
privileges: ['space_all'],
resources: ['space:marketing', 'space:sales'],
},
{
application: 'apm',
privileges: ['apm-privilege'],
resources: ['*'],
},
],
metadata: {
foo: 'test-metadata',
},
run_as: [],
transient_metadata: {
enabled: true,
},
},
});
});
it(`should update a role adding DLS and FLS privileges`, async () => {
await es.security.putRole({
name: 'role_to_update_with_dls_fls',
body: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
},
],
},
});
await supertestWithAdminScope
.put('/api/security/role/role_to_update_with_dls_fls')
.send({
elasticsearch: {
cluster: ['manage'],
indices: [
{
field_security: {
grant: ['*'],
except: ['geo.*'],
},
names: ['logstash-*'],
privileges: ['read'],
query: `{ "match": { "geo.src": "CN" } }`,
},
],
},
})
.expect(204);
const role = await es.security.getRole({ name: 'role_to_update_with_dls_fls' });
expect(role.role_to_update_with_dls_fls.cluster).to.eql(['manage']);
expect(role.role_to_update_with_dls_fls.indices[0].names).to.eql(['logstash-*']);
expect(role.role_to_update_with_dls_fls.indices[0].query).to.eql(
`{ "match": { "geo.src": "CN" } }`
);
});
// serverless only (stateful will allow)
it(`should not update a role with 'run as' privileges`, async () => {
await es.security.putRole({
name: 'role_to_update',
body: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'apm',
privileges: ['apm-privilege'],
resources: ['*'],
},
],
metadata: {
bar: 'old-metadata',
},
},
});
await supertestWithAdminScope
.put('/api/security/role/role_to_update')
.send({
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
run_as: ['admin'],
},
kibana: [
{
feature: {
dashboard: ['read'],
dev_tools: ['all'],
},
spaces: ['*'],
},
{
base: ['all'],
spaces: ['marketing', 'sales'],
},
],
})
.expect(400);
const role = await es.security.getRole({ name: 'role_to_update' });
expect(role).to.eql({
role_to_update: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
allow_restricted_indices: false,
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'apm',
privileges: ['apm-privilege'],
resources: ['*'],
},
],
metadata: {
bar: 'old-metadata',
},
run_as: [],
transient_metadata: {
enabled: true,
},
},
});
});
// serverless only (stateful will allow)
it(`should not update a role with remote cluster privileges`, async () => {
await es.security.putRole({
name: 'role_to_update',
body: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'apm',
privileges: ['apm-privilege'],
resources: ['*'],
},
],
metadata: {
bar: 'old-metadata',
},
},
});
await supertestWithAdminScope
.put('/api/security/role/role_to_update')
.send({
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
remote_cluster: [
{
clusters: ['remote_cluster1'],
privileges: ['monitor_enrich'],
},
],
},
kibana: [
{
feature: {
dashboard: ['read'],
dev_tools: ['all'],
},
spaces: ['*'],
},
{
base: ['all'],
spaces: ['marketing', 'sales'],
},
],
})
.expect(400);
const role = await es.security.getRole({ name: 'role_to_update' });
expect(role).to.eql({
role_to_update: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
allow_restricted_indices: false,
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'apm',
privileges: ['apm-privilege'],
resources: ['*'],
},
],
metadata: {
bar: 'old-metadata',
},
run_as: [],
transient_metadata: {
enabled: true,
},
},
});
});
// serverless only (stateful will allow)
it(`should not update a role with remote index privileges`, async () => {
await es.security.putRole({
name: 'role_to_update',
body: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'apm',
privileges: ['apm-privilege'],
resources: ['*'],
},
],
metadata: {
bar: 'old-metadata',
},
},
});
await supertestWithAdminScope
.put('/api/security/role/role_to_update')
.send({
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
remote_indices: [
{
clusters: ['remote_cluster1'],
names: ['remote_index1', 'remote_index2'],
privileges: ['all'],
},
],
},
kibana: [
{
feature: {
dashboard: ['read'],
dev_tools: ['all'],
},
spaces: ['*'],
},
{
base: ['all'],
spaces: ['marketing', 'sales'],
},
],
})
.expect(400);
const role = await es.security.getRole({ name: 'role_to_update' });
expect(role).to.eql({
role_to_update: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
allow_restricted_indices: false,
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'apm',
privileges: ['apm-privilege'],
resources: ['*'],
},
],
metadata: {
bar: 'old-metadata',
},
run_as: [],
transient_metadata: {
enabled: true,
},
},
});
});
});
describe('Delete Role', () => {
it('should delete an existing role', async () => {
await es.security.putRole({
name: 'role_to_delete',
body: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'apm',
privileges: ['apm-privilege'],
resources: ['*'],
},
],
metadata: {
bar: 'old-metadata',
},
},
});
await supertestWithAdminScope.delete('/api/security/role/role_to_delete').expect(204);
const deletedRole = await es.security.getRole(
{ name: 'role_to_delete' },
{ ignore: [404] }
);
expect(deletedRole).to.eql({});
});
});
});

View file

@ -20,11 +20,15 @@ export default createTestConfig({
suiteTags: { exclude: ['skipSvlOblt'] },
services,
// add feature flags
kbnServerArgs: ['--xpack.infra.enabled=true', '--xpack.security.roleManagementEnabled=true'],
kbnServerArgs: [
'--xpack.infra.enabled=true',
'--xpack.security.roleManagementEnabled=true', // enables custom roles
`--xpack.spaces.maxSpaces=100`, // enables spaces UI capabilities
],
// load tests in the index file
testFiles: [require.resolve('./index.feature_flags.ts')],
// include settings from project controller
// https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml
esServerArgs: ['xpack.ml.dfa.enabled=false'],
esServerArgs: ['xpack.ml.dfa.enabled=false', 'xpack.security.authc.native_roles.enabled=true'],
});

View file

@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./infra'));
loadTestFile(require.resolve('./platform_security'));
loadTestFile(require.resolve('../common/platform_security/roles_routes_feature_flag.ts'));
loadTestFile(require.resolve('../common/management/multiple_spaces_enabled.ts'));
});
}

View file

@ -19,7 +19,8 @@ export default createTestConfig({
suiteTags: { exclude: ['skipSvlSearch'] },
// add feature flags
kbnServerArgs: [
'--xpack.security.roleManagementEnabled=true',
'--xpack.security.roleManagementEnabled=true', // enables custom roles
`--xpack.spaces.maxSpaces=100`, // enables spaces UI capabilities
`--xpack.searchIndices.enabled=true`, // global empty state FF
],
// load tests in the index file
@ -27,5 +28,5 @@ export default createTestConfig({
// include settings from project controller
// https://github.com/elastic/project-controller/blob/main/internal/project/esproject/config/elasticsearch.yml
esServerArgs: [],
esServerArgs: ['xpack.security.authc.native_roles.enabled=true'],
});

View file

@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./search_indices'));
loadTestFile(require.resolve('./platform_security'));
loadTestFile(require.resolve('../common/platform_security/roles_routes_feature_flag.ts'));
loadTestFile(require.resolve('../common/management/multiple_spaces_enabled.ts'));
});
}

View file

@ -18,11 +18,14 @@ export default createTestConfig({
},
suiteTags: { exclude: ['skipSvlSec'] },
// add feature flags
kbnServerArgs: ['--xpack.security.roleManagementEnabled=true'],
kbnServerArgs: [
'--xpack.security.roleManagementEnabled=true', // enables custom roles
`--xpack.spaces.maxSpaces=100`, // enables spaces UI capabilities
],
// load tests in the index file
testFiles: [require.resolve('./index.feature_flags.ts')],
// include settings from project controller
// https://github.com/elastic/project-controller/blob/main/internal/project/security/config/elasticsearch.yml
esServerArgs: ['xpack.ml.nlp.enabled=true'],
esServerArgs: ['xpack.ml.nlp.enabled=true', 'xpack.security.authc.native_roles.enabled=true'],
});

View file

@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('Serverless security API - feature flags', function () {
loadTestFile(require.resolve('./platform_security'));
loadTestFile(require.resolve('../common/platform_security/roles_routes_feature_flag.ts'));
loadTestFile(require.resolve('../common/management/multiple_spaces_enabled.ts'));
});
}

View file

@ -5,72 +5,71 @@
* 2.0.
*/
import expect from '@kbn/expect';
import { Client } from '@elastic/elasticsearch';
import { ToolingLog } from '@kbn/tooling-log';
import { FtrProviderContext } from '../../../ftr_provider_context';
async function clearAllRoles(esClient: Client, logger: ToolingLog) {
const existingRoles = await esClient.security.getRole();
const esRolesNames = Object.entries(existingRoles)
.filter(([roleName, esRole]) => {
return !esRole.metadata?._reserved;
})
.map(([roleName]) => roleName);
if (esRolesNames.length > 0) {
await Promise.all(
esRolesNames.map(async (roleName) => {
await esClient.security.deleteRole({ name: roleName });
})
);
} else {
logger.debug('No Roles to delete.');
}
}
// Note: this suite is currently only called from the feature flags test config:
// x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts
// This can be moved into the common config groups once custom roles are enabled
// permanently in serverless.
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const pageObjects = getPageObjects(['common', 'svlCommonPage', 'svlManagementPage', 'security']);
const browser = getService('browser');
const es = getService('es');
const log = getService('log');
const platformSecurityUtils = getService('platformSecurityUtils');
describe('Roles', function () {
before(async () => {
await pageObjects.svlCommonPage.loginAsAdmin();
await pageObjects.common.navigateToApp('management');
await pageObjects.svlManagementPage.assertRoleManagementCardExists();
await pageObjects.svlManagementPage.clickRoleManagementCard();
const url = await browser.getCurrentUrl();
expect(url).to.contain('/management/security/roles');
});
after(async () => {
await clearAllRoles(es, log);
});
it('should not display reserved roles', async () => {
const table = await testSubjects.find('rolesTable');
const tableContent = await table.getVisibleText();
expect(tableContent).to.contain('No custom roles to show');
});
it('should create and only display custom roles', async () => {
const customRole = 'serverlesscustomrole';
await pageObjects.security.addRole(customRole, {
elasticsearch: {
indices: [
{
names: ['dlstest'],
privileges: ['read', 'view_index_metadata'],
},
],
},
describe('as Viewer', () => {
before(async () => {
await pageObjects.svlCommonPage.loginAsViewer();
await pageObjects.common.navigateToApp('management');
});
const table = await testSubjects.find('rolesTable');
const tableContent = await table.getVisibleText();
expect(tableContent).to.contain(customRole);
it('should not display the roles management card', async () => {
await pageObjects.svlManagementPage.assertRoleManagementCardDoesNotExist();
});
});
describe('as Admin', () => {
before(async () => {
await pageObjects.svlCommonPage.loginAsAdmin();
await pageObjects.common.navigateToApp('management');
await pageObjects.svlManagementPage.assertRoleManagementCardExists();
await pageObjects.svlManagementPage.clickRoleManagementCard();
const url = await browser.getCurrentUrl();
expect(url).to.contain('/management/security/roles');
});
after(async () => {
await platformSecurityUtils.clearAllRoles();
});
it('should not display reserved roles', async () => {
const table = await testSubjects.find('rolesTable');
const tableContent = await table.getVisibleText();
expect(tableContent).to.contain('No custom roles to show');
});
it('should create and only display custom roles', async () => {
const customRole = 'serverless-custom-role';
await pageObjects.security.addRole(customRole, {
elasticsearch: {
indices: [
{
names: ['dlstest'],
privileges: ['read', 'view_index_metadata'],
},
],
},
});
const rows = await testSubjects.findAll('roleRow');
expect(rows.length).equal(1);
const cells = await rows[0].findAllByClassName('euiTableRowCell');
expect(cells.length).greaterThan(1);
const cellContent = await cells[0].getVisibleText();
expect(cellContent).to.contain(customRole);
});
});
});
};

View file

@ -0,0 +1,102 @@
/*
* 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.
*/
// Note: this suite is currently only called from the feature flags test config:
// x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts
// These tests can be moved to the appropriate test file (spaces_selection,
// spaces_management) once multiple spaces are permanently enabled in production.
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getPageObject, getService }: FtrProviderContext) {
const svlCommon = getPageObject('common');
const svlCommonPage = getPageObject('svlCommonPage');
const svlCommonNavigation = getService('svlCommonNavigation');
const testSubjects = getService('testSubjects');
describe('spaces', function () {
describe('selection', function () {
describe('as Viewer', function () {
before(async () => {
await svlCommonPage.loginAsViewer();
});
it('displays the space selection menu in header', async () => {
await svlCommonNavigation.navigateToKibanaHome();
await svlCommonPage.assertProjectHeaderExists();
await testSubjects.existOrFail('spacesNavSelector');
});
it(`does not display the manage button in the space selection menu`, async () => {
await svlCommonNavigation.navigateToKibanaHome();
await svlCommonPage.assertProjectHeaderExists();
await testSubjects.click('spacesNavSelector');
await testSubjects.missingOrFail('manageSpaces');
});
});
describe('as Admin', function () {
before(async () => {
await svlCommonPage.loginAsAdmin();
});
it('displays the space selection menu in header', async () => {
await svlCommonNavigation.navigateToKibanaHome();
await svlCommonPage.assertProjectHeaderExists();
await testSubjects.existOrFail('spacesNavSelector');
});
it(`displays the manage button in the space selection menu`, async () => {
await svlCommonNavigation.navigateToKibanaHome();
await svlCommonPage.assertProjectHeaderExists();
await testSubjects.click('spacesNavSelector');
await testSubjects.existOrFail('manageSpaces');
});
});
});
describe('management', function () {
describe('as Viewer', function () {
before(async () => {
await svlCommonPage.loginAsViewer();
});
it('does not display the space management card', async () => {
await svlCommon.navigateToApp('management');
await testSubjects.missingOrFail('app-card-spaces');
});
});
describe('as Admin', function () {
before(async () => {
await svlCommonPage.loginAsAdmin();
});
it('displays the space management card', async () => {
await svlCommon.navigateToApp('management');
await testSubjects.existOrFail('app-card-spaces');
});
// xpack.spaces.allowFeatureVisibility: false for all solutions
it(`does not display feature visibility`, async () => {
await svlCommon.navigateToApp('management');
await testSubjects.click('app-card-spaces');
// create
await testSubjects.click('createSpace');
await testSubjects.missingOrFail('hideAllFeaturesLink');
await testSubjects.click('cancel-space-button');
// edit
await testSubjects.click('default-hyperlink');
await testSubjects.missingOrFail('hideAllFeaturesLink');
});
});
});
});
}

View file

@ -1,60 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// Note: this suite is currently only called from the feature flags test config:
// x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts
// This file can take the place of the spaces_selection test file when spaces
// are enabled permanently in serverless.
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getPageObject, getService }: FtrProviderContext) {
const svlCommonPage = getPageObject('svlCommonPage');
const svlCommonNavigation = getService('svlCommonNavigation');
const testSubjects = getService('testSubjects');
describe('space selection', function () {
describe('as Viewer', function () {
before(async () => {
await svlCommonPage.loginAsViewer();
});
it('displays the space selection menu in header', async () => {
await svlCommonNavigation.navigateToKibanaHome();
await svlCommonPage.assertProjectHeaderExists();
await testSubjects.existOrFail('spacesNavSelector');
});
it(`does not display the manage button in the space selection menu`, async () => {
await svlCommonNavigation.navigateToKibanaHome();
await svlCommonPage.assertProjectHeaderExists();
await testSubjects.click('spacesNavSelector');
await testSubjects.missingOrFail('manageSpaces');
});
});
describe('as Admin', function () {
before(async () => {
await svlCommonPage.loginAsAdmin();
});
it('displays the space selection menu in header', async () => {
await svlCommonNavigation.navigateToKibanaHome();
await svlCommonPage.assertProjectHeaderExists();
await testSubjects.existOrFail('spacesNavSelector');
});
it(`displays the manage button in the space selection menu`, async () => {
await svlCommonNavigation.navigateToKibanaHome();
await svlCommonPage.assertProjectHeaderExists();
await testSubjects.click('spacesNavSelector');
await testSubjects.existOrFail('manageSpaces');
});
});
});
}

View file

@ -22,6 +22,7 @@ export default createTestConfig({
'--xpack.infra.enabled=true',
'--xpack.infra.featureFlags.customThresholdAlertsEnabled=true',
'--xpack.security.roleManagementEnabled=true',
`--xpack.cloud.serverless.project_id='fakeprojectid'`,
`--xpack.cloud.base_url='https://cloud.elastic.co'`,
`--xpack.cloud.organization_url='/account/members'`,
`--xpack.spaces.maxSpaces=100`, // enables spaces UI capabilities

View file

@ -13,6 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./infra'));
loadTestFile(require.resolve('../common/platform_security/navigation/management_nav_cards.ts'));
loadTestFile(require.resolve('../common/platform_security/roles.ts'));
loadTestFile(require.resolve('../common/spaces/spaces_selection_enabled.ts'));
loadTestFile(require.resolve('../common/spaces/multiple_spaces_enabled.ts'));
});
}

View file

@ -13,6 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./elasticsearch_start.ts'));
loadTestFile(require.resolve('../common/platform_security/navigation/management_nav_cards.ts'));
loadTestFile(require.resolve('../common/platform_security/roles.ts'));
loadTestFile(require.resolve('../common/spaces/spaces_selection_enabled.ts'));
loadTestFile(require.resolve('../common/spaces/multiple_spaces_enabled.ts'));
});
}

View file

@ -12,6 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
// add tests that require feature flags, defined in config.feature_flags.ts
loadTestFile(require.resolve('../common/platform_security/navigation/management_nav_cards.ts'));
loadTestFile(require.resolve('../common/platform_security/roles.ts'));
loadTestFile(require.resolve('../common/spaces/spaces_selection_enabled.ts'));
loadTestFile(require.resolve('../common/spaces/multiple_spaces_enabled.ts'));
});
}

View file

@ -6,10 +6,12 @@
*/
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { services as deploymentAgnosticServices } from '@kbn/test-suites-xpack/api_integration/deployment_agnostic/services';
import { SupertestProvider } from './supertest';
import { SvlCommonApiServiceProvider } from './svl_common_api';
import { SvlReportingServiceProvider } from './svl_reporting';
import { DataViewApiProvider } from './data_view_api';
import { PlatformSecurityUtilsProvider } from './platform_security_utils';
export type {
InternalRequestHeader,
@ -25,5 +27,8 @@ export const services = {
svlCommonApi: SvlCommonApiServiceProvider,
svlReportingApi: SvlReportingServiceProvider,
svlUserManager: commonFunctionalServices.samlAuth,
samlAuth: commonFunctionalServices.samlAuth, // <--temp workaround until we can unify naming
roleScopedSupertest: deploymentAgnosticServices.roleScopedSupertest,
dataViewApi: DataViewApiProvider,
platformSecurityUtils: PlatformSecurityUtilsProvider,
};

View file

@ -0,0 +1,35 @@
/*
* 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 { FtrProviderContext } from '../../functional/ftr_provider_context';
export function PlatformSecurityUtilsProvider({ getService }: FtrProviderContext) {
const es = getService('es');
const log = getService('log');
return {
// call it from 'samlAuth' service when tests are migrated to deployment-agnostic
async clearAllRoles() {
const existingRoles = await es.security.getRole();
const esRolesNames = Object.entries(existingRoles)
.filter(([roleName, esRole]) => {
return !esRole.metadata?._reserved;
})
.map(([roleName]) => roleName);
if (esRolesNames.length > 0) {
await Promise.all(
esRolesNames.map(async (roleName) => {
await es.security.deleteRole({ name: roleName });
})
);
} else {
log.debug('No Roles to delete.');
}
},
};
}

View file

@ -98,6 +98,7 @@
"@kbn/test-suites-src",
"@kbn/console-plugin",
"@kbn/cloud-security-posture-common",
"@kbn/security-plugin-types-common",
"@kbn/core-saved-objects-import-export-server-internal",
]
}