Set spaces and roles CRUD APIs to public (#193534)

Closes #192153

## Summary

This PR sets the spaces and roles CRUD operation HTTP API endpoints to
public in both stateful and serverless offerings, and additionally,
switches to the versioned router to register these endpoints.

Prior to this PR, the access level was not explicitly set, thus any
endpoints registered in serverless were by default internal. CRUD
operations for spaces and roles are being set to public to support the
rollout of custom roles in serverless, which coincides with enabling
multiple spaces.

### Note
- Currently, roles APIs are only available in serverless via a feature
flag (`xpack.security.roleManagementEnabled`)
- Spaces APIs are already registered in serverless, however, the maximum
number of spaces is by default 1, rendering create and delete operations
unusable. By overriding `xpack.spaces.maxSpaces` to a number greater
than 1 (stateful default is 1000), it will effectively enable use of the
spaces CRUD operations in serverless.

## Tests
-
x-pack/test_serverless/api_integration/test_suites/common/management/multiple_spaces_enabled.ts
-
x-pack/test_serverless/api_integration/test_suites/common/management/spaces.ts
-
x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts
-
x-pack/test_serverless/api_integration/test_suites/common/platform_security/roles_routes_feature_flag.ts
- Unit tests for each endpoint (to account for versioned router)
- Flaky Test Runner:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7002

## Manual Testing
1. Start ES & Kibana in serverless mode with config options to enable
role management and multiple spaces

Elasticsearch:
```
xpack.security.authc.native_roles.enabled: true
```
 KIbana:
```
 xpack.security.roleManagementEnabled: true
 xpack.spaces.maxSpaces: 100
```
3. Issue each CRUD HTTP API without including the internal origin header
('x-elastic-internal-origin') and verify you do not receive a 400 with
the message "method [get|post|put|delete] exists but is not available
with the current configuration"
4. Repeat steps 1 & 2 from the current head of main and verify that you
DO receive a 400 with the message "method [get|post|put|delete] exists
but is not available with the current configuration"

Regression testing - ensure that interfaces which leverage spaces and
roles APIs are functioning properly
- Spaces management
- Space navigation
- Roles management

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jeramy Soucy 2024-10-03 16:28:54 +02:00 committed by GitHub
parent c3364db26c
commit 26f2928b08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 5121 additions and 673 deletions

View file

@ -5,7 +5,7 @@ set -euo pipefail
source .buildkite/scripts/common/util.sh
echo --- Capture OAS snapshot
cmd="node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions"
cmd="node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces"
if is_pr && ! is_auto_commit_disabled; then
cmd="$cmd --update"
fi

File diff suppressed because it is too large Load diff

View file

@ -6263,6 +6263,323 @@
]
}
},
"/api/spaces/space": {
"get": {
"description": "Get all spaces",
"operationId": "%2Fapi%2Fspaces%2Fspace#0",
"parameters": [
{
"description": "The version of the API to use",
"in": "header",
"name": "elastic-api-version",
"schema": {
"default": "2023-10-31",
"enum": [
"2023-10-31"
],
"type": "string"
}
},
{
"in": "query",
"name": "purpose",
"required": false,
"schema": {
"enum": [
"any",
"copySavedObjectsIntoSpace",
"shareSavedObjectsIntoSpace"
],
"type": "string"
}
},
{
"in": "query",
"name": "include_authorized_purposes",
"required": true,
"schema": {
"anyOf": [
{
"items": {},
"type": "array"
},
{
"type": "boolean"
},
{
"type": "number"
},
{
"type": "object"
},
{
"type": "string"
}
],
"nullable": true,
"oneOf": [
{
"enum": [
false
],
"type": "boolean",
"x-oas-optional": true
},
{
"type": "boolean",
"x-oas-optional": true
}
]
}
}
],
"responses": {},
"summary": "",
"tags": [
"spaces"
]
},
"post": {
"description": "Create a space",
"operationId": "%2Fapi%2Fspaces%2Fspace#1",
"parameters": [
{
"description": "The version of the API to use",
"in": "header",
"name": "elastic-api-version",
"schema": {
"default": "2023-10-31",
"enum": [
"2023-10-31"
],
"type": "string"
}
},
{
"description": "A required header to protect against CSRF attacks",
"in": "header",
"name": "kbn-xsrf",
"required": true,
"schema": {
"example": "true",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"properties": {
"_reserved": {
"type": "boolean"
},
"color": {
"type": "string"
},
"description": {
"type": "string"
},
"disabledFeatures": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"id": {
"type": "string"
},
"imageUrl": {
"type": "string"
},
"initials": {
"maxLength": 2,
"type": "string"
},
"name": {
"minLength": 1,
"type": "string"
}
},
"required": [
"id",
"name"
],
"type": "object"
}
}
}
},
"responses": {},
"summary": "",
"tags": [
"spaces"
]
}
},
"/api/spaces/space/{id}": {
"delete": {
"description": "Delete a space",
"operationId": "%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2",
"parameters": [
{
"description": "The version of the API to use",
"in": "header",
"name": "elastic-api-version",
"schema": {
"default": "2023-10-31",
"enum": [
"2023-10-31"
],
"type": "string"
}
},
{
"description": "A required header to protect against CSRF attacks",
"in": "header",
"name": "kbn-xsrf",
"required": true,
"schema": {
"example": "true",
"type": "string"
}
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {},
"summary": "",
"tags": [
"spaces"
]
},
"get": {
"description": "Get a space",
"operationId": "%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0",
"parameters": [
{
"description": "The version of the API to use",
"in": "header",
"name": "elastic-api-version",
"schema": {
"default": "2023-10-31",
"enum": [
"2023-10-31"
],
"type": "string"
}
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {},
"summary": "",
"tags": [
"spaces"
]
},
"put": {
"description": "Update a space",
"operationId": "%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1",
"parameters": [
{
"description": "The version of the API to use",
"in": "header",
"name": "elastic-api-version",
"schema": {
"default": "2023-10-31",
"enum": [
"2023-10-31"
],
"type": "string"
}
},
{
"description": "A required header to protect against CSRF attacks",
"in": "header",
"name": "kbn-xsrf",
"required": true,
"schema": {
"example": "true",
"type": "string"
}
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"properties": {
"_reserved": {
"type": "boolean"
},
"color": {
"type": "string"
},
"description": {
"type": "string"
},
"disabledFeatures": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"id": {
"type": "string"
},
"imageUrl": {
"type": "string"
},
"initials": {
"maxLength": 2,
"type": "string"
},
"name": {
"minLength": 1,
"type": "string"
}
},
"required": [
"id",
"name"
],
"type": "object"
}
}
}
},
"responses": {},
"summary": "",
"tags": [
"spaces"
]
}
},
"/api/status": {
"get": {
"operationId": "%2Fapi%2Fstatus#0",
@ -6360,6 +6677,9 @@
{
"name": "connectors"
},
{
"name": "spaces"
},
{
"name": "system"
}

View file

@ -16241,6 +16241,214 @@ paths:
tags:
- Security AI Assistant API
- Prompts API
/api/spaces/space:
get:
description: Get all spaces
operationId: '%2Fapi%2Fspaces%2Fspace#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: query
name: purpose
required: false
schema:
enum:
- any
- copySavedObjectsIntoSpace
- shareSavedObjectsIntoSpace
type: string
- in: query
name: include_authorized_purposes
required: true
schema:
anyOf:
- items: {}
type: array
- type: boolean
- type: number
- type: object
- type: string
nullable: true
oneOf:
- enum:
- false
type: boolean
x-oas-optional: true
- type: boolean
x-oas-optional: true
responses: {}
summary: ''
tags:
- spaces
post:
description: Create a space
operationId: '%2Fapi%2Fspaces%2Fspace#1'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
_reserved:
type: boolean
color:
type: string
description:
type: string
disabledFeatures:
default: []
items:
type: string
type: array
id:
type: string
imageUrl:
type: string
initials:
maxLength: 2
type: string
name:
minLength: 1
type: string
required:
- id
- name
responses: {}
summary: ''
tags:
- spaces
/api/spaces/space/{id}:
delete:
description: Delete a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: id
required: true
schema:
type: string
responses: {}
summary: ''
tags:
- spaces
get:
description: Get a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: path
name: id
required: true
schema:
type: string
responses: {}
summary: ''
tags:
- spaces
put:
description: Update a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
_reserved:
type: boolean
color:
type: string
description:
type: string
disabledFeatures:
default: []
items:
type: string
type: array
id:
type: string
imageUrl:
type: string
initials:
maxLength: 2
type: string
name:
minLength: 1
type: string
required:
- id
- name
responses: {}
summary: ''
tags:
- spaces
/api/status:
get:
operationId: '%2Fapi%2Fstatus#0'
@ -33323,4 +33531,5 @@ tags:
name: Security Timeline API
- description: SLO APIs enable you to define, manage and track service-level objectives
name: slo
- name: spaces
- name: system

View file

@ -9860,6 +9860,214 @@ paths:
-X POST api/saved_objects/_import?createNewCopies=true
-H "kbn-xsrf: true"
--form file=@file.ndjson
/api/spaces/space:
get:
description: Get all spaces
operationId: '%2Fapi%2Fspaces%2Fspace#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: query
name: purpose
required: false
schema:
enum:
- any
- copySavedObjectsIntoSpace
- shareSavedObjectsIntoSpace
type: string
- in: query
name: include_authorized_purposes
required: true
schema:
anyOf:
- items: {}
type: array
- type: boolean
- type: number
- type: object
- type: string
nullable: true
oneOf:
- enum:
- false
type: boolean
x-oas-optional: true
- type: boolean
x-oas-optional: true
responses: {}
summary: ''
tags:
- spaces
post:
description: Create a space
operationId: '%2Fapi%2Fspaces%2Fspace#1'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
_reserved:
type: boolean
color:
type: string
description:
type: string
disabledFeatures:
default: []
items:
type: string
type: array
id:
type: string
imageUrl:
type: string
initials:
maxLength: 2
type: string
name:
minLength: 1
type: string
required:
- id
- name
responses: {}
summary: ''
tags:
- spaces
/api/spaces/space/{id}:
delete:
description: Delete a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: id
required: true
schema:
type: string
responses: {}
summary: ''
tags:
- spaces
get:
description: Get a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: path
name: id
required: true
schema:
type: string
responses: {}
summary: ''
tags:
- spaces
put:
description: Update a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
_reserved:
type: boolean
color:
type: string
description:
type: string
disabledFeatures:
default: []
items:
type: string
type: array
id:
type: string
imageUrl:
type: string
initials:
maxLength: 2
type: string
name:
minLength: 1
type: string
required:
- id
- name
responses: {}
summary: ''
tags:
- spaces
/api/status:
get:
operationId: '%2Fapi%2Fstatus#0'
@ -16476,4 +16684,5 @@ tags:
x-displayName: Saved objects
- description: SLO APIs enable you to define, manage and track service-level objectives
name: slo
- name: spaces
- name: system

View file

@ -20330,6 +20330,949 @@ paths:
tags:
- Security AI Assistant API
- Prompts API
/api/security/role:
get:
operationId: '%2Fapi%2Fsecurity%2Frole#0'
parameters: []
responses: {}
summary: Get all roles
tags:
- roles
/api/security/role/{name}:
delete:
operationId: '%2Fapi%2Fsecurity%2Frole%2F%7Bname%7D#1'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: name
required: true
schema:
minLength: 1
type: string
responses: {}
summary: Delete a role
tags:
- roles
get:
operationId: '%2Fapi%2Fsecurity%2Frole%2F%7Bname%7D#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: path
name: name
required: true
schema:
minLength: 1
type: string
responses: {}
summary: Get a role
tags:
- roles
put:
operationId: '%2Fapi%2Fsecurity%2Frole%2F%7Bname%7D#2'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: name
required: true
schema:
maxLength: 1024
minLength: 1
type: string
- in: query
name: createOnly
required: false
schema:
default: false
type: boolean
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
description:
maxLength: 2048
type: string
elasticsearch:
additionalProperties: false
type: object
properties:
cluster:
items:
type: string
type: array
indices:
items:
additionalProperties: false
type: object
properties:
allow_restricted_indices:
type: boolean
field_security:
additionalProperties:
items:
type: string
type: array
type: object
names:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
query:
type: string
required:
- names
- privileges
type: array
remote_cluster:
items:
additionalProperties: false
type: object
properties:
clusters:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
required:
- privileges
- clusters
type: array
remote_indices:
items:
additionalProperties: false
type: object
properties:
allow_restricted_indices:
type: boolean
clusters:
items:
type: string
minItems: 1
type: array
field_security:
additionalProperties:
items:
type: string
type: array
type: object
names:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
query:
type: string
required:
- clusters
- names
- privileges
type: array
run_as:
items:
type: string
type: array
kibana:
items:
additionalProperties: false
type: object
properties:
base:
anyOf:
- items: {}
type: array
- type: boolean
- type: number
- type: object
- type: string
nullable: true
oneOf:
- items:
type: string
type: array
- items:
type: string
type: array
feature:
additionalProperties:
items:
type: string
type: array
type: object
spaces:
anyOf:
- items:
enum:
- '*'
type: string
maxItems: 1
minItems: 1
type: array
- items:
type: string
type: array
default:
- '*'
required:
- base
type: array
metadata:
additionalProperties: {}
type: object
required:
- elasticsearch
responses: {}
summary: Create or update a role
tags:
- roles
/api/security/roles:
post:
operationId: '%2Fapi%2Fsecurity%2Froles#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
roles:
additionalProperties:
additionalProperties: false
type: object
properties:
description:
maxLength: 2048
type: string
elasticsearch:
additionalProperties: false
type: object
properties:
cluster:
items:
type: string
type: array
indices:
items:
additionalProperties: false
type: object
properties:
allow_restricted_indices:
type: boolean
field_security:
additionalProperties:
items:
type: string
type: array
type: object
names:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
query:
type: string
required:
- names
- privileges
type: array
remote_cluster:
items:
additionalProperties: false
type: object
properties:
clusters:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
required:
- privileges
- clusters
type: array
remote_indices:
items:
additionalProperties: false
type: object
properties:
allow_restricted_indices:
type: boolean
clusters:
items:
type: string
minItems: 1
type: array
field_security:
additionalProperties:
items:
type: string
type: array
type: object
names:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
query:
type: string
required:
- clusters
- names
- privileges
type: array
run_as:
items:
type: string
type: array
kibana:
items:
additionalProperties: false
type: object
properties:
base:
anyOf:
- items: {}
type: array
- type: boolean
- type: number
- type: object
- type: string
nullable: true
oneOf:
- items:
type: string
type: array
- items:
type: string
type: array
feature:
additionalProperties:
items:
type: string
type: array
type: object
spaces:
anyOf:
- items:
enum:
- '*'
type: string
maxItems: 1
minItems: 1
type: array
- items:
type: string
type: array
default:
- '*'
required:
- base
type: array
metadata:
additionalProperties: {}
type: object
required:
- elasticsearch
type: object
required:
- roles
responses: {}
summary: Create or update roles
tags:
- roles
/api/spaces/_copy_saved_objects:
post:
description: Copy saved objects to spaces
operationId: '%2Fapi%2Fspaces%2F_copy_saved_objects#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
compatibilityMode:
default: false
type: boolean
createNewCopies:
default: true
type: boolean
includeReferences:
default: false
type: boolean
objects:
items:
additionalProperties: false
type: object
properties:
id:
type: string
type:
type: string
required:
- type
- id
type: array
overwrite:
default: false
type: boolean
spaces:
items:
type: string
type: array
required:
- spaces
- objects
responses: {}
summary: ''
tags: []
/api/spaces/_disable_legacy_url_aliases:
post:
description: Disable legacy URL aliases
operationId: '%2Fapi%2Fspaces%2F_disable_legacy_url_aliases#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
aliases:
items:
additionalProperties: false
type: object
properties:
sourceId:
type: string
targetSpace:
type: string
targetType:
type: string
required:
- targetSpace
- targetType
- sourceId
type: array
required:
- aliases
responses: {}
summary: ''
tags: []
/api/spaces/_get_shareable_references:
post:
description: Get shareable references
operationId: '%2Fapi%2Fspaces%2F_get_shareable_references#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
objects:
items:
additionalProperties: false
type: object
properties:
id:
type: string
type:
type: string
required:
- type
- id
type: array
required:
- objects
responses: {}
summary: ''
tags: []
/api/spaces/_resolve_copy_saved_objects_errors:
post:
description: Resolve conflicts copying saved objects
operationId: '%2Fapi%2Fspaces%2F_resolve_copy_saved_objects_errors#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
compatibilityMode:
default: false
type: boolean
createNewCopies:
default: true
type: boolean
includeReferences:
default: false
type: boolean
objects:
items:
additionalProperties: false
type: object
properties:
id:
type: string
type:
type: string
required:
- type
- id
type: array
retries:
additionalProperties:
items:
additionalProperties: false
type: object
properties:
createNewCopy:
type: boolean
destinationId:
type: string
id:
type: string
ignoreMissingReferences:
type: boolean
overwrite:
default: false
type: boolean
type:
type: string
required:
- type
- id
type: array
type: object
required:
- retries
- objects
responses: {}
summary: ''
tags: []
/api/spaces/_update_objects_spaces:
post:
description: Update saved objects in spaces
operationId: '%2Fapi%2Fspaces%2F_update_objects_spaces#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
objects:
items:
additionalProperties: false
type: object
properties:
id:
type: string
type:
type: string
required:
- type
- id
type: array
spacesToAdd:
items:
type: string
type: array
spacesToRemove:
items:
type: string
type: array
required:
- objects
- spacesToAdd
- spacesToRemove
responses: {}
summary: ''
tags: []
/api/spaces/space:
get:
description: Get all spaces
operationId: '%2Fapi%2Fspaces%2Fspace#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: query
name: purpose
required: false
schema:
enum:
- any
- copySavedObjectsIntoSpace
- shareSavedObjectsIntoSpace
type: string
- in: query
name: include_authorized_purposes
required: true
schema:
anyOf:
- items: {}
type: array
- type: boolean
- type: number
- type: object
- type: string
nullable: true
oneOf:
- enum:
- false
type: boolean
x-oas-optional: true
- type: boolean
x-oas-optional: true
responses: {}
summary: ''
tags:
- spaces
post:
description: Create a space
operationId: '%2Fapi%2Fspaces%2Fspace#1'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
_reserved:
type: boolean
color:
type: string
description:
type: string
disabledFeatures:
default: []
items:
type: string
type: array
id:
type: string
imageUrl:
type: string
initials:
maxLength: 2
type: string
name:
minLength: 1
type: string
solution:
enum:
- security
- oblt
- es
- classic
type: string
required:
- id
- name
responses: {}
summary: ''
tags:
- spaces
/api/spaces/space/{id}:
delete:
description: Delete a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: id
required: true
schema:
type: string
responses: {}
summary: ''
tags:
- spaces
get:
description: Get a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: path
name: id
required: true
schema:
type: string
responses: {}
summary: ''
tags:
- spaces
put:
description: Update a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
_reserved:
type: boolean
color:
type: string
description:
type: string
disabledFeatures:
default: []
items:
type: string
type: array
id:
type: string
imageUrl:
type: string
initials:
maxLength: 2
type: string
name:
minLength: 1
type: string
solution:
enum:
- security
- oblt
- es
- classic
type: string
required:
- id
- name
responses: {}
summary: ''
tags:
- spaces
/api/status:
get:
operationId: '%2Fapi%2Fstatus#0'
@ -41270,6 +42213,7 @@ tags:
- name: Fleet uninstall tokens
- description: Machine learning
name: ml
- name: roles
- description: >
Export sets of saved objects that you want to import into {kib}, resolve
import errors, and rotate an encryption key for encrypted saved objects
@ -41325,4 +42269,5 @@ tags:
name: Security Timeline API
- description: SLO APIs enable you to define, manage and track service-level objectives
name: slo
- name: spaces
- name: system

View file

@ -13132,6 +13132,949 @@ paths:
summary: Resolve a saved object
tags:
- saved objects
/api/security/role:
get:
operationId: '%2Fapi%2Fsecurity%2Frole#0'
parameters: []
responses: {}
summary: Get all roles
tags:
- roles
/api/security/role/{name}:
delete:
operationId: '%2Fapi%2Fsecurity%2Frole%2F%7Bname%7D#1'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: name
required: true
schema:
minLength: 1
type: string
responses: {}
summary: Delete a role
tags:
- roles
get:
operationId: '%2Fapi%2Fsecurity%2Frole%2F%7Bname%7D#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: path
name: name
required: true
schema:
minLength: 1
type: string
responses: {}
summary: Get a role
tags:
- roles
put:
operationId: '%2Fapi%2Fsecurity%2Frole%2F%7Bname%7D#2'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: name
required: true
schema:
maxLength: 1024
minLength: 1
type: string
- in: query
name: createOnly
required: false
schema:
default: false
type: boolean
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
description:
maxLength: 2048
type: string
elasticsearch:
additionalProperties: false
type: object
properties:
cluster:
items:
type: string
type: array
indices:
items:
additionalProperties: false
type: object
properties:
allow_restricted_indices:
type: boolean
field_security:
additionalProperties:
items:
type: string
type: array
type: object
names:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
query:
type: string
required:
- names
- privileges
type: array
remote_cluster:
items:
additionalProperties: false
type: object
properties:
clusters:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
required:
- privileges
- clusters
type: array
remote_indices:
items:
additionalProperties: false
type: object
properties:
allow_restricted_indices:
type: boolean
clusters:
items:
type: string
minItems: 1
type: array
field_security:
additionalProperties:
items:
type: string
type: array
type: object
names:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
query:
type: string
required:
- clusters
- names
- privileges
type: array
run_as:
items:
type: string
type: array
kibana:
items:
additionalProperties: false
type: object
properties:
base:
anyOf:
- items: {}
type: array
- type: boolean
- type: number
- type: object
- type: string
nullable: true
oneOf:
- items:
type: string
type: array
- items:
type: string
type: array
feature:
additionalProperties:
items:
type: string
type: array
type: object
spaces:
anyOf:
- items:
enum:
- '*'
type: string
maxItems: 1
minItems: 1
type: array
- items:
type: string
type: array
default:
- '*'
required:
- base
type: array
metadata:
additionalProperties: {}
type: object
required:
- elasticsearch
responses: {}
summary: Create or update a role
tags:
- roles
/api/security/roles:
post:
operationId: '%2Fapi%2Fsecurity%2Froles#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
roles:
additionalProperties:
additionalProperties: false
type: object
properties:
description:
maxLength: 2048
type: string
elasticsearch:
additionalProperties: false
type: object
properties:
cluster:
items:
type: string
type: array
indices:
items:
additionalProperties: false
type: object
properties:
allow_restricted_indices:
type: boolean
field_security:
additionalProperties:
items:
type: string
type: array
type: object
names:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
query:
type: string
required:
- names
- privileges
type: array
remote_cluster:
items:
additionalProperties: false
type: object
properties:
clusters:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
required:
- privileges
- clusters
type: array
remote_indices:
items:
additionalProperties: false
type: object
properties:
allow_restricted_indices:
type: boolean
clusters:
items:
type: string
minItems: 1
type: array
field_security:
additionalProperties:
items:
type: string
type: array
type: object
names:
items:
type: string
minItems: 1
type: array
privileges:
items:
type: string
minItems: 1
type: array
query:
type: string
required:
- clusters
- names
- privileges
type: array
run_as:
items:
type: string
type: array
kibana:
items:
additionalProperties: false
type: object
properties:
base:
anyOf:
- items: {}
type: array
- type: boolean
- type: number
- type: object
- type: string
nullable: true
oneOf:
- items:
type: string
type: array
- items:
type: string
type: array
feature:
additionalProperties:
items:
type: string
type: array
type: object
spaces:
anyOf:
- items:
enum:
- '*'
type: string
maxItems: 1
minItems: 1
type: array
- items:
type: string
type: array
default:
- '*'
required:
- base
type: array
metadata:
additionalProperties: {}
type: object
required:
- elasticsearch
type: object
required:
- roles
responses: {}
summary: Create or update roles
tags:
- roles
/api/spaces/_copy_saved_objects:
post:
description: Copy saved objects to spaces
operationId: '%2Fapi%2Fspaces%2F_copy_saved_objects#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
compatibilityMode:
default: false
type: boolean
createNewCopies:
default: true
type: boolean
includeReferences:
default: false
type: boolean
objects:
items:
additionalProperties: false
type: object
properties:
id:
type: string
type:
type: string
required:
- type
- id
type: array
overwrite:
default: false
type: boolean
spaces:
items:
type: string
type: array
required:
- spaces
- objects
responses: {}
summary: ''
tags: []
/api/spaces/_disable_legacy_url_aliases:
post:
description: Disable legacy URL aliases
operationId: '%2Fapi%2Fspaces%2F_disable_legacy_url_aliases#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
aliases:
items:
additionalProperties: false
type: object
properties:
sourceId:
type: string
targetSpace:
type: string
targetType:
type: string
required:
- targetSpace
- targetType
- sourceId
type: array
required:
- aliases
responses: {}
summary: ''
tags: []
/api/spaces/_get_shareable_references:
post:
description: Get shareable references
operationId: '%2Fapi%2Fspaces%2F_get_shareable_references#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
objects:
items:
additionalProperties: false
type: object
properties:
id:
type: string
type:
type: string
required:
- type
- id
type: array
required:
- objects
responses: {}
summary: ''
tags: []
/api/spaces/_resolve_copy_saved_objects_errors:
post:
description: Resolve conflicts copying saved objects
operationId: '%2Fapi%2Fspaces%2F_resolve_copy_saved_objects_errors#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
compatibilityMode:
default: false
type: boolean
createNewCopies:
default: true
type: boolean
includeReferences:
default: false
type: boolean
objects:
items:
additionalProperties: false
type: object
properties:
id:
type: string
type:
type: string
required:
- type
- id
type: array
retries:
additionalProperties:
items:
additionalProperties: false
type: object
properties:
createNewCopy:
type: boolean
destinationId:
type: string
id:
type: string
ignoreMissingReferences:
type: boolean
overwrite:
default: false
type: boolean
type:
type: string
required:
- type
- id
type: array
type: object
required:
- retries
- objects
responses: {}
summary: ''
tags: []
/api/spaces/_update_objects_spaces:
post:
description: Update saved objects in spaces
operationId: '%2Fapi%2Fspaces%2F_update_objects_spaces#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
objects:
items:
additionalProperties: false
type: object
properties:
id:
type: string
type:
type: string
required:
- type
- id
type: array
spacesToAdd:
items:
type: string
type: array
spacesToRemove:
items:
type: string
type: array
required:
- objects
- spacesToAdd
- spacesToRemove
responses: {}
summary: ''
tags: []
/api/spaces/space:
get:
description: Get all spaces
operationId: '%2Fapi%2Fspaces%2Fspace#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: query
name: purpose
required: false
schema:
enum:
- any
- copySavedObjectsIntoSpace
- shareSavedObjectsIntoSpace
type: string
- in: query
name: include_authorized_purposes
required: true
schema:
anyOf:
- items: {}
type: array
- type: boolean
- type: number
- type: object
- type: string
nullable: true
oneOf:
- enum:
- false
type: boolean
x-oas-optional: true
- type: boolean
x-oas-optional: true
responses: {}
summary: ''
tags:
- spaces
post:
description: Create a space
operationId: '%2Fapi%2Fspaces%2Fspace#1'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
_reserved:
type: boolean
color:
type: string
description:
type: string
disabledFeatures:
default: []
items:
type: string
type: array
id:
type: string
imageUrl:
type: string
initials:
maxLength: 2
type: string
name:
minLength: 1
type: string
solution:
enum:
- security
- oblt
- es
- classic
type: string
required:
- id
- name
responses: {}
summary: ''
tags:
- spaces
/api/spaces/space/{id}:
delete:
description: Delete a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: id
required: true
schema:
type: string
responses: {}
summary: ''
tags:
- spaces
get:
description: Get a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: path
name: id
required: true
schema:
type: string
responses: {}
summary: ''
tags:
- spaces
put:
description: Update a space
operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
_reserved:
type: boolean
color:
type: string
description:
type: string
disabledFeatures:
default: []
items:
type: string
type: array
id:
type: string
imageUrl:
type: string
initials:
maxLength: 2
type: string
name:
minLength: 1
type: string
solution:
enum:
- security
- oblt
- es
- classic
type: string
required:
- id
- name
responses: {}
summary: ''
tags:
- spaces
/api/status:
get:
operationId: '%2Fapi%2Fstatus#0'
@ -23448,6 +24391,7 @@ tags:
- name: Fleet uninstall tokens
- description: Machine learning
name: ml
- name: roles
- description: >
Export sets of saved objects that you want to import into {kib}, resolve
import errors, and rotate an encryption key for encrypted saved objects
@ -23474,4 +24418,5 @@ tags:
x-displayName: Saved objects
- description: SLO APIs enable you to define, manage and track service-level objectives
name: slo
- name: spaces
- name: system

View file

@ -322,7 +322,10 @@ export interface HttpFetchOptions extends HttpRequestInit {
context?: KibanaExecutionContext;
/** @experimental */
/**
* When defined, the API version string used to populate the ELASTIC_HTTP_VERSION_HEADER.
* Defaults to undefined.
*/
version?: ApiVersion;
}

View file

@ -16,15 +16,19 @@ export class Role {
public async create(name: string, role: any) {
this.log.debug(`creating role ${name}`);
const { data, status, statusText } = await this.kibanaServer.request({
path: `/api/security/role/${name}`,
method: 'PUT',
body: {
kibana: role.kibana,
elasticsearch: role.elasticsearch,
},
retries: 0,
});
const { data, status, statusText } = await this.kibanaServer
.request({
path: `/api/security/role/${name}`,
method: 'PUT',
body: {
kibana: role.kibana,
elasticsearch: role.elasticsearch,
},
retries: 0,
})
.catch((e) => {
throw new Error(util.inspect(e.axiosError.response, true));
});
if (status !== 204) {
throw new Error(
`Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}`

View file

@ -13,6 +13,6 @@
"@kbn/test-subj-selector",
"@kbn/ftr-common-functional-services",
"@kbn/std",
"@kbn/expect"
"@kbn/expect",
]
}

View file

@ -120,6 +120,11 @@ export async function runTests(log: ToolingLog, options: RunTestsOptions) {
logsDir: options.logsDir,
installDir: options.installDir,
onEarlyExit,
extraKbnOpts: [
config.get('serverless')
? '--server.versioned.versionResolution=newest'
: '--server.versioned.versionResolution=oldest',
],
});
if (abortCtrl.signal.aborted) {

View file

@ -116,3 +116,14 @@ export const IMAGE_FILE_TYPES = ['image/svg+xml', 'image/jpeg', 'image/png', 'im
* Prefix for API actions.
*/
export const API_OPERATION_PREFIX = 'api:';
/**
* The API version numbers used with the versioned router.
*/
export const API_VERSIONS = {
roles: {
public: {
v1: '2023-10-31',
},
},
};

View file

@ -46,6 +46,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount';
import type { Cluster } from '@kbn/remote-clusters-plugin/public';
import { REMOTE_CLUSTERS_PATH } from '@kbn/remote-clusters-plugin/public';
import { KibanaPrivileges } from '@kbn/security-role-management-model';
import { API_VERSIONS as SPACES_API_VERSIONS } from '@kbn/spaces-plugin/common';
import type { Space, SpacesApiUi } from '@kbn/spaces-plugin/public';
import type { PublicMethodsOf } from '@kbn/utility-types';
@ -272,7 +273,7 @@ function useRole(
function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup) {
const [spaces, setSpaces] = useState<{ enabled: boolean; list: Space[] } | null>(null);
useEffect(() => {
http.get<Space[]>('/api/spaces/space').then(
http.get<Space[]>('/api/spaces/space', { version: SPACES_API_VERSIONS.public.v1 }).then(
(fetchedSpaces) => setSpaces({ enabled: true, list: fetchedSpaces }),
(err: IHttpFetchError) => {
// Spaces plugin can be disabled and hence this endpoint can be unavailable.

View file

@ -9,25 +9,31 @@ import type { HttpStart } from '@kbn/core/public';
import type { BulkUpdatePayload, BulkUpdateRoleResponse } from '@kbn/security-plugin-types-public';
import type { Role, RoleIndexPrivilege, RoleRemoteIndexPrivilege } from '../../../common';
import { API_VERSIONS } from '../../../common/constants';
import { copyRole } from '../../../common/model';
const version = API_VERSIONS.roles.public.v1;
export class RolesAPIClient {
constructor(private readonly http: HttpStart) {}
public getRoles = async () => {
return await this.http.get<Role[]>('/api/security/role');
return await this.http.get<Role[]>('/api/security/role', { version });
};
public getRole = async (roleName: string) => {
return await this.http.get<Role>(`/api/security/role/${encodeURIComponent(roleName)}`);
return await this.http.get<Role>(`/api/security/role/${encodeURIComponent(roleName)}`, {
version,
});
};
public deleteRole = async (roleName: string) => {
await this.http.delete(`/api/security/role/${encodeURIComponent(roleName)}`);
await this.http.delete(`/api/security/role/${encodeURIComponent(roleName)}`, { version });
};
public saveRole = async ({ role, createOnly = false }: { role: Role; createOnly?: boolean }) => {
await this.http.put(`/api/security/role/${encodeURIComponent(role.name)}`, {
version,
body: JSON.stringify(this.transformRoleForSave(copyRole(role))),
query: { createOnly },
});
@ -37,6 +43,7 @@ export class RolesAPIClient {
rolesUpdate,
}: BulkUpdatePayload): Promise<BulkUpdateRoleResponse> => {
return await this.http.post('/api/security/roles', {
version,
body: JSON.stringify({
roles: Object.fromEntries(
rolesUpdate.map((role) => [role.name, this.transformRoleForSave(copyRole(role))])

View file

@ -9,9 +9,11 @@ import Boom from '@hapi/boom';
import { kibanaResponseFactory } from '@kbn/core/server';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import type { MockedVersionedRouter } from '@kbn/core-http-router-server-mocks';
import type { LicenseCheck } from '@kbn/licensing-plugin/server';
import { defineDeleteRolesRoutes } from './delete';
import { API_VERSIONS } from '../../../../common/constants';
import { routeDefinitionParamsMock } from '../../index.mock';
interface TestOptions {
@ -28,6 +30,8 @@ describe('DELETE role', () => {
) => {
test(description, async () => {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
const versionedRouterMock = mockRouteDefinitionParams.router
.versioned as MockedVersionedRouter;
const mockCoreContext = coreMock.createRequestHandlerContext();
const mockLicensingContext = {
license: { check: jest.fn().mockReturnValue(licenseCheckResult) },
@ -44,7 +48,9 @@ describe('DELETE role', () => {
}
defineDeleteRolesRoutes(mockRouteDefinitionParams);
const [[, handler]] = mockRouteDefinitionParams.router.delete.mock.calls;
const handler = versionedRouterMock.getRoute('delete', '/api/security/role/{name}').versions[
API_VERSIONS.roles.public.v1
].handler;
const headers = { authorization: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({

View file

@ -8,32 +8,40 @@
import { schema } from '@kbn/config-schema';
import type { RouteDefinitionParams } from '../..';
import { API_VERSIONS } from '../../../../common/constants';
import { wrapIntoCustomErrorResponse } from '../../../errors';
import { createLicensedRouteHandler } from '../../licensed_route_handler';
export function defineDeleteRolesRoutes({ router }: RouteDefinitionParams) {
router.delete(
{
router.versioned
.delete({
path: '/api/security/role/{name}',
access: 'public',
summary: `Delete a role`,
options: {
access: 'public',
summary: `Delete a role`,
tags: ['oas-tag:roles'],
},
validate: {
params: schema.object({ name: schema.string({ minLength: 1 }) }),
},
},
createLicensedRouteHandler(async (context, request, response) => {
try {
const esClient = (await context.core).elasticsearch.client;
await esClient.asCurrentUser.security.deleteRole({
name: request.params.name,
});
return response.noContent();
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
})
);
.addVersion(
{
version: API_VERSIONS.roles.public.v1,
validate: {
request: {
params: schema.object({ name: schema.string({ minLength: 1 }) }),
},
},
},
createLicensedRouteHandler(async (context, request, response) => {
try {
const esClient = (await context.core).elasticsearch.client;
await esClient.asCurrentUser.security.deleteRole({
name: request.params.name,
});
return response.noContent();
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
})
);
}

View file

@ -9,9 +9,11 @@ import Boom from '@hapi/boom';
import { kibanaResponseFactory } from '@kbn/core/server';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import type { MockedVersionedRouter } from '@kbn/core-http-router-server-mocks';
import type { LicenseCheck } from '@kbn/licensing-plugin/server';
import { defineGetRolesRoutes } from './get';
import { API_VERSIONS } from '../../../../common/constants';
import { routeDefinitionParamsMock } from '../../index.mock';
const application = 'kibana-.kibana';
@ -31,6 +33,8 @@ describe('GET role', () => {
) => {
test(description, async () => {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
const versionedRouterMock = mockRouteDefinitionParams.router
.versioned as MockedVersionedRouter;
mockRouteDefinitionParams.authz.applicationName = application;
mockRouteDefinitionParams.getFeatures = jest.fn().mockResolvedValue([]);
@ -50,7 +54,9 @@ describe('GET role', () => {
}
defineGetRolesRoutes(mockRouteDefinitionParams);
const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls;
const handler = versionedRouterMock.getRoute('get', '/api/security/role/{name}').versions[
API_VERSIONS.roles.public.v1
].handler;
const headers = { authorization: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({

View file

@ -8,6 +8,7 @@
import { schema } from '@kbn/config-schema';
import type { RouteDefinitionParams } from '../..';
import { API_VERSIONS } from '../../../../common/constants';
import { transformElasticsearchRoleToRole } from '../../../authorization';
import { wrapIntoCustomErrorResponse } from '../../../errors';
import { createLicensedRouteHandler } from '../../licensed_route_handler';
@ -18,47 +19,54 @@ export function defineGetRolesRoutes({
getFeatures,
logger,
}: RouteDefinitionParams) {
router.get(
{
router.versioned
.get({
path: '/api/security/role/{name}',
access: 'public',
summary: `Get a role`,
options: {
access: 'public',
summary: `Get a role`,
tags: ['oas-tag:roles'],
},
validate: {
params: schema.object({ name: schema.string({ minLength: 1 }) }),
},
},
createLicensedRouteHandler(async (context, request, response) => {
try {
const esClient = (await context.core).elasticsearch.client;
const [features, elasticsearchRoles] = await Promise.all([
getFeatures(),
await esClient.asCurrentUser.security.getRole({
name: request.params.name,
}),
]);
const elasticsearchRole = elasticsearchRoles[request.params.name];
if (elasticsearchRole) {
return response.ok({
body: transformElasticsearchRoleToRole(
features,
// @ts-expect-error `SecurityIndicesPrivileges.names` expected to be `string[]`
elasticsearchRole,
request.params.name,
authz.applicationName,
logger
),
});
}
return response.notFound();
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
})
);
.addVersion(
{
version: API_VERSIONS.roles.public.v1,
validate: {
request: {
params: schema.object({ name: schema.string({ minLength: 1 }) }),
},
},
},
createLicensedRouteHandler(async (context, request, response) => {
try {
const esClient = (await context.core).elasticsearch.client;
const [features, elasticsearchRoles] = await Promise.all([
getFeatures(),
await esClient.asCurrentUser.security.getRole({
name: request.params.name,
}),
]);
const elasticsearchRole = elasticsearchRoles[request.params.name];
if (elasticsearchRole) {
return response.ok({
body: transformElasticsearchRoleToRole(
features,
// @ts-expect-error `SecurityIndicesPrivileges.names` expected to be `string[]`
elasticsearchRole,
request.params.name,
authz.applicationName,
logger
),
});
}
return response.notFound();
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
})
);
}

View file

@ -9,9 +9,11 @@ import Boom from '@hapi/boom';
import { kibanaResponseFactory } from '@kbn/core/server';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import type { MockedVersionedRouter } from '@kbn/core-http-router-server-mocks';
import type { LicenseCheck } from '@kbn/licensing-plugin/server';
import { defineGetAllRolesRoutes } from './get_all';
import { API_VERSIONS } from '../../../../common/constants';
import { routeDefinitionParamsMock } from '../../index.mock';
const application = 'kibana-.kibana';
@ -31,6 +33,8 @@ describe('GET all roles', () => {
) => {
test(description, async () => {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
const versionedRouterMock = mockRouteDefinitionParams.router
.versioned as MockedVersionedRouter;
mockRouteDefinitionParams.authz.applicationName = application;
mockRouteDefinitionParams.getFeatures = jest.fn().mockResolvedValue([]);
@ -50,7 +54,9 @@ describe('GET all roles', () => {
}
defineGetAllRolesRoutes(mockRouteDefinitionParams);
const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls;
const handler = versionedRouterMock.getRoute('get', '/api/security/role').versions[
API_VERSIONS.roles.public.v1
].handler;
const headers = { authorization: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({

View file

@ -6,6 +6,7 @@
*/
import type { RouteDefinitionParams } from '../..';
import { API_VERSIONS } from '../../../../common/constants';
import { compareRolesByName, transformElasticsearchRoleToRole } from '../../../authorization';
import { wrapIntoCustomErrorResponse } from '../../../errors';
import { createLicensedRouteHandler } from '../../licensed_route_handler';
@ -18,45 +19,50 @@ export function defineGetAllRolesRoutes({
buildFlavor,
config,
}: RouteDefinitionParams) {
router.get(
{
router.versioned
.get({
path: '/api/security/role',
access: 'public',
summary: `Get all roles`,
options: {
access: 'public',
summary: `Get all roles`,
tags: ['oas-tag:roles'],
},
validate: false,
},
createLicensedRouteHandler(async (context, request, response) => {
try {
const hideReservedRoles = buildFlavor === 'serverless';
const esClient = (await context.core).elasticsearch.client;
const [features, elasticsearchRoles] = await Promise.all([
getFeatures(),
await esClient.asCurrentUser.security.getRole(),
]);
// Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name.
return response.ok({
body: Object.entries(elasticsearchRoles)
.map(([roleName, elasticsearchRole]) =>
transformElasticsearchRoleToRole(
features,
// @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[]
elasticsearchRole,
roleName,
authz.applicationName,
logger
)
)
.filter((role) => {
return !hideReservedRoles || !role.metadata?._reserved;
})
.sort(compareRolesByName),
});
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
})
);
.addVersion(
{
version: API_VERSIONS.roles.public.v1,
validate: false,
},
createLicensedRouteHandler(async (context, request, response) => {
try {
const hideReservedRoles = buildFlavor === 'serverless';
const esClient = (await context.core).elasticsearch.client;
const [features, elasticsearchRoles] = await Promise.all([
getFeatures(),
await esClient.asCurrentUser.security.getRole(),
]);
// Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name.
return response.ok({
body: Object.entries(elasticsearchRoles)
.map(([roleName, elasticsearchRole]) =>
transformElasticsearchRoleToRole(
features,
// @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[]
elasticsearchRole,
roleName,
authz.applicationName,
logger
)
)
.filter((role) => {
return !hideReservedRoles || !role.metadata?._reserved;
})
.sort(compareRolesByName),
});
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
})
);
}

View file

@ -7,12 +7,14 @@
import { kibanaResponseFactory } from '@kbn/core/server';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import type { MockedVersionedRouter } from '@kbn/core-http-router-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 { API_VERSIONS } from '../../../../common/constants';
import { securityFeatureUsageServiceMock } from '../../../feature_usage/index.mock';
import { routeDefinitionParamsMock } from '../../index.mock';
@ -89,6 +91,7 @@ const postRolesTest = (
) => {
test(description, async () => {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
const versionedRouterMock = mockRouteDefinitionParams.router.versioned as MockedVersionedRouter;
mockRouteDefinitionParams.authz.applicationName = application;
mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap);
@ -158,13 +161,15 @@ const postRolesTest = (
);
defineBulkCreateOrUpdateRolesRoutes(mockRouteDefinitionParams);
const [[{ validate }, handler]] = mockRouteDefinitionParams.router.post.mock.calls;
const { handler, config } = versionedRouterMock.getRoute('post', '/api/security/roles')
.versions[API_VERSIONS.roles.public.v1];
const headers = { authorization: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({
method: 'post',
path: '/api/security/roles',
body: payload !== undefined ? (validate as any).body.validate(payload) : undefined,
body:
payload !== undefined ? (config.validate as any).request.body.validate(payload) : undefined,
headers,
});

View file

@ -11,6 +11,7 @@ import {
transformPutPayloadToElasticsearchRole,
} from './model';
import type { RouteDefinitionParams } from '../..';
import { API_VERSIONS } from '../../../../common/constants';
import { wrapIntoCustomErrorResponse } from '../../../errors';
import { validateKibanaPrivileges } from '../../../lib';
import { createLicensedRouteHandler } from '../../licensed_route_handler';
@ -39,97 +40,104 @@ export function defineBulkCreateOrUpdateRolesRoutes({
getFeatures,
getFeatureUsageService,
}: RouteDefinitionParams) {
router.post(
{
router.versioned
.post({
path: '/api/security/roles',
access: 'public',
summary: 'Create or update roles',
options: {
access: 'public',
summary: 'Create or update roles',
tags: ['oas-tag: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 ?? {}) },
})
.addVersion(
{
version: API_VERSIONS.roles.public.v1,
validate: {
request: {
body: getBulkCreateOrUpdatePayloadSchema(() => {
const privileges = authz.privileges.get();
return {
global: Object.keys(privileges.global),
space: Object.keys(privileges.space),
};
}),
},
});
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
})
);
},
},
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

@ -8,11 +8,13 @@
import type { Type } from '@kbn/config-schema';
import { kibanaResponseFactory } from '@kbn/core/server';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import type { MockedVersionedRouter } from '@kbn/core-http-router-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 { definePutRolesRoutes } from './put';
import { API_VERSIONS } from '../../../../common/constants';
import { securityFeatureUsageServiceMock } from '../../../feature_usage/index.mock';
import { routeDefinitionParamsMock } from '../../index.mock';
@ -74,6 +76,7 @@ const putRoleTest = (
) => {
test(description, async () => {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
const versionedRouterMock = mockRouteDefinitionParams.router.versioned as MockedVersionedRouter;
mockRouteDefinitionParams.authz.applicationName = application;
mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap);
@ -143,7 +146,8 @@ const putRoleTest = (
);
definePutRolesRoutes(mockRouteDefinitionParams);
const [[{ validate }, handler]] = mockRouteDefinitionParams.router.put.mock.calls;
const { handler, config } = versionedRouterMock.getRoute('put', '/api/security/role/{name}')
.versions[API_VERSIONS.roles.public.v1];
const headers = { authorization: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({
@ -151,7 +155,8 @@ const putRoleTest = (
path: `/api/security/role/${name}`,
query: { createOnly },
params: { name },
body: payload !== undefined ? (validate as any).body.validate(payload) : undefined,
body:
payload !== undefined ? (config.validate as any).request.body.validate(payload) : undefined,
headers,
});
@ -188,11 +193,15 @@ describe('PUT role', () => {
let requestParamsSchema: Type<any>;
beforeEach(() => {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
const versionedRouterMock = mockRouteDefinitionParams.router
.versioned as MockedVersionedRouter;
mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap);
definePutRolesRoutes(mockRouteDefinitionParams);
const [[{ validate }]] = mockRouteDefinitionParams.router.put.mock.calls;
requestParamsSchema = (validate as any).params;
const { config } = versionedRouterMock.getRoute('put', '/api/security/role/{name}').versions[
API_VERSIONS.roles.public.v1
];
requestParamsSchema = (config.validate as any).request.params;
});
test('requires name in params', () => {

View file

@ -10,6 +10,7 @@ import { schema } from '@kbn/config-schema';
import { roleGrantsSubFeaturePrivileges } from './lib';
import { getPutPayloadSchema, transformPutPayloadToElasticsearchRole } from './model';
import type { RouteDefinitionParams } from '../..';
import { API_VERSIONS } from '../../../../common/constants';
import { wrapIntoCustomErrorResponse } from '../../../errors';
import { validateKibanaPrivileges } from '../../../lib';
import { createLicensedRouteHandler } from '../../licensed_route_handler';
@ -20,75 +21,85 @@ export function definePutRolesRoutes({
getFeatures,
getFeatureUsageService,
}: RouteDefinitionParams) {
router.put(
{
router.versioned
.put({
path: '/api/security/role/{name}',
access: 'public',
summary: `Create or update a role`,
options: {
access: 'public',
summary: `Create or update a role`,
tags: ['oas-tag:roles'],
},
validate: {
params: schema.object({ name: schema.string({ minLength: 1, maxLength: 1024 }) }),
query: schema.object({ createOnly: schema.boolean({ defaultValue: false }) }),
body: getPutPayloadSchema(() => {
const privileges = authz.privileges.get();
return {
global: Object.keys(privileges.global),
space: Object.keys(privileges.space),
};
}),
},
},
createLicensedRouteHandler(async (context, request, response) => {
const { name } = request.params;
const { createOnly } = request.query;
try {
const esClient = (await context.core).elasticsearch.client;
const [features, rawRoles] = await Promise.all([
getFeatures(),
esClient.asCurrentUser.security.getRole({ name: request.params.name }, { ignore: [404] }),
]);
const { validationErrors } = validateKibanaPrivileges(features, request.body.kibana);
if (validationErrors.length) {
return response.badRequest({
body: {
message: `Role cannot be updated due to validation errors: ${JSON.stringify(
validationErrors
)}`,
},
});
}
if (createOnly && !!rawRoles[name]) {
return response.conflict({
body: {
message: `Role already exists and cannot be created: ${name}`,
},
});
}
const body = transformPutPayloadToElasticsearchRole(
request.body,
authz.applicationName,
rawRoles[name] ? rawRoles[name].applications : []
);
await esClient.asCurrentUser.security.putRole({
name: request.params.name,
body,
});
if (roleGrantsSubFeaturePrivileges(features, request.body)) {
getFeatureUsageService().recordSubFeaturePrivilegeUsage();
}
return response.noContent();
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
})
);
.addVersion(
{
version: API_VERSIONS.roles.public.v1,
validate: {
request: {
params: schema.object({ name: schema.string({ minLength: 1, maxLength: 1024 }) }),
query: schema.object({ createOnly: schema.boolean({ defaultValue: false }) }),
body: getPutPayloadSchema(() => {
const privileges = authz.privileges.get();
return {
global: Object.keys(privileges.global),
space: Object.keys(privileges.space),
};
}),
},
},
},
createLicensedRouteHandler(async (context, request, response) => {
const { name } = request.params;
const { createOnly } = request.query;
try {
const esClient = (await context.core).elasticsearch.client;
const [features, rawRoles] = await Promise.all([
getFeatures(),
esClient.asCurrentUser.security.getRole(
{ name: request.params.name },
{ ignore: [404] }
),
]);
const { validationErrors } = validateKibanaPrivileges(features, request.body.kibana);
if (validationErrors.length) {
return response.badRequest({
body: {
message: `Role cannot be updated due to validation errors: ${JSON.stringify(
validationErrors
)}`,
},
});
}
if (createOnly && !!rawRoles[name]) {
return response.conflict({
body: {
message: `Role already exists and cannot be created: ${name}`,
},
});
}
const body = transformPutPayloadToElasticsearchRole(
request.body,
authz.applicationName,
rawRoles[name] ? rawRoles[name].applications : []
);
await esClient.asCurrentUser.security.putRole({
name: request.params.name,
body,
});
if (roleGrantsSubFeaturePrivileges(features, request.body)) {
getFeatureUsageService().recordSubFeaturePrivilegeUsage();
}
return response.noContent();
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
})
);
}

View file

@ -86,6 +86,7 @@
"@kbn/security-authorization-core",
"@kbn/security-role-management-model",
"@kbn/security-ui-components",
"@kbn/core-http-router-server-mocks",
],
"exclude": [
"target/**/*",

View file

@ -43,3 +43,12 @@ export const SOLUTION_VIEW_CLASSIC = 'classic' as const;
export const FEATURE_PRIVILEGES_ALL = 'all' as const;
export const FEATURE_PRIVILEGES_READ = 'read' as const;
export const FEATURE_PRIVILEGES_CUSTOM = 'custom' as const;
/**
* The API version numbers used with the versioned router.
*/
export const API_VERSIONS = {
public: {
v1: '2023-10-31',
},
};

View file

@ -11,6 +11,7 @@ export {
SPACE_SEARCH_COUNT_THRESHOLD,
ENTER_SPACE_PATH,
DEFAULT_SPACE_ID,
API_VERSIONS,
} from './constants';
export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser';
export type {

View file

@ -13,7 +13,12 @@ import type { SavedObjectsCollectMultiNamespaceReferencesResponse } from '@kbn/c
import type { LegacyUrlAliasTarget } from '@kbn/core-saved-objects-common';
import type { Role } from '@kbn/security-plugin-types-common';
import type { GetAllSpacesOptions, GetSpaceResult, Space } from '../../common';
import {
API_VERSIONS,
type GetAllSpacesOptions,
type GetSpaceResult,
type Space,
} from '../../common';
import type { CopySavedObjectsToSpaceResponse } from '../copy_saved_objects_to_space/types';
import type { SpaceContentTypeSummaryItem } from '../types';
@ -23,6 +28,7 @@ interface SavedObjectTarget {
}
const TAG_TYPE = 'tag';
const version = API_VERSIONS.public.v1;
export class SpacesManager {
private activeSpace$: BehaviorSubject<Space | null> = new BehaviorSubject<Space | null>(null);
@ -49,11 +55,11 @@ export class SpacesManager {
public async getSpaces(options: GetAllSpacesOptions = {}): Promise<GetSpaceResult[]> {
const { purpose, includeAuthorizedPurposes } = options;
const query = { purpose, include_authorized_purposes: includeAuthorizedPurposes };
return await this.http.get('/api/spaces/space', { query });
return await this.http.get('/api/spaces/space', { query, version });
}
public async getSpace(id: string): Promise<Space> {
return await this.http.get(`/api/spaces/space/${encodeURIComponent(id)}`);
return await this.http.get(`/api/spaces/space/${encodeURIComponent(id)}`, { version });
}
public async getActiveSpace({ forceRefresh = false } = {}) {
@ -69,6 +75,7 @@ export class SpacesManager {
public async createSpace(space: Space) {
await this.http.post(`/api/spaces/space`, {
body: JSON.stringify(space),
version,
});
}
@ -78,6 +85,7 @@ export class SpacesManager {
overwrite: true,
},
body: JSON.stringify(space),
version,
});
const activeSpaceId = (await this.getActiveSpace()).id;
@ -88,7 +96,7 @@ export class SpacesManager {
}
public async deleteSpace(space: Space) {
await this.http.delete(`/api/spaces/space/${encodeURIComponent(space.id)}`);
await this.http.delete(`/api/spaces/space/${encodeURIComponent(space.id)}`, { version });
}
public async disableLegacyUrlAliases(aliases: LegacyUrlAliasTarget[]) {

View file

@ -16,9 +16,11 @@ import {
httpServiceMock,
loggingSystemMock,
} from '@kbn/core/server/mocks';
import type { MockedVersionedRouter } from '@kbn/core-http-router-server-mocks';
import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
import { initDeleteSpacesApi } from './delete';
import { API_VERSIONS } from '../../../../common';
import { spacesConfig } from '../../../lib/__fixtures__';
import { SpacesClientService } from '../../../spaces_client';
import { SpacesService } from '../../../spaces_service';
@ -36,7 +38,7 @@ describe('Spaces Public API', () => {
const setup = async () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter();
const versionedRouterMock = router.versioned as MockedVersionedRouter;
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects);
const log = loggingSystemMock.create().get('spaces');
@ -71,10 +73,13 @@ describe('Spaces Public API', () => {
isServerless: false,
});
const [routeDefinition, routeHandler] = router.delete.mock.calls[0];
const { handler: routeHandler, config } = versionedRouterMock.getRoute(
'delete',
'/api/spaces/space/{id}'
).versions[API_VERSIONS.public.v1];
return {
routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>,
routeValidation: (config.validate as any).request as RouteValidatorConfig<{}, {}, {}>,
routeHandler,
savedObjectsRepositoryMock,
};

View file

@ -11,47 +11,55 @@ import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import type { ExternalRouteDeps } from '.';
import { API_VERSIONS } from '../../../../common';
import { wrapError } from '../../../lib/errors';
import { createLicensedRouteHandler } from '../../lib';
export function initDeleteSpacesApi(deps: ExternalRouteDeps) {
const { router, log, getSpacesService, isServerless } = deps;
const { router, log, getSpacesService } = deps;
router.delete(
{
router.versioned
.delete({
path: '/api/spaces/space/{id}',
access: 'public',
description: `Delete a space`,
options: {
access: isServerless ? 'internal' : 'public',
description: `Delete a space`,
tags: ['oas-tag:spaces'],
},
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
createLicensedRouteHandler(async (context, request, response) => {
const spacesClient = getSpacesService().createSpacesClient(request);
const id = request.params.id;
try {
await spacesClient.delete(id);
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();
} else if (SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)) {
log.error(
`Failed to delete space '${id}', cannot execute script in Elasticsearch query: ${error.message}`
);
return response.customError(
wrapError(Boom.badRequest('Cannot execute script in Elasticsearch query'))
);
}
return response.customError(wrapError(error));
}
return response.noContent();
})
);
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {
request: {
params: schema.object({
id: schema.string(),
}),
},
},
},
createLicensedRouteHandler(async (context, request, response) => {
const spacesClient = getSpacesService().createSpacesClient(request);
const id = request.params.id;
try {
await spacesClient.delete(id);
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();
} else if (SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)) {
log.error(
`Failed to delete space '${id}', cannot execute script in Elasticsearch query: ${error.message}`
);
return response.customError(
wrapError(Boom.badRequest('Cannot execute script in Elasticsearch query'))
);
}
return response.customError(wrapError(error));
}
return response.noContent();
})
);
}

View file

@ -14,9 +14,11 @@ import {
httpServiceMock,
loggingSystemMock,
} from '@kbn/core/server/mocks';
import type { MockedVersionedRouter } from '@kbn/core-http-router-server-mocks';
import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
import { initGetSpaceApi } from './get';
import { API_VERSIONS } from '../../../../common';
import { spacesConfig } from '../../../lib/__fixtures__';
import { SpacesClientService } from '../../../spaces_client';
import { SpacesService } from '../../../spaces_service';
@ -35,6 +37,7 @@ describe('GET space', () => {
const setup = async () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter();
const versionedRouterMock = router.versioned as MockedVersionedRouter;
const coreStart = coreMock.createStart();
@ -70,8 +73,12 @@ describe('GET space', () => {
isServerless: false,
});
const { handler } = versionedRouterMock.getRoute('get', '/api/spaces/space/{id}').versions[
API_VERSIONS.public.v1
];
return {
routeHandler: router.get.mock.calls[0][1],
routeHandler: handler,
};
};

View file

@ -9,40 +9,47 @@ import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import type { ExternalRouteDeps } from '.';
import { API_VERSIONS } from '../../../../common';
import { wrapError } from '../../../lib/errors';
import { createLicensedRouteHandler } from '../../lib';
export function initGetSpaceApi(deps: ExternalRouteDeps) {
const { router, getSpacesService, isServerless } = deps;
const { router, getSpacesService } = deps;
router.get(
{
router.versioned
.get({
path: '/api/spaces/space/{id}',
access: 'public',
description: `Get a space`,
options: {
access: isServerless ? 'internal' : 'public',
description: `Get a space`,
tags: ['oas-tag:spaces'],
},
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
createLicensedRouteHandler(async (context, request, response) => {
const spaceId = request.params.id;
const spacesClient = getSpacesService().createSpacesClient(request);
try {
const space = await spacesClient.get(spaceId);
return response.ok({
body: space,
});
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();
}
return response.customError(wrapError(error));
}
})
);
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {
request: {
params: schema.object({
id: schema.string(),
}),
},
},
},
createLicensedRouteHandler(async (context, request, response) => {
const spaceId = request.params.id;
const spacesClient = getSpacesService().createSpacesClient(request);
try {
const space = await spacesClient.get(spaceId);
return response.ok({
body: space,
});
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();
}
return response.customError(wrapError(error));
}
})
);
}

View file

@ -15,10 +15,13 @@ import {
httpServiceMock,
loggingSystemMock,
} from '@kbn/core/server/mocks';
import type { MockedVersionedRouter } from '@kbn/core-http-router-server-mocks';
import type { RouteValidatorConfig } from '@kbn/core-http-server';
import { getRequestValidation } from '@kbn/core-http-server';
import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
import { initGetAllSpacesApi } from './get_all';
import { API_VERSIONS } from '../../../../common';
import { spacesConfig } from '../../../lib/__fixtures__';
import { SpacesClientService } from '../../../spaces_client';
import { SpacesService } from '../../../spaces_service';
@ -37,6 +40,7 @@ describe('GET /spaces/space', () => {
const setup = async () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter();
const versionedRouterMock = router.versioned as MockedVersionedRouter;
const coreStart = coreMock.createStart();
@ -72,9 +76,13 @@ describe('GET /spaces/space', () => {
isServerless: false,
});
const { handler, config } = versionedRouterMock.getRoute('get', '/api/spaces/space').versions[
API_VERSIONS.public.v1
];
return {
routeConfig: router.get.mock.calls[0][0],
routeHandler: router.get.mock.calls[0][1],
routeValidation: (config.validate as any).request as RouteValidatorConfig<{}, {}, {}> | false,
routeHandler: handler,
};
};
@ -92,17 +100,17 @@ describe('GET /spaces/space', () => {
});
it(`returns expected result when specifying include_authorized_purposes=true`, async () => {
const { routeConfig, routeHandler } = await setup();
const { routeValidation, routeHandler } = await setup();
const request = httpServerMock.createKibanaRequest({
method: 'get',
query: { purpose, include_authorized_purposes: true },
});
if (routeConfig.validate === false) {
if (routeValidation === false) {
throw new Error('Test setup failure. Expected route validation');
}
const queryParamsValidation = getRequestValidation(routeConfig.validate)
const queryParamsValidation = getRequestValidation(routeValidation)
.query! as ObjectType<any>;
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);

View file

@ -8,63 +8,70 @@
import { schema } from '@kbn/config-schema';
import type { ExternalRouteDeps } from '.';
import type { Space } from '../../../../common';
import { API_VERSIONS, type Space } from '../../../../common';
import { wrapError } from '../../../lib/errors';
import { createLicensedRouteHandler } from '../../lib';
export function initGetAllSpacesApi(deps: ExternalRouteDeps) {
const { router, log, getSpacesService, isServerless } = deps;
const { router, log, getSpacesService } = deps;
router.get(
{
router.versioned
.get({
path: '/api/spaces/space',
access: 'public',
description: `Get all spaces`,
options: {
access: isServerless ? 'internal' : 'public',
description: `Get all spaces`,
tags: ['oas-tag:spaces'],
},
validate: {
query: schema.object({
purpose: schema.maybe(
schema.oneOf([
schema.literal('any'),
schema.literal('copySavedObjectsIntoSpace'),
schema.literal('shareSavedObjectsIntoSpace'),
])
),
include_authorized_purposes: schema.conditional(
schema.siblingRef('purpose'),
schema.string(),
schema.maybe(schema.literal(false)),
schema.maybe(schema.boolean())
),
}),
},
},
createLicensedRouteHandler(async (context, request, response) => {
log.debug(`Inside GET /api/spaces/space`);
const { purpose, include_authorized_purposes: includeAuthorizedPurposes } = request.query;
const spacesClient = getSpacesService().createSpacesClient(request);
let spaces: Space[];
try {
log.debug(
`Attempting to retrieve all spaces for ${purpose} purpose with includeAuthorizedPurposes=${includeAuthorizedPurposes}`
);
spaces = await spacesClient.getAll({ purpose, includeAuthorizedPurposes });
log.debug(
`Retrieved ${spaces.length} spaces for ${purpose} purpose with includeAuthorizedPurposes=${includeAuthorizedPurposes}`
);
} catch (error) {
log.debug(
`Error retrieving spaces for ${purpose} purpose with includeAuthorizedPurposes=${includeAuthorizedPurposes}: ${error}`
);
return response.customError(wrapError(error));
}
return response.ok({ body: spaces });
})
);
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {
request: {
query: schema.object({
purpose: schema.maybe(
schema.oneOf([
schema.literal('any'),
schema.literal('copySavedObjectsIntoSpace'),
schema.literal('shareSavedObjectsIntoSpace'),
])
),
include_authorized_purposes: schema.conditional(
schema.siblingRef('purpose'),
schema.string(),
schema.maybe(schema.literal(false)),
schema.maybe(schema.boolean())
),
}),
},
},
},
createLicensedRouteHandler(async (context, request, response) => {
log.debug(`Inside GET /api/spaces/space`);
const { purpose, include_authorized_purposes: includeAuthorizedPurposes } = request.query;
const spacesClient = getSpacesService().createSpacesClient(request);
let spaces: Space[];
try {
log.debug(
`Attempting to retrieve all spaces for ${purpose} purpose with includeAuthorizedPurposes=${includeAuthorizedPurposes}`
);
spaces = await spacesClient.getAll({ purpose, includeAuthorizedPurposes });
log.debug(
`Retrieved ${spaces.length} spaces for ${purpose} purpose with includeAuthorizedPurposes=${includeAuthorizedPurposes}`
);
} catch (error) {
log.debug(
`Error retrieving spaces for ${purpose} purpose with includeAuthorizedPurposes=${includeAuthorizedPurposes}: ${error}`
);
return response.customError(wrapError(error));
}
return response.ok({ body: spaces });
})
);
}

View file

@ -16,9 +16,11 @@ import {
httpServiceMock,
loggingSystemMock,
} from '@kbn/core/server/mocks';
import type { MockedVersionedRouter } from '@kbn/core-http-router-server-mocks';
import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
import { initPostSpacesApi } from './post';
import { API_VERSIONS } from '../../../../common';
import { spacesConfig } from '../../../lib/__fixtures__';
import { SpacesClientService } from '../../../spaces_client';
import { SpacesService } from '../../../spaces_service';
@ -36,6 +38,7 @@ describe('Spaces Public API', () => {
const setup = async () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter();
const versionedRouterMock = router.versioned as MockedVersionedRouter;
const coreStart = coreMock.createStart();
@ -75,11 +78,13 @@ describe('Spaces Public API', () => {
isServerless: false,
});
const [routeDefinition, routeHandler] = router.post.mock.calls[0];
const { handler, config } = versionedRouterMock.getRoute('post', '/api/spaces/space').versions[
API_VERSIONS.public.v1
];
return {
routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>,
routeHandler,
routeValidation: (config.validate as any).request as RouteValidatorConfig<{}, {}, {}>,
routeHandler: handler,
savedObjectsRepositoryMock,
};
};

View file

@ -10,6 +10,7 @@ import Boom from '@hapi/boom';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import type { ExternalRouteDeps } from '.';
import { API_VERSIONS } from '../../../../common';
import { wrapError } from '../../../lib/errors';
import { getSpaceSchema } from '../../../lib/space_schema';
import { createLicensedRouteHandler } from '../../lib';
@ -17,37 +18,42 @@ import { createLicensedRouteHandler } from '../../lib';
export function initPostSpacesApi(deps: ExternalRouteDeps) {
const { router, log, getSpacesService, isServerless } = deps;
router.post(
{
router.versioned
.post({
path: '/api/spaces/space',
access: 'public',
description: `Create a space`,
options: {
access: isServerless ? 'internal' : 'public',
description: `Create a space`,
tags: ['oas-tag:spaces'],
},
validate: {
body: getSpaceSchema(isServerless),
},
},
createLicensedRouteHandler(async (context, request, response) => {
log.debug(`Inside POST /api/spaces/space`);
const spacesClient = getSpacesService().createSpacesClient(request);
const space = request.body;
try {
log.debug(`Attempting to create space`);
const createdSpace = await spacesClient.create(space);
return response.ok({ body: createdSpace });
} catch (error) {
if (SavedObjectsErrorHelpers.isConflictError(error)) {
const { body } = wrapError(
Boom.conflict(`A space with the identifier ${space.id} already exists.`)
);
return response.conflict({ body });
}
log.debug(`Error creating space: ${error}`);
return response.customError(wrapError(error));
}
})
);
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {
request: {
body: getSpaceSchema(isServerless),
},
},
},
createLicensedRouteHandler(async (context, request, response) => {
log.debug(`Inside POST /api/spaces/space`);
const spacesClient = getSpacesService().createSpacesClient(request);
const space = request.body;
try {
log.debug(`Attempting to create space`);
const createdSpace = await spacesClient.create(space);
return response.ok({ body: createdSpace });
} catch (error) {
if (SavedObjectsErrorHelpers.isConflictError(error)) {
const { body } = wrapError(
Boom.conflict(`A space with the identifier ${space.id} already exists.`)
);
return response.conflict({ body });
}
log.debug(`Error creating space: ${error}`);
return response.customError(wrapError(error));
}
})
);
}

View file

@ -16,9 +16,11 @@ import {
httpServiceMock,
loggingSystemMock,
} from '@kbn/core/server/mocks';
import type { MockedVersionedRouter } from '@kbn/core-http-router-server-mocks';
import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
import { initPutSpacesApi } from './put';
import { API_VERSIONS } from '../../../../common';
import { spacesConfig } from '../../../lib/__fixtures__';
import { SpacesClientService } from '../../../spaces_client';
import { SpacesService } from '../../../spaces_service';
@ -36,6 +38,7 @@ describe('PUT /api/spaces/space', () => {
const setup = async () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter();
const versionedRouterMock = router.versioned as MockedVersionedRouter;
const coreStart = coreMock.createStart();
@ -75,11 +78,12 @@ describe('PUT /api/spaces/space', () => {
isServerless: false,
});
const [routeDefinition, routeHandler] = router.put.mock.calls[0];
const { handler, config } = versionedRouterMock.getRoute('put', '/api/spaces/space/{id}')
.versions[API_VERSIONS.public.v1];
return {
routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>,
routeHandler,
routeValidation: (config.validate as any).request as RouteValidatorConfig<{}, {}, {}>,
routeHandler: handler,
savedObjectsRepositoryMock,
};
};

View file

@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import type { ExternalRouteDeps } from '.';
import type { Space } from '../../../../common';
import { API_VERSIONS, type Space } from '../../../../common';
import { wrapError } from '../../../lib/errors';
import { getSpaceSchema } from '../../../lib/space_schema';
import { createLicensedRouteHandler } from '../../lib';
@ -17,37 +17,44 @@ import { createLicensedRouteHandler } from '../../lib';
export function initPutSpacesApi(deps: ExternalRouteDeps) {
const { router, getSpacesService, isServerless } = deps;
router.put(
{
router.versioned
.put({
path: '/api/spaces/space/{id}',
access: 'public',
description: `Update a space`,
options: {
access: isServerless ? 'internal' : 'public',
description: `Update a space`,
tags: ['oas-tag:spaces'],
},
validate: {
params: schema.object({
id: schema.string(),
}),
body: getSpaceSchema(isServerless),
},
},
createLicensedRouteHandler(async (context, request, response) => {
const spacesClient = getSpacesService().createSpacesClient(request);
const space = request.body;
const id = request.params.id;
let result: Space;
try {
result = await spacesClient.update(id, { ...space });
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();
}
return response.customError(wrapError(error));
}
return response.ok({ body: result });
})
);
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {
request: {
params: schema.object({
id: schema.string(),
}),
body: getSpaceSchema(isServerless),
},
},
},
createLicensedRouteHandler(async (context, request, response) => {
const spacesClient = getSpacesService().createSpacesClient(request);
const space = request.body;
const id = request.params.id;
let result: Space;
try {
result = await spacesClient.update(id, { ...space });
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();
}
return response.customError(wrapError(error));
}
return response.ok({ body: result });
})
);
}

View file

@ -38,7 +38,6 @@
"@kbn/security-plugin-types-public",
"@kbn/cloud-plugin",
"@kbn/core-analytics-browser",
"@kbn/core-analytics-browser",
"@kbn/security-plugin-types-common",
"@kbn/core-application-browser",
"@kbn/unsaved-changes-prompt",
@ -52,6 +51,7 @@
"@kbn/core-notifications-browser",
"@kbn/logging",
"@kbn/core-logging-browser-mocks",
"@kbn/core-http-router-server-mocks"
],
"exclude": [
"target/**/*",

View file

@ -44,7 +44,9 @@ export function SpacesServiceProvider({ getService }: FtrProviderContext) {
: undefined;
const axios = Axios.create({
headers: { 'kbn-xsrf': 'x-pack/ftr/services/spaces/space' },
headers: {
'kbn-xsrf': 'x-pack/ftr/services/spaces/space',
},
baseURL: url,
maxRedirects: 0,
validateStatus: () => true, // we do our own validation below and throw better error messages

View file

@ -26,6 +26,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const svlCommonApi = getService('svlCommonApi');
const roleScopedSupertest = getService('roleScopedSupertest');
const samlAuth = getService('samlAuth');
// CRUD operations to become public APIs: https://github.com/elastic/kibana/issues/192153
let supertestAdminWithApiKey: SupertestWithRoleScopeType;
let supertestAdminWithCookieCredentials: SupertestWithRoleScopeType;
@ -48,7 +50,7 @@ export default function ({ getService }: FtrProviderContext) {
describe('spaces', function () {
before(async () => {
supertestAdminWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withInternalHeaders: true,
withCommonHeaders: true,
});
supertestAdminWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope(
'admin',
@ -242,42 +244,166 @@ export default function ({ getService }: FtrProviderContext) {
});
});
// These tests just test access to API endpoints
// These tests just test access to API endpoints, in this case
// when accessed without internal headers they will return 400
// They will be included in deployment agnostic testing once spaces
// are enabled in production.
describe(`Access`, () => {
it('#copyToSpace', async () => {
const { body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_copy_saved_objects'
);
svlCommonApi.assertResponseStatusCode(400, status, body);
});
it('#resolveCopyToSpaceErrors', async () => {
const { body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_resolve_copy_saved_objects_errors'
);
svlCommonApi.assertResponseStatusCode(400, status, body);
});
it('#updateObjectsSpaces', async () => {
const { body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_update_objects_spaces'
);
svlCommonApi.assertResponseStatusCode(400, status, body);
});
it('#getShareableReferences', async () => {
const { body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_get_shareable_references')
.send({
objects: [{ type: 'a', id: 'a' }],
describe(`internal`, () => {
it('#getActiveSpace requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey
.get('/internal/spaces/_active_space')
.set(samlAuth.getCommonRequestHeader()));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [get] exists but is not available with the current configuration'
),
});
svlCommonApi.assertResponseStatusCode(200, status, body);
expect(status).toBe(400);
({ body, status } = await supertestAdminWithApiKey
.get('/internal/spaces/_active_space')
.set(samlAuth.getInternalRequestHeader()));
// expect success because we're using the internal header
expect(body).toEqual(
expect.objectContaining({
id: 'default',
})
);
expect(status).toBe(200);
});
it('#copyToSpace requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_copy_saved_objects'
));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [post] exists but is not available with the current configuration'
),
});
({ body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_copy_saved_objects')
.set(samlAuth.getInternalRequestHeader()));
svlCommonApi.assertResponseStatusCode(400, status, body);
// expect 400 for missing body
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: expected a plain object value, but found [null] instead.',
});
});
it('#resolveCopyToSpaceErrors requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_resolve_copy_saved_objects_errors'
));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [post] exists but is not available with the current configuration'
),
});
({ body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_resolve_copy_saved_objects_errors')
.set(samlAuth.getInternalRequestHeader()));
svlCommonApi.assertResponseStatusCode(400, status, body);
// expect 400 for missing body
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: expected a plain object value, but found [null] instead.',
});
});
it('#updateObjectsSpaces requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_update_objects_spaces'
));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [post] exists but is not available with the current configuration'
),
});
({ body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_update_objects_spaces')
.set(samlAuth.getInternalRequestHeader()));
svlCommonApi.assertResponseStatusCode(400, status, body);
// expect 400 for missing body
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: expected a plain object value, but found [null] instead.',
});
});
it('#getShareableReferences requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_get_shareable_references'
));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [post] exists but is not available with the current configuration'
),
});
({ body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_get_shareable_references')
.set(samlAuth.getInternalRequestHeader())
.send({
objects: [{ type: 'a', id: 'a' }],
}));
svlCommonApi.assertResponseStatusCode(200, status, body);
});
});
it('#disableLegacyUrlAliases', async () => {
const { body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_disable_legacy_url_aliases'
);
// without a request body we would normally a 400 bad request if the endpoint was registered
svlCommonApi.assertApiNotFound(body, status);
describe(`disabled`, () => {
it('#disableLegacyUrlAliases', async () => {
const { body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_disable_legacy_url_aliases'
);
// without a request body we would normally a 400 bad request if the endpoint was registered
svlCommonApi.assertApiNotFound(body, status);
});
});
});
});

View file

@ -19,7 +19,9 @@ export default function ({ getService }: FtrProviderContext) {
describe('spaces', function () {
before(async () => {
// admin is the only predefined role that will work for all 3 solutions
supertestAdminWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('admin');
supertestAdminWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withCommonHeaders: true,
});
supertestAdminWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope(
'admin',
{
@ -33,136 +35,36 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('route access', () => {
it('#delete', async () => {
const { body, status } = await supertestAdminWithApiKey
.delete('/api/spaces/space/default')
.set(samlAuth.getInternalRequestHeader());
svlCommonApi.assertResponseStatusCode(400, status, body);
});
// Skipped due to change in QA environment for role management and spaces
// TODO: revisit once the change is rolled out to all environments
it.skip('#create', async () => {
const { body, status } = await supertestAdminWithApiKey
.post('/api/spaces/space')
.set(samlAuth.getInternalRequestHeader())
.send({
describe('public (CRUD)', () => {
// Skipped due to change in QA environment for role management and spaces
// TODO: revisit once the change is rolled out to all environments
it.skip('#create', async () => {
const { body, status } = await supertestAdminWithApiKey.post('/api/spaces/space').send({
id: 'custom',
name: 'Custom',
disabledFeatures: [],
});
svlCommonApi.assertResponseStatusCode(400, status, body);
});
svlCommonApi.assertResponseStatusCode(400, status, body);
it('#update requires internal header', async () => {
const { body, status } = await supertestAdminWithApiKey
.put('/api/spaces/space/default')
.set(samlAuth.getInternalRequestHeader())
.send({
id: 'default',
name: 'UPDATED!',
disabledFeatures: [],
});
svlCommonApi.assertResponseStatusCode(200, status, body);
});
it('#copyToSpace', async () => {
const { body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_copy_saved_objects')
.set(samlAuth.getInternalRequestHeader());
svlCommonApi.assertResponseStatusCode(400, status, body);
});
it('#resolveCopyToSpaceErrors', async () => {
const { body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_resolve_copy_saved_objects_errors')
.set(samlAuth.getInternalRequestHeader());
svlCommonApi.assertResponseStatusCode(400, status, body);
});
it('#updateObjectsSpaces', async () => {
const { body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_update_objects_spaces')
.set(samlAuth.getInternalRequestHeader());
svlCommonApi.assertResponseStatusCode(400, status, body);
});
it('#getShareableReferences', async () => {
const { body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_get_shareable_references')
.set(samlAuth.getInternalRequestHeader())
.send({
objects: [{ type: 'a', id: 'a' }],
});
svlCommonApi.assertResponseStatusCode(200, status, body);
});
it('#disableLegacyUrlAliases', async () => {
const { body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_disable_legacy_url_aliases')
.set(samlAuth.getInternalRequestHeader());
// without a request body we would normally a 400 bad request if the endpoint was registered
svlCommonApi.assertApiNotFound(body, status);
});
describe('internal', () => {
it('#get requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey
.get('/api/spaces/space/default')
.set(samlAuth.getCommonRequestHeader()));
// expect a rejection because we're not using the internal header
// Should fail due to maximum spaces limit, not because of lacking internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [get] exists but is not available with the current configuration'
),
message:
'Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting',
});
expect(status).toBe(400);
});
({ body, status } = await supertestAdminWithApiKey
.get('/api/spaces/space/default')
.set(samlAuth.getInternalRequestHeader()));
it('#get', async () => {
const { body, status } = await supertestAdminWithApiKey.get('/api/spaces/space/default');
// expect success because we're using the internal header
expect(body).toEqual(
expect.objectContaining({
id: 'default',
})
);
expect(body).toEqual(expect.objectContaining({ id: 'default' }));
expect(status).toBe(200);
});
it('#getAll requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey
.get('/api/spaces/space')
.set(samlAuth.getCommonRequestHeader()));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [get] exists but is not available with the current configuration'
),
});
expect(status).toBe(400);
({ body, status } = await supertestAdminWithApiKey
.get('/api/spaces/space')
.set(samlAuth.getInternalRequestHeader()));
it('#getAll', async () => {
const { body, status } = await supertestAdminWithApiKey.get('/api/spaces/space');
// expect success because we're using the internal header
expect(body).toEqual(
expect.arrayContaining([
@ -174,6 +76,35 @@ export default function ({ getService }: FtrProviderContext) {
expect(status).toBe(200);
});
it('#update', async () => {
const { body, status } = await supertestAdminWithApiKey
.put('/api/spaces/space/default')
.send({
id: 'default',
name: 'UPDATED!',
disabledFeatures: [],
});
svlCommonApi.assertResponseStatusCode(200, status, body);
});
it('#delete', async () => {
const { body, status } = await supertestAdminWithApiKey.delete(
'/api/spaces/space/default'
);
svlCommonApi.assertResponseStatusCode(400, status, body);
// 400 with specific reason - cannot delete the default space
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: 'The default space cannot be deleted because it is reserved.',
});
});
});
describe('internal', () => {
it('#getActiveSpace requires internal header', async () => {
let body: any;
let status: number;
@ -202,6 +133,133 @@ export default function ({ getService }: FtrProviderContext) {
);
expect(status).toBe(200);
});
it('#copyToSpace requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_copy_saved_objects'
));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [post] exists but is not available with the current configuration'
),
});
({ body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_copy_saved_objects')
.set(samlAuth.getInternalRequestHeader()));
svlCommonApi.assertResponseStatusCode(400, status, body);
// expect 400 for missing body
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: expected a plain object value, but found [null] instead.',
});
});
it('#resolveCopyToSpaceErrors requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_resolve_copy_saved_objects_errors'
));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [post] exists but is not available with the current configuration'
),
});
({ body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_resolve_copy_saved_objects_errors')
.set(samlAuth.getInternalRequestHeader()));
svlCommonApi.assertResponseStatusCode(400, status, body);
// expect 400 for missing body
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: expected a plain object value, but found [null] instead.',
});
});
it('#updateObjectsSpaces requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_update_objects_spaces'
));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [post] exists but is not available with the current configuration'
),
});
({ body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_update_objects_spaces')
.set(samlAuth.getInternalRequestHeader()));
svlCommonApi.assertResponseStatusCode(400, status, body);
// expect 400 for missing body
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: expected a plain object value, but found [null] instead.',
});
});
it('#getShareableReferences requires internal header', async () => {
let body: any;
let status: number;
({ body, status } = await supertestAdminWithApiKey.post(
'/api/spaces/_get_shareable_references'
));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [post] exists but is not available with the current configuration'
),
});
({ body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_get_shareable_references')
.set(samlAuth.getInternalRequestHeader())
.send({
objects: [{ type: 'a', id: 'a' }],
}));
svlCommonApi.assertResponseStatusCode(200, status, body);
});
});
describe('disabled', () => {
it('#disableLegacyUrlAliases', async () => {
const { body, status } = await supertestAdminWithApiKey
.post('/api/spaces/_disable_legacy_url_aliases')
.set(samlAuth.getInternalRequestHeader());
// without a request body we would normally a 400 bad request if the endpoint was registered
svlCommonApi.assertApiNotFound(body, status);
});
});
});

View file

@ -37,65 +37,66 @@ export default function ({ getService }: FtrProviderContext) {
'admin',
{
useCookieHeader: true,
withInternalHeaders: true,
}
);
supertestAdminWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withInternalHeaders: true,
withCommonHeaders: true,
});
});
after(async () => {
await supertestAdminWithApiKey.destroy();
});
describe('route access', () => {
describe('internal', () => {
describe('disabled', () => {
// Skipped due to change in QA environment for role management and spaces
// TODO: revisit once the change is rolled out to all environments
it.skip('get all privileges', async () => {
const { body, status } = await supertestAdminWithApiKey.get('/api/security/privileges');
svlCommonApi.assertApiNotFound(body, status);
});
describe('disabled', () => {
// Skipped due to change in QA environment for role management and spaces
// TODO: revisit once the change is rolled out to all environments
it.skip('get all privileges', async () => {
const { body, status } = await supertestAdminWithApiKey.get('/api/security/privileges');
svlCommonApi.assertApiNotFound(body, status);
});
// Skipped due to change in QA environment for role management and spaces
// TODO: revisit once the change is rolled out to all environments
it.skip('get built-in elasticsearch privileges', async () => {
const { body, status } = await supertestAdminWithCookieCredentials.get(
'/internal/security/esPrivileges/builtin'
);
svlCommonApi.assertApiNotFound(body, status);
});
// Skipped due to change in QA environment for role management and spaces
// TODO: revisit once the change is rolled out to all environments
it.skip('get built-in elasticsearch privileges', async () => {
const { body, status } = await supertestAdminWithCookieCredentials.get(
'/internal/security/esPrivileges/builtin'
);
svlCommonApi.assertApiNotFound(body, status);
});
it('create/update roleAuthc', async () => {
const { body, status } = await supertestAdminWithApiKey.put('/api/security/role/test');
svlCommonApi.assertApiNotFound(body, status);
});
// Role CRUD APIs are gated behind the xpack.security.roleManagementEnabled config
// setting. This setting is false by default on serverless. When the custom roles
// feature is enabled, this setting will be true, and the tests from
// roles_routes_feature_flag.ts can be moved here to replace these.
it('create/update roleAuthc', async () => {
const { body, status } = await supertestAdminWithApiKey.put('/api/security/role/test');
svlCommonApi.assertApiNotFound(body, status);
});
it('get roleAuthc', async () => {
const { body, status } = await supertestAdminWithApiKey.get(
'/api/security/role/superuser'
);
svlCommonApi.assertApiNotFound(body, status);
});
it('get role', async () => {
const { body, status } = await supertestAdminWithApiKey.get(
'/api/security/role/superuser'
);
svlCommonApi.assertApiNotFound(body, status);
});
it('get all roles', async () => {
const { body, status } = await supertestAdminWithApiKey.get('/api/security/role');
svlCommonApi.assertApiNotFound(body, status);
});
it('get all roles', async () => {
const { body, status } = await supertestAdminWithApiKey.get('/api/security/role');
svlCommonApi.assertApiNotFound(body, status);
});
it('delete roleAuthc', async () => {
const { body, status } = await supertestAdminWithApiKey.delete(
'/api/security/role/superuser'
);
svlCommonApi.assertApiNotFound(body, status);
});
it('delete role', async () => {
const { body, status } = await supertestAdminWithApiKey.delete(
'/api/security/role/superuser'
);
svlCommonApi.assertApiNotFound(body, status);
});
it('get shared saved object permissions', async () => {
const { body, status } = await supertestAdminWithCookieCredentials.get(
'/internal/security/_share_saved_object_permissions'
);
svlCommonApi.assertApiNotFound(body, status);
});
it('get shared saved object permissions', async () => {
const { body, status } = await supertestAdminWithCookieCredentials.get(
'/internal/security/_share_saved_object_permissions'
);
svlCommonApi.assertApiNotFound(body, status);
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import expect from '@kbn/expect';
import expect from 'expect';
import type { Role } from '@kbn/security-plugin-types-common';
import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integration/deployment_agnostic/services';
import { FtrProviderContext } from '../../../ftr_provider_context';
@ -26,6 +26,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const platformSecurityUtils = getService('platformSecurityUtils');
const roleScopedSupertest = getService('roleScopedSupertest');
const svlCommonApi = getService('svlCommonApi');
let supertestAdminWithApiKey: SupertestWithRoleScopeType;
let supertestAdminWithCookieCredentials: SupertestWithRoleScopeType;
const es = getService('es');
@ -41,7 +42,7 @@ export default function ({ getService }: FtrProviderContext) {
}
);
supertestAdminWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
withInternalHeaders: true,
withCommonHeaders: true,
});
});
after(async () => {
@ -86,7 +87,7 @@ export default function ({ getService }: FtrProviderContext) {
.expect(204);
const role = await es.security.getRole({ name: 'role_with_privileges' });
expect(role).to.eql({
expect(role).toEqual({
role_with_privileges: {
cluster: ['manage'],
indices: [
@ -425,7 +426,6 @@ export default function ({ getService }: FtrProviderContext) {
.expect(200)
.expect((res: { body: Role[] }) => {
const roles = res.body;
expect(roles).to.be.an('array');
const success = roles.every((role) => {
return (
@ -440,8 +440,8 @@ export default function ({ getService }: FtrProviderContext) {
const expectedRole = roles.find((role) => role.name === 'space_role_to_get');
expect(success).to.be(true);
expect(expectedRole).to.be.an('object');
expect(success).toBe(true);
expect(expectedRole).toBeTruthy();
});
});
});
@ -508,7 +508,7 @@ export default function ({ getService }: FtrProviderContext) {
.expect(204);
const role = await es.security.getRole({ name: 'role_to_update' });
expect(role).to.eql({
expect(role).toEqual({
role_to_update: {
cluster: ['manage'],
indices: [
@ -582,9 +582,9 @@ export default function ({ getService }: FtrProviderContext) {
const role = await es.security.getRole({ name: 'role_to_update_with_dls_fls' });
expect(role.role_to_update_with_dls_fls.cluster).to.eql(['manage']);
expect(role.role_to_update_with_dls_fls.indices[0].names).to.eql(['logstash-*']);
expect(role.role_to_update_with_dls_fls.indices[0].query).to.eql(
expect(role.role_to_update_with_dls_fls.cluster).toEqual(['manage']);
expect(role.role_to_update_with_dls_fls.indices[0].names).toEqual(['logstash-*']);
expect(role.role_to_update_with_dls_fls.indices[0].query).toEqual(
`{ "match": { "geo.src": "CN" } }`
);
});
@ -652,7 +652,7 @@ export default function ({ getService }: FtrProviderContext) {
.expect(400);
const role = await es.security.getRole({ name: 'role_to_update' });
expect(role).to.eql({
expect(role).toEqual({
role_to_update: {
cluster: ['monitor'],
indices: [
@ -753,7 +753,7 @@ export default function ({ getService }: FtrProviderContext) {
.expect(400);
const role = await es.security.getRole({ name: 'role_to_update' });
expect(role).to.eql({
expect(role).toEqual({
role_to_update: {
cluster: ['monitor'],
indices: [
@ -855,7 +855,7 @@ export default function ({ getService }: FtrProviderContext) {
.expect(400);
const role = await es.security.getRole({ name: 'role_to_update' });
expect(role).to.eql({
expect(role).toEqual({
role_to_update: {
cluster: ['monitor'],
indices: [
@ -924,7 +924,26 @@ export default function ({ getService }: FtrProviderContext) {
{ name: 'role_to_delete' },
{ ignore: [404] }
);
expect(deletedRole).to.eql({});
expect(deletedRole).toEqual({});
});
});
describe('Access', () => {
describe('public', () => {
it('reset session page', async () => {
const { status } = await supertestAdminWithCookieCredentials.get(
'/internal/security/reset_session_page.js'
);
expect(status).toBe(200);
});
});
describe('Disabled', () => {
it('get shared saved object permissions', async () => {
const { body, status } = await supertestAdminWithCookieCredentials.get(
'/internal/security/_share_saved_object_permissions'
);
svlCommonApi.assertApiNotFound(body, status);
});
});
});
});