mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
Bulk Role Endpoint (#189173)
## Summary This PR adds a new `POST security/roles` API that can be used to bulk create or update roles. ## How to test 1. Create empty roles ``` POST kbn:/api/security/roles { "roles": { "bulk_role_1": {}, "bulk_role_2": {} } } ``` <details> <summary>2. Create roles with Kibana and ES privileges</summary> POST kbn:/api/security/roles { "roles": { "bulk_role_with_privilege_1": { "elasticsearch": { "cluster": ["manage"], "indices": [ { "names": ["logstash-*"], "privileges": ["read", "view_index_metadata"] } ], "run_as": ["watcher_user"] }, "kibana": [ { "base": ["read"] }, { "feature": { "dashboard": ["read"], "discover": ["all"], "ml": ["all"] }, "spaces": ["marketing", "sales"] } ] }, "bulk_role_with_privilege_2": { "elasticsearch": { "cluster": ["manage"], "indices": [ { "names": ["logstash-*"], "privileges": ["read", "view_index_metadata"] } ], "run_as": ["watcher_user"] }, "kibana": [ { "base": ["read"] }, { "feature": { "dashboard": ["read"], "discover": ["all"], "ml": ["all"] }, "spaces": ["marketing", "sales"] } ] } } } </details> <details> <summary>3. Create roles failing validation </summary> POST kbn:/api/security/roles { "roles": { "bulk_role_es_invalid": { "elasticsearch": { "cluster": ["bla"] } }, "bulk_role_kibana_invalid": { "kibana": [ { "spaces": ["bar-space"], "base": [], "feature": { "fleetv2": ["all", "read"] } } ] }, "bulk_role_valid": { "elasticsearch": { "cluster": ["all"] } } } } </details> <details> <summary>4. Check validation for license (under basic license should return security_exception) </summary> POST kbn:/api/security/roles { "roles": { "role_with_privileges_dls_fls": { "metadata": { "foo": "test-metadata" }, "elasticsearch": { "cluster": ["manage"], "indices": [ { "field_security": { "grant": ["*"], "except": ["geo.*"] }, "names": ["logstash-*"], "privileges": ["read", "view_index_metadata"], "query": "{ \"match\": { \"geo.src\": \"CN\" } }" } ], "run_as": ["watcher_user"] } } } } </details> ### Checklist - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed __Fixes: https://github.com/elastic/kibana/issues/187427__ ## Release Notes Added API endpoint `POST security/roles` that can be used to bulk create or update roles. --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
ebaa751ad1
commit
c8608461ae
14 changed files with 2069 additions and 23 deletions
|
@ -9,6 +9,7 @@ WARNING: Do not use the {ref}/security-api.html#security-role-apis[{es} role man
|
|||
The following {kib} role management APIs are available:
|
||||
|
||||
* <<role-management-api-put, Create or update role API>> to create a new {kib} role, or update the attributes of an existing role
|
||||
* <<role-management-api-put-bulk, Bulk create or update roles API>> to create a new {kib} roles, or update the attributes of existing roles
|
||||
|
||||
* <<role-management-api-get, Get all {kib} roles API>> to retrieve all {kib} roles
|
||||
|
||||
|
@ -20,3 +21,4 @@ include::role-management/put.asciidoc[]
|
|||
include::role-management/get.asciidoc[]
|
||||
include::role-management/get-all.asciidoc[]
|
||||
include::role-management/delete.asciidoc[]
|
||||
include::role-management/put-bulk.asciidoc[]
|
||||
|
|
377
docs/api/role-management/put-bulk.asciidoc
Normal file
377
docs/api/role-management/put-bulk.asciidoc
Normal file
|
@ -0,0 +1,377 @@
|
|||
[[role-management-api-put-bulk]]
|
||||
=== Bulk create or update roles API
|
||||
++++
|
||||
<titleabbrev>Bulk create or update roles API</titleabbrev>
|
||||
++++
|
||||
|
||||
preview::["This functionality is in technical preview, and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features."]
|
||||
|
||||
experimental[] Create new {kib} roles, or update the attributes of an existing roles. {kib} roles are stored in the
|
||||
{es} native realm.
|
||||
|
||||
[[role-management-api-put-bulk-request]]
|
||||
==== Request
|
||||
|
||||
`POST <kibana host>:<port>/api/security/roles`
|
||||
|
||||
[[role-management-api-put-bulk-prereqs]]
|
||||
==== Prerequisite
|
||||
|
||||
To use the bulk create or update roles API, you must have the `manage_security` cluster privilege.
|
||||
|
||||
[role="child_attributes"]
|
||||
[[role-management-api-bulk-response-body]]
|
||||
==== Request body
|
||||
|
||||
`roles`::
|
||||
(object) Object that specifies the roles to add as a role name to role map.
|
||||
`<role_name>` (required):: (string) The role name.
|
||||
`description`::
|
||||
(Optional, string) Description for the role.
|
||||
|
||||
`metadata`::
|
||||
(Optional, object) In the `metadata` object, keys that begin with `_` are reserved for system usage.
|
||||
|
||||
`elasticsearch`::
|
||||
(Optional, object) {es} cluster and index privileges. Valid keys include
|
||||
`cluster`, `indices`, `remote_indices`, `remote_cluster`, and `run_as`. For more information, see
|
||||
{ref}/defining-roles.html[Defining roles].
|
||||
|
||||
`kibana`::
|
||||
(list) Objects that specify the <<kibana-privileges, Kibana privileges>> for the role.
|
||||
+
|
||||
.Properties of `kibana`
|
||||
[%collapsible%open]
|
||||
=====
|
||||
`base` :::
|
||||
(Optional, list) A base privilege. When specified, the base must be `["all"]` or `["read"]`.
|
||||
When the `base` privilege is specified, you are unable to use the `feature` section.
|
||||
"all" grants read/write access to all {kib} features for the specified spaces.
|
||||
"read" grants read-only access to all {kib} features for the specified spaces.
|
||||
|
||||
`feature` :::
|
||||
(object) Contains privileges for specific features.
|
||||
When the `feature` privileges are specified, you are unable to use the `base` section.
|
||||
To retrieve a list of available features, use the <<features-api-get, features API>>.
|
||||
|
||||
`spaces` :::
|
||||
(list) The spaces to apply the privileges to.
|
||||
To grant access to all spaces, set to `["*"]`, or omit the value.
|
||||
=====
|
||||
|
||||
[[role-management-api-bulk-put-response-codes]]
|
||||
==== Response code
|
||||
|
||||
`200`::
|
||||
Indicates a successful call.
|
||||
|
||||
==== Examples
|
||||
|
||||
Grant access to various features in all spaces:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
$ curl -X POST api/security/roles
|
||||
{
|
||||
"roles": {
|
||||
"my_kibana_role_1": {
|
||||
"description": "my_kibana_role_1_description",
|
||||
"metadata": {
|
||||
"version": 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": []
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"base": [],
|
||||
"feature": {
|
||||
"discover": ["all"],
|
||||
"visualize": ["all"],
|
||||
"dashboard": ["all"],
|
||||
"dev_tools": ["read"],
|
||||
"advancedSettings": ["read"],
|
||||
"indexPatterns": ["read"],
|
||||
"graph": ["all"],
|
||||
"apm": ["read"],
|
||||
"maps": ["read"],
|
||||
"canvas": ["read"],
|
||||
"infrastructure": ["all"],
|
||||
"logs": ["all"],
|
||||
"uptime": ["all"]
|
||||
},
|
||||
"spaces": ["*"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"my_kibana_role_2": {
|
||||
"description": "my_kibana_role_2_description",
|
||||
"metadata": {
|
||||
"version": 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": []
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"base": [],
|
||||
"feature": {
|
||||
"discover": ["all"],
|
||||
"visualize": ["all"],
|
||||
"dashboard": ["all"],
|
||||
"dev_tools": ["read"],
|
||||
"logs": ["all"],
|
||||
"uptime": ["all"]
|
||||
},
|
||||
"spaces": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
Grant dashboard-only access to only the Marketing space for `my_kibana_role_1` and dashboard-only access to only Sales space for `my_kibana_role_2`:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
$ curl -X POST api/security/roles
|
||||
{
|
||||
"roles": {
|
||||
"my_kibana_role_1": {
|
||||
"description": "Grants dashboard-only access to only the Marketing space.",
|
||||
"metadata": {
|
||||
"version": 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": []
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"base": [],
|
||||
"feature": {
|
||||
"dashboard": ["read"]
|
||||
},
|
||||
"spaces": ["marketing"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"my_kibana_role_2": {
|
||||
"description": "Grants dashboard-only access to only the Sales space.",
|
||||
"metadata": {
|
||||
"version": 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": []
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"base": [],
|
||||
"feature": {
|
||||
"dashboard": ["read"]
|
||||
},
|
||||
"spaces": ["sales"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
Grant full access to all features in the Default space for `my_kibana_role_1` and `my_kibana_role_2`:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
$ curl -X PUT api/security/role
|
||||
{
|
||||
"roles": {
|
||||
"my_kibana_role_1": {
|
||||
"description": "Grants full access to all features in the Default space.",
|
||||
"metadata": {
|
||||
"version": 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": []
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"base": ["all"],
|
||||
"feature": {},
|
||||
"spaces": ["default"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"my_kibana_role_2": {
|
||||
"description": "Grants full access to all features in the Default space.",
|
||||
"metadata": {
|
||||
"version": 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": []
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"base": ["all"],
|
||||
"feature": {},
|
||||
"spaces": ["default"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
Grant different access to different spaces:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
$ curl -X POST api/security/roles
|
||||
{
|
||||
"roles": {
|
||||
"my_kibana_role_1": {
|
||||
"description": "Grants full access to discover and dashboard features in the default space. Grants read access in the marketing, and sales spaces.",
|
||||
"metadata": {
|
||||
"version": 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": []
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"base": [],
|
||||
"feature": {
|
||||
"discover": ["all"],
|
||||
"dashboard": ["all"]
|
||||
},
|
||||
"spaces": ["default"]
|
||||
},
|
||||
{
|
||||
"base": ["read"],
|
||||
"spaces": ["marketing", "sales"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"my_kibana_role_2": {
|
||||
"description": "Grants full access to discover and dashboard features in the default space. Grants read access in the marketing space.",
|
||||
"metadata": {
|
||||
"version": 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": []
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"base": [],
|
||||
"feature": {
|
||||
"discover": ["all"],
|
||||
"dashboard": ["all"]
|
||||
},
|
||||
"spaces": ["default"]
|
||||
},
|
||||
{
|
||||
"base": ["read"],
|
||||
"spaces": ["marketing"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
Grant access to {kib} and {es}:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
$ curl -X POST api/security/roles
|
||||
{
|
||||
"roles": {
|
||||
"my_kibana_role_1": {
|
||||
"description": "Grants all cluster privileges and full access to index1 and index2. Grants full access to remote_index1 and remote_index2, and the monitor_enrich cluster privilege on remote_cluster1. Grants all Kibana privileges in the default space.",
|
||||
"metadata": {
|
||||
"version": 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster": ["all"],
|
||||
"indices": [
|
||||
{
|
||||
"names": ["index1", "index2"],
|
||||
"privileges": ["all"]
|
||||
}
|
||||
],
|
||||
"remote_indices": [
|
||||
{
|
||||
"clusters": ["remote_cluster1"],
|
||||
"names": ["remote_index1", "remote_index2"],
|
||||
"privileges": ["all"]
|
||||
}
|
||||
],
|
||||
"remote_cluster": [
|
||||
{
|
||||
"clusters": ["remote_cluster1"],
|
||||
"privileges": ["monitor_enrich"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"base": ["all"],
|
||||
"feature": {},
|
||||
"spaces": ["default"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"my_kibana_role_2": {
|
||||
"description": "Grants all cluster privileges and full access to index1. Grants full access to remote_index1, and the monitor_enrich cluster privilege on remote_cluster1. Grants all Kibana privileges in the default space.",
|
||||
"metadata": {
|
||||
"version": 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster": ["all"],
|
||||
"indices": [
|
||||
{
|
||||
"names": ["index1"],
|
||||
"privileges": ["all"]
|
||||
}
|
||||
],
|
||||
"remote_indices": [
|
||||
{
|
||||
"clusters": ["remote_cluster1"],
|
||||
"names": ["remote_index1"],
|
||||
"privileges": ["all"]
|
||||
}
|
||||
],
|
||||
"remote_cluster": [
|
||||
{
|
||||
"clusters": ["remote_cluster1"],
|
||||
"privileges": ["monitor_enrich"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"base": ["all"],
|
||||
"feature": {},
|
||||
"spaces": ["default"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
|
@ -9,6 +9,7 @@ import { defineDeleteRolesRoutes } from './delete';
|
|||
import { defineGetRolesRoutes } from './get';
|
||||
import { defineGetAllRolesRoutes } from './get_all';
|
||||
import { defineGetAllRolesBySpaceRoutes } from './get_all_by_space';
|
||||
import { defineBulkCreateOrUpdateRolesRoutes } from './post';
|
||||
import { definePutRolesRoutes } from './put';
|
||||
import type { RouteDefinitionParams } from '../..';
|
||||
|
||||
|
@ -18,4 +19,5 @@ export function defineRolesRoutes(params: RouteDefinitionParams) {
|
|||
defineDeleteRolesRoutes(params);
|
||||
definePutRolesRoutes(params);
|
||||
defineGetAllRolesBySpaceRoutes(params);
|
||||
defineBulkCreateOrUpdateRolesRoutes(params);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { roleGrantsSubFeaturePrivileges } from './role_privileges';
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 type { KibanaFeature } from '@kbn/features-plugin/common';
|
||||
|
||||
import type { RolePayloadSchemaType } from '../model/put_payload';
|
||||
|
||||
export const roleGrantsSubFeaturePrivileges = (
|
||||
features: KibanaFeature[],
|
||||
role: RolePayloadSchemaType
|
||||
) => {
|
||||
if (!role.kibana) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const subFeaturePrivileges = new Map(
|
||||
features.map((feature) => [
|
||||
feature.id,
|
||||
feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2),
|
||||
])
|
||||
);
|
||||
|
||||
const hasAnySubFeaturePrivileges = role.kibana.some((kibanaPrivilege) =>
|
||||
Object.entries(kibanaPrivilege.feature ?? {}).some(([featureId, privileges]) => {
|
||||
return !!subFeaturePrivileges.get(featureId)?.some(({ id }) => privileges.includes(id));
|
||||
})
|
||||
);
|
||||
|
||||
return hasAnySubFeaturePrivileges;
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { getBulkCreateOrUpdatePayloadSchema } from './bulk_create_or_update_payload';
|
||||
|
||||
describe('getBulkCreateOrUpdatePayloadSchema', () => {
|
||||
const mockGetBasePrivilegeNames = jest.fn(() => ({
|
||||
global: ['all', 'read'],
|
||||
space: ['all', 'read'],
|
||||
}));
|
||||
|
||||
const bulkCreateOrUpdatePayloadSchema =
|
||||
getBulkCreateOrUpdatePayloadSchema(mockGetBasePrivilegeNames);
|
||||
|
||||
it('should validate a correct payload', () => {
|
||||
const payload = {
|
||||
roles: {
|
||||
role1: {
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => bulkCreateOrUpdatePayloadSchema.validate(payload)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw an error for missing roles', () => {
|
||||
const payload = {};
|
||||
|
||||
expect(() =>
|
||||
bulkCreateOrUpdatePayloadSchema.validate(payload)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[roles]: expected value of type [object] but got [undefined]"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error for invalid role structure', () => {
|
||||
const payload = {
|
||||
roles: {
|
||||
role1: 'invalid_structure',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
bulkCreateOrUpdatePayloadSchema.validate(payload)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[roles.role1]: could not parse object value from json input"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 type { TypeOf } from '@kbn/config-schema';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { getPutPayloadSchema } from './put_payload';
|
||||
|
||||
export function getBulkCreateOrUpdatePayloadSchema(
|
||||
getBasePrivilegeNames: () => { global: string[]; space: string[] }
|
||||
) {
|
||||
return schema.object({
|
||||
roles: schema.recordOf(schema.string(), getPutPayloadSchema(getBasePrivilegeNames)),
|
||||
});
|
||||
}
|
||||
|
||||
export type BulkCreateOrUpdateRolesPayloadSchemaType = TypeOf<
|
||||
ReturnType<typeof getBulkCreateOrUpdatePayloadSchema>
|
||||
>;
|
|
@ -7,3 +7,6 @@
|
|||
|
||||
export type { RolePayloadSchemaType } from './put_payload';
|
||||
export { transformPutPayloadToElasticsearchRole, getPutPayloadSchema } from './put_payload';
|
||||
|
||||
export { getBulkCreateOrUpdatePayloadSchema } from './bulk_create_or_update_payload';
|
||||
export type { BulkCreateOrUpdateRolesPayloadSchemaType } from './bulk_create_or_update_payload';
|
||||
|
|
|
@ -0,0 +1,999 @@
|
|||
/*
|
||||
* 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 { kibanaResponseFactory } from '@kbn/core/server';
|
||||
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
|
||||
import { KibanaFeature } from '@kbn/features-plugin/server';
|
||||
import type { LicenseCheck } from '@kbn/licensing-plugin/server';
|
||||
import { GLOBAL_RESOURCE } from '@kbn/security-plugin-types-server';
|
||||
|
||||
import type { BulkCreateOrUpdateRolesPayloadSchemaType } from './model/bulk_create_or_update_payload';
|
||||
import { defineBulkCreateOrUpdateRolesRoutes } from './post';
|
||||
import { securityFeatureUsageServiceMock } from '../../../feature_usage/index.mock';
|
||||
import { routeDefinitionParamsMock } from '../../index.mock';
|
||||
|
||||
const application = 'kibana-.kibana';
|
||||
const privilegeMap = {
|
||||
global: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
space: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
features: {
|
||||
foo: {
|
||||
'foo-privilege-1': [],
|
||||
'foo-privilege-2': [],
|
||||
},
|
||||
bar: {
|
||||
'bar-privilege-1': [],
|
||||
'bar-privilege-2': [],
|
||||
},
|
||||
},
|
||||
reserved: {
|
||||
customApplication1: [],
|
||||
customApplication2: [],
|
||||
},
|
||||
};
|
||||
|
||||
const kibanaFeature = new KibanaFeature({
|
||||
id: 'bar',
|
||||
name: 'bar',
|
||||
privileges: {
|
||||
all: {
|
||||
requireAllSpaces: true,
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
read: {
|
||||
disabled: true,
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
},
|
||||
app: [],
|
||||
category: { id: 'bar', label: 'bar' },
|
||||
});
|
||||
|
||||
interface TestOptions {
|
||||
licenseCheckResult?: LicenseCheck;
|
||||
apiResponses?: {
|
||||
get: () => unknown;
|
||||
post: () => Record<string, unknown>;
|
||||
};
|
||||
payload: BulkCreateOrUpdateRolesPayloadSchemaType;
|
||||
asserts: {
|
||||
statusCode: number;
|
||||
result?: Record<string, any>;
|
||||
apiArguments?: { get: unknown[]; post: unknown[] };
|
||||
recordSubFeaturePrivilegeUsage?: boolean;
|
||||
};
|
||||
features?: KibanaFeature[];
|
||||
}
|
||||
|
||||
const postRolesTest = (
|
||||
description: string,
|
||||
{ payload, licenseCheckResult = { state: 'valid' }, apiResponses, asserts, features }: TestOptions
|
||||
) => {
|
||||
test(description, async () => {
|
||||
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
|
||||
mockRouteDefinitionParams.authz.applicationName = application;
|
||||
mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap);
|
||||
|
||||
const mockCoreContext = coreMock.createRequestHandlerContext();
|
||||
const mockLicensingContext = {
|
||||
license: { check: jest.fn().mockReturnValue(licenseCheckResult) },
|
||||
} as any;
|
||||
const mockContext = coreMock.createCustomRequestHandlerContext({
|
||||
core: mockCoreContext,
|
||||
licensing: mockLicensingContext,
|
||||
});
|
||||
|
||||
if (apiResponses?.get) {
|
||||
mockCoreContext.elasticsearch.client.asCurrentUser.security.getRole.mockResponseImplementationOnce(
|
||||
(() => ({ body: apiResponses?.get() })) as any
|
||||
);
|
||||
}
|
||||
|
||||
if (apiResponses?.post) {
|
||||
mockCoreContext.elasticsearch.client.asCurrentUser.transport.request.mockImplementationOnce(
|
||||
(() => ({ ...apiResponses?.post() })) as any
|
||||
);
|
||||
}
|
||||
|
||||
mockRouteDefinitionParams.getFeatureUsageService.mockReturnValue(
|
||||
securityFeatureUsageServiceMock.createStartContract()
|
||||
);
|
||||
|
||||
mockRouteDefinitionParams.getFeatures.mockResolvedValue(
|
||||
features ?? [
|
||||
new KibanaFeature({
|
||||
id: 'feature_1',
|
||||
name: 'feature 1',
|
||||
app: [],
|
||||
category: { id: 'foo', label: 'foo' },
|
||||
privileges: {
|
||||
all: {
|
||||
ui: [],
|
||||
savedObject: { all: [], read: [] },
|
||||
},
|
||||
read: {
|
||||
ui: [],
|
||||
savedObject: { all: [], read: [] },
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'sub feature 1',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub_feature_privilege_1',
|
||||
name: 'first sub-feature privilege',
|
||||
includeIn: 'none',
|
||||
ui: [],
|
||||
savedObject: { all: [], read: [] },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
defineBulkCreateOrUpdateRolesRoutes(mockRouteDefinitionParams);
|
||||
const [[{ validate }, handler]] = mockRouteDefinitionParams.router.post.mock.calls;
|
||||
|
||||
const headers = { authorization: 'foo' };
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
method: 'post',
|
||||
path: '/api/security/roles',
|
||||
body: payload !== undefined ? (validate as any).body.validate(payload) : undefined,
|
||||
headers,
|
||||
});
|
||||
|
||||
const response = await handler(mockContext, mockRequest, kibanaResponseFactory);
|
||||
expect(response.status).toBe(asserts.statusCode);
|
||||
expect(response.payload).toEqual(asserts.result);
|
||||
|
||||
if (asserts.apiArguments?.get) {
|
||||
expect(
|
||||
mockCoreContext.elasticsearch.client.asCurrentUser.security.getRole
|
||||
).toHaveBeenCalledWith(...asserts.apiArguments?.get);
|
||||
}
|
||||
if (asserts.apiArguments?.post) {
|
||||
const [body] = asserts.apiArguments?.post ?? [];
|
||||
expect(
|
||||
mockCoreContext.elasticsearch.client.asCurrentUser.transport.request
|
||||
).toHaveBeenCalledWith({
|
||||
method: 'POST',
|
||||
path: '/_security/role',
|
||||
body,
|
||||
});
|
||||
}
|
||||
expect(mockLicensingContext.license.check).toHaveBeenCalledWith('security', 'basic');
|
||||
|
||||
if (asserts.recordSubFeaturePrivilegeUsage) {
|
||||
expect(
|
||||
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
|
||||
).toHaveBeenCalledTimes(
|
||||
(response.payload?.created?.length ?? 0) +
|
||||
(response.payload?.updated?.length ?? 0) +
|
||||
(response.payload?.noop?.length ?? 0)
|
||||
);
|
||||
} else {
|
||||
expect(
|
||||
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
|
||||
).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
describe('POST roles', () => {
|
||||
describe('failure', () => {
|
||||
postRolesTest('returns result of license checker', {
|
||||
payload: { roles: {} } as BulkCreateOrUpdateRolesPayloadSchemaType,
|
||||
licenseCheckResult: { state: 'invalid', message: 'test forbidden message' },
|
||||
asserts: { statusCode: 403, result: { message: 'test forbidden message' } },
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
postRolesTest(`creates empty roles`, {
|
||||
payload: {
|
||||
roles: {
|
||||
'role-1': {
|
||||
elasticsearch: {},
|
||||
kibana: [],
|
||||
},
|
||||
'role-2': {
|
||||
elasticsearch: {},
|
||||
kibana: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
apiResponses: {
|
||||
get: () => ({}),
|
||||
post: () => ({ created: ['role-1', 'role-2'] }),
|
||||
},
|
||||
asserts: {
|
||||
apiArguments: {
|
||||
get: [{ name: 'role-1,role-2' }, { ignore: [404] }],
|
||||
post: [
|
||||
{
|
||||
roles: {
|
||||
'role-1': {
|
||||
applications: [],
|
||||
cluster: [],
|
||||
indices: [],
|
||||
remote_indices: undefined,
|
||||
remote_cluster: undefined,
|
||||
run_as: [],
|
||||
metadata: undefined,
|
||||
},
|
||||
'role-2': {
|
||||
applications: [],
|
||||
cluster: [],
|
||||
indices: [],
|
||||
remote_indices: undefined,
|
||||
remote_cluster: undefined,
|
||||
run_as: [],
|
||||
metadata: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
statusCode: 200,
|
||||
result: { created: ['role-1', 'role-2'] },
|
||||
},
|
||||
});
|
||||
|
||||
postRolesTest('returns validation errors', {
|
||||
payload: {
|
||||
roles: {
|
||||
'role-1': {
|
||||
elasticsearch: {},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['bar-space'],
|
||||
base: [],
|
||||
feature: {
|
||||
bar: ['all', 'read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
apiResponses: {
|
||||
get: () => ({}),
|
||||
post: () => ({}),
|
||||
},
|
||||
features: [kibanaFeature],
|
||||
asserts: {
|
||||
statusCode: 200,
|
||||
result: {
|
||||
errors: {
|
||||
'role-1': {
|
||||
type: 'kibana_privilege_validation_exception',
|
||||
reason:
|
||||
'Role cannot be updated due to validation errors: ["Feature privilege [bar.all] requires all spaces to be selected but received [bar-space]","Feature [bar] does not support privilege [read]."]',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
postRolesTest(`returns errors for not updated/created roles`, {
|
||||
payload: {
|
||||
roles: {
|
||||
'role-1': {
|
||||
elasticsearch: {},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['bar-space'],
|
||||
base: [],
|
||||
feature: {
|
||||
bar: ['all', 'read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'role-2': {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'role-3': {
|
||||
elasticsearch: {},
|
||||
kibana: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
features: [kibanaFeature],
|
||||
apiResponses: {
|
||||
get: () => ({}),
|
||||
post: () => ({
|
||||
created: ['role-3'],
|
||||
errors: {
|
||||
count: 1,
|
||||
details: {
|
||||
'role-2': {
|
||||
type: 'action_request_validation_exception',
|
||||
reason: 'Validation Failed',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
asserts: {
|
||||
apiArguments: {
|
||||
get: [{ name: 'role-2,role-3' }, { ignore: [404] }],
|
||||
post: [
|
||||
{
|
||||
roles: {
|
||||
'role-2': {
|
||||
applications: [],
|
||||
cluster: [],
|
||||
indices: [
|
||||
{
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test'],
|
||||
},
|
||||
],
|
||||
remote_indices: undefined,
|
||||
remote_cluster: undefined,
|
||||
run_as: [],
|
||||
metadata: undefined,
|
||||
},
|
||||
'role-3': {
|
||||
applications: [],
|
||||
cluster: [],
|
||||
indices: [],
|
||||
remote_indices: undefined,
|
||||
remote_cluster: undefined,
|
||||
run_as: [],
|
||||
metadata: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
statusCode: 200,
|
||||
result: {
|
||||
created: ['role-3'],
|
||||
errors: {
|
||||
'role-1': {
|
||||
type: 'kibana_privilege_validation_exception',
|
||||
reason:
|
||||
'Role cannot be updated due to validation errors: ["Feature privilege [bar.all] requires all spaces to be selected but received [bar-space]","Feature [bar] does not support privilege [read]."]',
|
||||
},
|
||||
'role-2': {
|
||||
reason: 'Validation Failed',
|
||||
type: 'action_request_validation_exception',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
postRolesTest(
|
||||
`creates non-existing role and updates role which has existing kibana privileges`,
|
||||
{
|
||||
payload: {
|
||||
roles: {
|
||||
'new-role': {
|
||||
kibana: [],
|
||||
elasticsearch: {
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
{
|
||||
clusters: ['cluster3', 'cluster4'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'existing-role': {
|
||||
elasticsearch: {
|
||||
cluster: ['test-cluster-privilege'],
|
||||
indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
|
||||
except: ['test-field-security-except-1', 'test-field-security-except-2'],
|
||||
},
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
query: `{ "match": { "title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
run_as: ['test-run-as-1', 'test-run-as-2'],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
foo: ['foo-privilege-1'],
|
||||
bar: ['bar-privilege-1'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
{
|
||||
base: ['all'],
|
||||
spaces: ['test-space-1', 'test-space-2'],
|
||||
},
|
||||
{
|
||||
feature: {
|
||||
bar: ['bar-privilege-2'],
|
||||
},
|
||||
spaces: ['test-space-3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
apiResponses: {
|
||||
get: () => ({
|
||||
'existing-role': {
|
||||
cluster: ['old-cluster-privilege'],
|
||||
indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['old-field-security-grant-1', 'old-field-security-grant-2'],
|
||||
except: ['old-field-security-except-1', 'old-field-security-except-2'],
|
||||
},
|
||||
names: ['old-index-name'],
|
||||
privileges: ['old-privilege'],
|
||||
query: `{ "match": { "old-title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
run_as: ['old-run-as'],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['old-kibana-privilege'],
|
||||
resources: ['old-resource'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
post: () => ({ updated: ['existing-role'], created: ['new-role'] }),
|
||||
},
|
||||
asserts: {
|
||||
apiArguments: {
|
||||
get: [{ name: 'new-role,existing-role' }, { ignore: [404] }],
|
||||
post: [
|
||||
{
|
||||
roles: {
|
||||
'new-role': {
|
||||
applications: [],
|
||||
cluster: [],
|
||||
indices: [],
|
||||
remote_indices: undefined,
|
||||
run_as: [],
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
{
|
||||
clusters: ['cluster3', 'cluster4'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
],
|
||||
metadata: undefined,
|
||||
},
|
||||
'existing-role': {
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['feature_foo.foo-privilege-1', 'feature_bar.bar-privilege-1'],
|
||||
resources: [GLOBAL_RESOURCE],
|
||||
},
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all'],
|
||||
resources: ['space:test-space-1', 'space:test-space-2'],
|
||||
},
|
||||
{
|
||||
application,
|
||||
privileges: ['feature_bar.bar-privilege-2'],
|
||||
resources: ['space:test-space-3'],
|
||||
},
|
||||
],
|
||||
cluster: ['test-cluster-privilege'],
|
||||
indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
|
||||
except: ['test-field-security-except-1', 'test-field-security-except-2'],
|
||||
},
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
query: `{ "match": { "title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
remote_indices: undefined,
|
||||
metadata: undefined,
|
||||
run_as: ['test-run-as-1', 'test-run-as-2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
statusCode: 200,
|
||||
result: { updated: ['existing-role'], created: ['new-role'] },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
postRolesTest(`notifies when sub-feature privileges are included`, {
|
||||
payload: {
|
||||
roles: {
|
||||
'role-1': {
|
||||
elasticsearch: {},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
feature_1: ['sub_feature_privilege_1'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'role-2': {
|
||||
elasticsearch: {},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
feature_1: ['sub_feature_privilege_1'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
apiResponses: {
|
||||
get: () => ({}),
|
||||
post: () => ({ created: ['role-1', 'role-2'] }),
|
||||
},
|
||||
asserts: {
|
||||
recordSubFeaturePrivilegeUsage: true,
|
||||
apiArguments: {
|
||||
get: [{ name: 'role-1,role-2' }, { ignore: [404] }],
|
||||
post: [
|
||||
{
|
||||
roles: {
|
||||
'role-1': {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
remote_indices: undefined,
|
||||
run_as: [],
|
||||
applications: [
|
||||
{
|
||||
application: 'kibana-.kibana',
|
||||
privileges: ['feature_feature_1.sub_feature_privilege_1'],
|
||||
resources: ['*'],
|
||||
},
|
||||
],
|
||||
metadata: undefined,
|
||||
},
|
||||
'role-2': {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
remote_indices: undefined,
|
||||
run_as: [],
|
||||
applications: [
|
||||
{
|
||||
application: 'kibana-.kibana',
|
||||
privileges: ['feature_feature_1.sub_feature_privilege_1'],
|
||||
resources: ['*'],
|
||||
},
|
||||
],
|
||||
metadata: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
statusCode: 200,
|
||||
result: { created: ['role-1', 'role-2'] },
|
||||
},
|
||||
});
|
||||
|
||||
postRolesTest(`creates roles with everything`, {
|
||||
payload: {
|
||||
roles: {
|
||||
'role-1': {
|
||||
description: 'role 1 test description',
|
||||
metadata: {
|
||||
foo: 'test-metadata',
|
||||
},
|
||||
elasticsearch: {
|
||||
cluster: ['test-cluster-privilege'],
|
||||
indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
|
||||
except: ['test-field-security-except-1', 'test-field-security-except-2'],
|
||||
},
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
query: `{ "match": { "title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
remote_indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
|
||||
except: ['test-field-security-except-1', 'test-field-security-except-2'],
|
||||
},
|
||||
clusters: ['test-cluster-name-1', 'test-cluster-name-2'],
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
query: `{ "match": { "title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
run_as: ['test-run-as-1', 'test-run-as-2'],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: ['all', 'read'],
|
||||
spaces: ['*'],
|
||||
},
|
||||
{
|
||||
base: ['all', 'read'],
|
||||
spaces: ['test-space-1', 'test-space-2'],
|
||||
},
|
||||
{
|
||||
feature: {
|
||||
foo: ['foo-privilege-1', 'foo-privilege-2'],
|
||||
},
|
||||
spaces: ['test-space-3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
'role-2': {
|
||||
description: 'role 2 test description',
|
||||
metadata: {
|
||||
foo: 'test-metadata',
|
||||
},
|
||||
elasticsearch: {
|
||||
cluster: ['test-cluster-privilege'],
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
{
|
||||
clusters: ['cluster3', 'cluster4'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
|
||||
except: ['test-field-security-except-1', 'test-field-security-except-2'],
|
||||
},
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
query: `{ "match": { "title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
remote_indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
|
||||
except: ['test-field-security-except-1', 'test-field-security-except-2'],
|
||||
},
|
||||
clusters: ['test-cluster-name-1', 'test-cluster-name-2'],
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
query: `{ "match": { "title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
run_as: ['test-run-as-1', 'test-run-as-2'],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: ['all', 'read'],
|
||||
spaces: ['test-space-1', 'test-space-2'],
|
||||
},
|
||||
{
|
||||
feature: {
|
||||
foo: ['foo-privilege-1', 'foo-privilege-2'],
|
||||
},
|
||||
spaces: ['test-space-3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
apiResponses: {
|
||||
get: () => ({}),
|
||||
post: () => ({ created: ['role-1', 'role-2'] }),
|
||||
},
|
||||
asserts: {
|
||||
apiArguments: {
|
||||
get: [{ name: 'role-1,role-2' }, { ignore: [404] }],
|
||||
post: [
|
||||
{
|
||||
roles: {
|
||||
'role-1': {
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['all', 'read'],
|
||||
resources: [GLOBAL_RESOURCE],
|
||||
},
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:test-space-1', 'space:test-space-2'],
|
||||
},
|
||||
{
|
||||
application,
|
||||
privileges: ['feature_foo.foo-privilege-1', 'feature_foo.foo-privilege-2'],
|
||||
resources: ['space:test-space-3'],
|
||||
},
|
||||
],
|
||||
cluster: ['test-cluster-privilege'],
|
||||
description: 'role 1 test description',
|
||||
indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
|
||||
except: ['test-field-security-except-1', 'test-field-security-except-2'],
|
||||
},
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
query: `{ "match": { "title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
remote_indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
|
||||
except: ['test-field-security-except-1', 'test-field-security-except-2'],
|
||||
},
|
||||
clusters: ['test-cluster-name-1', 'test-cluster-name-2'],
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
query: `{ "match": { "title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
metadata: { foo: 'test-metadata' },
|
||||
run_as: ['test-run-as-1', 'test-run-as-2'],
|
||||
},
|
||||
'role-2': {
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['space_all', 'space_read'],
|
||||
resources: ['space:test-space-1', 'space:test-space-2'],
|
||||
},
|
||||
{
|
||||
application,
|
||||
privileges: ['feature_foo.foo-privilege-1', 'feature_foo.foo-privilege-2'],
|
||||
resources: ['space:test-space-3'],
|
||||
},
|
||||
],
|
||||
cluster: ['test-cluster-privilege'],
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
{
|
||||
clusters: ['cluster3', 'cluster4'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
],
|
||||
description: 'role 2 test description',
|
||||
indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
|
||||
except: ['test-field-security-except-1', 'test-field-security-except-2'],
|
||||
},
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
query: `{ "match": { "title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
remote_indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
|
||||
except: ['test-field-security-except-1', 'test-field-security-except-2'],
|
||||
},
|
||||
clusters: ['test-cluster-name-1', 'test-cluster-name-2'],
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
query: `{ "match": { "title": "foo" } }`,
|
||||
},
|
||||
],
|
||||
metadata: { foo: 'test-metadata' },
|
||||
run_as: ['test-run-as-1', 'test-run-as-2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
statusCode: 200,
|
||||
result: { created: ['role-1', 'role-2'] },
|
||||
},
|
||||
});
|
||||
|
||||
postRolesTest(`updates roles which have existing other application privileges`, {
|
||||
payload: {
|
||||
roles: {
|
||||
'role-1': {
|
||||
elasticsearch: {
|
||||
cluster: ['test-cluster-privilege'],
|
||||
indices: [
|
||||
{
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
},
|
||||
],
|
||||
run_as: ['test-run-as-1', 'test-run-as-2'],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: ['all', 'read'],
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
'role-2': {
|
||||
elasticsearch: {
|
||||
cluster: ['test-cluster-privilege'],
|
||||
indices: [
|
||||
{
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
},
|
||||
],
|
||||
run_as: ['test-run-as-1', 'test-run-as-2'],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: ['all', 'read'],
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
apiResponses: {
|
||||
get: () => ({
|
||||
'role-1': {
|
||||
cluster: ['old-cluster-privilege'],
|
||||
indices: [],
|
||||
run_as: ['old-run-as'],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['old-kibana-privilege'],
|
||||
resources: ['old-resource'],
|
||||
},
|
||||
{
|
||||
application: 'logstash-foo',
|
||||
privileges: ['logstash-privilege'],
|
||||
resources: ['logstash-resource'],
|
||||
},
|
||||
],
|
||||
},
|
||||
'role-2': {
|
||||
cluster: ['old-cluster-privilege'],
|
||||
indices: [
|
||||
{
|
||||
names: ['old-index-name'],
|
||||
privileges: ['old-privilege'],
|
||||
},
|
||||
],
|
||||
run_as: ['old-run-as'],
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['old-kibana-privilege'],
|
||||
resources: ['old-resource'],
|
||||
},
|
||||
{
|
||||
application: 'beats-foo',
|
||||
privileges: ['beats-privilege'],
|
||||
resources: ['beats-resource'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
post: () => ({ updated: ['role-1', 'role-2'] }),
|
||||
},
|
||||
asserts: {
|
||||
apiArguments: {
|
||||
get: [{ name: 'role-1,role-2' }, { ignore: [404] }],
|
||||
post: [
|
||||
{
|
||||
roles: {
|
||||
'role-1': {
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['all', 'read'],
|
||||
resources: [GLOBAL_RESOURCE],
|
||||
},
|
||||
{
|
||||
application: 'logstash-foo',
|
||||
privileges: ['logstash-privilege'],
|
||||
resources: ['logstash-resource'],
|
||||
},
|
||||
],
|
||||
cluster: ['test-cluster-privilege'],
|
||||
indices: [
|
||||
{
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
},
|
||||
],
|
||||
run_as: ['test-run-as-1', 'test-run-as-2'],
|
||||
},
|
||||
'role-2': {
|
||||
applications: [
|
||||
{
|
||||
application,
|
||||
privileges: ['all', 'read'],
|
||||
resources: [GLOBAL_RESOURCE],
|
||||
},
|
||||
{
|
||||
application: 'beats-foo',
|
||||
privileges: ['beats-privilege'],
|
||||
resources: ['beats-resource'],
|
||||
},
|
||||
],
|
||||
cluster: ['test-cluster-privilege'],
|
||||
indices: [
|
||||
{
|
||||
names: ['test-index-name-1', 'test-index-name-2'],
|
||||
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
|
||||
},
|
||||
],
|
||||
run_as: ['test-run-as-1', 'test-run-as-2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
statusCode: 200,
|
||||
result: { updated: ['role-1', 'role-2'] },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 { roleGrantsSubFeaturePrivileges } from './lib';
|
||||
import {
|
||||
getBulkCreateOrUpdatePayloadSchema,
|
||||
transformPutPayloadToElasticsearchRole,
|
||||
} from './model';
|
||||
import type { RouteDefinitionParams } from '../..';
|
||||
import { wrapIntoCustomErrorResponse } from '../../../errors';
|
||||
import { validateKibanaPrivileges } from '../../../lib';
|
||||
import { createLicensedRouteHandler } from '../../licensed_route_handler';
|
||||
|
||||
type RolesErrorsDetails = Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
reason: string;
|
||||
}
|
||||
>;
|
||||
|
||||
interface ESRolesResponse {
|
||||
noop?: string[];
|
||||
created?: string[];
|
||||
updated?: string[];
|
||||
errors?: {
|
||||
count: number;
|
||||
details: RolesErrorsDetails;
|
||||
};
|
||||
}
|
||||
|
||||
export function defineBulkCreateOrUpdateRolesRoutes({
|
||||
router,
|
||||
authz,
|
||||
getFeatures,
|
||||
getFeatureUsageService,
|
||||
}: RouteDefinitionParams) {
|
||||
router.post(
|
||||
{
|
||||
path: '/api/security/roles',
|
||||
options: {
|
||||
summary: 'Create or update roles',
|
||||
},
|
||||
validate: {
|
||||
body: getBulkCreateOrUpdatePayloadSchema(() => {
|
||||
const privileges = authz.privileges.get();
|
||||
return {
|
||||
global: Object.keys(privileges.global),
|
||||
space: Object.keys(privileges.space),
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
try {
|
||||
const esClient = (await context.core).elasticsearch.client;
|
||||
const features = await getFeatures();
|
||||
|
||||
const { roles } = request.body;
|
||||
const validatedRolesNames = [];
|
||||
const kibanaErrors: RolesErrorsDetails = {};
|
||||
|
||||
for (const [roleName, role] of Object.entries(roles)) {
|
||||
const { validationErrors } = validateKibanaPrivileges(features, role.kibana);
|
||||
|
||||
if (validationErrors.length) {
|
||||
kibanaErrors[roleName] = {
|
||||
type: 'kibana_privilege_validation_exception',
|
||||
reason: `Role cannot be updated due to validation errors: ${JSON.stringify(
|
||||
validationErrors
|
||||
)}`,
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
validatedRolesNames.push(roleName);
|
||||
}
|
||||
|
||||
const rawRoles = await esClient.asCurrentUser.security.getRole(
|
||||
{ name: validatedRolesNames.join(',') },
|
||||
{ ignore: [404] }
|
||||
);
|
||||
|
||||
const esRolesPayload = Object.fromEntries(
|
||||
validatedRolesNames.map((roleName) => [
|
||||
roleName,
|
||||
transformPutPayloadToElasticsearchRole(
|
||||
roles[roleName],
|
||||
authz.applicationName,
|
||||
rawRoles[roleName] ? rawRoles[roleName].applications : []
|
||||
),
|
||||
])
|
||||
);
|
||||
|
||||
const esResponse = await esClient.asCurrentUser.transport.request<ESRolesResponse>({
|
||||
method: 'POST',
|
||||
path: '/_security/role',
|
||||
body: { roles: esRolesPayload },
|
||||
});
|
||||
|
||||
for (const roleName of [
|
||||
...(esResponse.created ?? []),
|
||||
...(esResponse.updated ?? []),
|
||||
...(esResponse.noop ?? []),
|
||||
]) {
|
||||
if (roleGrantsSubFeaturePrivileges(features, roles[roleName])) {
|
||||
getFeatureUsageService().recordSubFeaturePrivilegeUsage();
|
||||
}
|
||||
}
|
||||
|
||||
const { created, noop, updated, errors: esErrors } = esResponse;
|
||||
const hasAnyErrors = Object.keys(kibanaErrors).length || esErrors?.count;
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
created,
|
||||
noop,
|
||||
updated,
|
||||
...(hasAnyErrors && {
|
||||
errors: { ...kibanaErrors, ...(esErrors?.details ?? {}) },
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return response.customError(wrapIntoCustomErrorResponse(error));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
|
@ -6,36 +6,14 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { KibanaFeature } from '@kbn/features-plugin/common';
|
||||
|
||||
import type { RolePayloadSchemaType } from './model';
|
||||
import { roleGrantsSubFeaturePrivileges } from './lib';
|
||||
import { getPutPayloadSchema, transformPutPayloadToElasticsearchRole } from './model';
|
||||
import type { RouteDefinitionParams } from '../..';
|
||||
import { wrapIntoCustomErrorResponse } from '../../../errors';
|
||||
import { validateKibanaPrivileges } from '../../../lib';
|
||||
import { createLicensedRouteHandler } from '../../licensed_route_handler';
|
||||
|
||||
const roleGrantsSubFeaturePrivileges = (features: KibanaFeature[], role: RolePayloadSchemaType) => {
|
||||
if (!role.kibana) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const subFeaturePrivileges = new Map(
|
||||
features.map((feature) => [
|
||||
feature.id,
|
||||
feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2),
|
||||
])
|
||||
);
|
||||
|
||||
const hasAnySubFeaturePrivileges = role.kibana.some((kibanaPrivilege) =>
|
||||
Object.entries(kibanaPrivilege.feature ?? {}).some(([featureId, privileges]) => {
|
||||
return !!subFeaturePrivileges.get(featureId)?.some(({ id }) => privileges.includes(id));
|
||||
})
|
||||
);
|
||||
|
||||
return hasAnySubFeaturePrivileges;
|
||||
};
|
||||
|
||||
export function definePutRolesRoutes({
|
||||
router,
|
||||
authz,
|
||||
|
|
|
@ -21,5 +21,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./roles'));
|
||||
loadTestFile(require.resolve('./users'));
|
||||
loadTestFile(require.resolve('./privileges'));
|
||||
loadTestFile(require.resolve('./roles_bulk'));
|
||||
});
|
||||
}
|
||||
|
|
425
x-pack/test/api_integration/apis/security/roles_bulk.ts
Normal file
425
x-pack/test/api_integration/apis/security/roles_bulk.ts
Normal file
|
@ -0,0 +1,425 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const es = getService('es');
|
||||
const supertest = getService('supertest');
|
||||
const config = getService('config');
|
||||
const basic = config.get('esTestCluster.license') === 'basic';
|
||||
|
||||
describe('Roles Bulk', () => {
|
||||
after(async () => {
|
||||
await supertest.delete('/api/security/role/bulk_role_1').set('kbn-xsrf', 'xxx').expect(204);
|
||||
await supertest.delete('/api/security/role/bulk_role_2').set('kbn-xsrf', 'xxx').expect(204);
|
||||
await supertest
|
||||
.delete('/api/security/role/bulk_role_valid')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(204);
|
||||
await supertest
|
||||
.delete('/api/security/role/bulk_role_with_privilege_1')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(204);
|
||||
await supertest
|
||||
.delete('/api/security/role/bulk_role_with_privilege_2')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(204);
|
||||
await supertest
|
||||
.delete('/api/security/role/bulk_role_to_update_1')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(204);
|
||||
await supertest
|
||||
.delete('/api/security/role/bulk_role_to_update_2')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(204);
|
||||
|
||||
const emptyRoles = await es.security.getRole(
|
||||
{ name: 'bulk_role_1,bulk_role_2' },
|
||||
{ ignore: [404] }
|
||||
);
|
||||
expect(emptyRoles).to.eql({});
|
||||
const rolesWithPrivileges = await es.security.getRole(
|
||||
{ name: 'bulk_role_with_privilege_1,bulk_role_with_privilege_2,bulk_role_valid' },
|
||||
{ ignore: [404] }
|
||||
);
|
||||
expect(rolesWithPrivileges).to.eql({});
|
||||
|
||||
const rolesToUpdate = await es.security.getRole(
|
||||
{ name: 'bulk_role_to_update_1,bulk_role_to_update_2' },
|
||||
{ ignore: [404] }
|
||||
);
|
||||
expect(rolesToUpdate).to.eql({});
|
||||
});
|
||||
|
||||
describe('Create Roles', () => {
|
||||
it('should allow us to create empty roles', async () => {
|
||||
await supertest
|
||||
.post('/api/security/roles')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
roles: {
|
||||
bulk_role_1: {},
|
||||
bulk_role_2: {},
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body).to.eql({ created: ['bulk_role_1', 'bulk_role_2'] });
|
||||
});
|
||||
});
|
||||
|
||||
it('should create roles with kibana and elasticsearch privileges', async () => {
|
||||
await supertest
|
||||
.post('/api/security/roles')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
roles: {
|
||||
bulk_role_with_privilege_1: {
|
||||
elasticsearch: {
|
||||
cluster: ['manage'],
|
||||
indices: [
|
||||
{
|
||||
names: ['logstash-*'],
|
||||
privileges: ['read', 'view_index_metadata'],
|
||||
},
|
||||
],
|
||||
run_as: ['watcher_user'],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: ['read'],
|
||||
},
|
||||
{
|
||||
feature: {
|
||||
dashboard: ['read'],
|
||||
discover: ['all'],
|
||||
ml: ['all'],
|
||||
},
|
||||
spaces: ['marketing', 'sales'],
|
||||
},
|
||||
],
|
||||
},
|
||||
bulk_role_with_privilege_2: {
|
||||
elasticsearch: {
|
||||
cluster: ['manage'],
|
||||
indices: [
|
||||
{
|
||||
names: ['logstash-*'],
|
||||
privileges: ['read', 'view_index_metadata'],
|
||||
},
|
||||
],
|
||||
run_as: ['watcher_user'],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: ['read'],
|
||||
},
|
||||
{
|
||||
feature: {
|
||||
dashboard: ['read'],
|
||||
discover: ['all'],
|
||||
ml: ['all'],
|
||||
},
|
||||
spaces: ['marketing', 'sales'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const role = await es.security.getRole({ name: 'bulk_role_with_privilege_1' });
|
||||
expect(role).to.eql({
|
||||
bulk_role_with_privilege_1: {
|
||||
metadata: {},
|
||||
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'],
|
||||
},
|
||||
],
|
||||
run_as: ['watcher_user'],
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`should ${basic ? 'not' : ''} create a role with kibana and FLS/DLS elasticsearch
|
||||
privileges on ${basic ? 'basic' : 'trial'} licenses`, async () => {
|
||||
await supertest
|
||||
.post('/api/security/roles')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
roles: {
|
||||
role_with_privileges_dls_fls: {
|
||||
metadata: {
|
||||
foo: 'test-metadata',
|
||||
},
|
||||
elasticsearch: {
|
||||
cluster: ['manage'],
|
||||
indices: [
|
||||
{
|
||||
field_security: {
|
||||
grant: ['*'],
|
||||
except: ['geo.*'],
|
||||
},
|
||||
names: ['logstash-*'],
|
||||
privileges: ['read', 'view_index_metadata'],
|
||||
query: `{ "match": { "geo.src": "CN" } }`,
|
||||
},
|
||||
],
|
||||
run_as: ['watcher_user'],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const { errors, created } = response.body;
|
||||
if (basic) {
|
||||
expect(created).to.be(undefined);
|
||||
expect(errors).to.have.property('role_with_privileges_dls_fls');
|
||||
expect(errors.role_with_privileges_dls_fls.type).to.be('security_exception');
|
||||
expect(errors.role_with_privileges_dls_fls.reason).to.contain(
|
||||
`current license is non-compliant for [field and document level security]`
|
||||
);
|
||||
} else {
|
||||
expect(created).to.eql(['role_with_privileges_dls_fls']);
|
||||
expect(errors).to.be(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should return noop if roles exist and did not change', async () => {
|
||||
await supertest
|
||||
.post('/api/security/roles')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
roles: {
|
||||
bulk_role_1: {},
|
||||
bulk_role_2: {},
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body).to.eql({ noop: ['bulk_role_1', 'bulk_role_2'] });
|
||||
});
|
||||
});
|
||||
|
||||
it('should return validation errors for roles that failed', async () => {
|
||||
await supertest
|
||||
.post('/api/security/roles')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
roles: {
|
||||
bulk_role_es_invalid: {
|
||||
elasticsearch: {
|
||||
cluster: ['bla'],
|
||||
},
|
||||
},
|
||||
bulk_role_kibana_invalid: {
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['bar-space'],
|
||||
base: [],
|
||||
feature: {
|
||||
fleetv2: ['all', 'read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
bulk_role_valid: {
|
||||
elasticsearch: {
|
||||
cluster: ['all'],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const { created, errors } = response.body;
|
||||
expect(created).to.eql(['bulk_role_valid']);
|
||||
expect(errors).to.have.property('bulk_role_es_invalid');
|
||||
expect(errors).to.have.property('bulk_role_kibana_invalid');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update Roles', () => {
|
||||
it('should update roles with elasticsearch, kibana and other applications privileges', async () => {
|
||||
await es.security.putRole({
|
||||
name: 'bulk_role_to_update_1',
|
||||
body: {
|
||||
cluster: ['monitor'],
|
||||
indices: [
|
||||
{
|
||||
names: ['beats-*'],
|
||||
privileges: ['write'],
|
||||
},
|
||||
],
|
||||
applications: [
|
||||
{
|
||||
application: 'kibana-.kibana',
|
||||
privileges: ['read'],
|
||||
resources: ['*'],
|
||||
},
|
||||
{
|
||||
application: 'logstash-default',
|
||||
privileges: ['logstash-privilege'],
|
||||
resources: ['*'],
|
||||
},
|
||||
],
|
||||
run_as: ['reporting_user'],
|
||||
metadata: {
|
||||
bar: 'old-metadata',
|
||||
},
|
||||
},
|
||||
});
|
||||
await es.security.putRole({ name: 'bulk_role_to_update_2', body: {} });
|
||||
|
||||
await supertest
|
||||
.post('/api/security/roles')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
roles: {
|
||||
bulk_role_to_update_1: {
|
||||
metadata: {
|
||||
foo: 'test-metadata',
|
||||
},
|
||||
elasticsearch: {
|
||||
cluster: ['manage'],
|
||||
indices: [
|
||||
{
|
||||
names: ['logstash-*'],
|
||||
privileges: ['read', 'view_index_metadata'],
|
||||
allow_restricted_indices: true,
|
||||
},
|
||||
],
|
||||
run_as: ['watcher_user'],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
dashboard: ['read'],
|
||||
dev_tools: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
{
|
||||
base: ['all'],
|
||||
spaces: ['marketing', 'sales'],
|
||||
},
|
||||
],
|
||||
},
|
||||
bulk_role_to_update_2: {
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
dashboard: ['read'],
|
||||
dev_tools: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
{
|
||||
base: ['all'],
|
||||
spaces: ['observability', 'sales'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body).to.eql({
|
||||
updated: ['bulk_role_to_update_1', 'bulk_role_to_update_2'],
|
||||
});
|
||||
});
|
||||
|
||||
const role = await es.security.getRole({
|
||||
name: 'bulk_role_to_update_1,bulk_role_to_update_2',
|
||||
});
|
||||
|
||||
expect(role).to.eql({
|
||||
bulk_role_to_update_1: {
|
||||
cluster: ['manage'],
|
||||
indices: [
|
||||
{
|
||||
names: ['logstash-*'],
|
||||
privileges: ['read', 'view_index_metadata'],
|
||||
allow_restricted_indices: true,
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
bar: 'old-metadata',
|
||||
foo: 'test-metadata',
|
||||
},
|
||||
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: 'logstash-default',
|
||||
privileges: ['logstash-privilege'],
|
||||
resources: ['*'],
|
||||
},
|
||||
],
|
||||
run_as: ['watcher_user'],
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
bulk_role_to_update_2: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
application: 'kibana-.kibana',
|
||||
privileges: ['feature_dashboard.read', 'feature_dev_tools.all'],
|
||||
resources: ['*'],
|
||||
},
|
||||
{
|
||||
application: 'kibana-.kibana',
|
||||
privileges: ['space_all'],
|
||||
resources: ['space:observability', 'space:sales'],
|
||||
},
|
||||
],
|
||||
run_as: [],
|
||||
metadata: {},
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -21,5 +21,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./roles'));
|
||||
loadTestFile(require.resolve('./users'));
|
||||
loadTestFile(require.resolve('./privileges_basic'));
|
||||
loadTestFile(require.resolve('./roles_bulk'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue