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:
elena-shostak 2024-08-08 14:33:25 +02:00 committed by GitHub
parent ebaa751ad1
commit c8608461ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 2069 additions and 23 deletions

View file

@ -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[]

View 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

View file

@ -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);
}

View file

@ -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';

View file

@ -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;
};

View file

@ -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"`
);
});
});

View file

@ -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>
>;

View file

@ -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';

View file

@ -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'] },
},
});
});
});

View file

@ -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));
}
})
);
}

View file

@ -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,

View file

@ -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'));
});
}

View 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,
},
},
});
});
});
});
}

View file

@ -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'));
});
}