From 26f2928b0887c9fda4403c0ce3fcc332b7c0e69a Mon Sep 17 00:00:00 2001 From: Jeramy Soucy Date: Thu, 3 Oct 2024 16:28:54 +0200 Subject: [PATCH] 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> --- .../scripts/steps/capture_oas_snapshot.sh | 2 +- oas_docs/bundle.json | 1427 +++++++++++++++++ oas_docs/bundle.serverless.json | 320 ++++ .../output/kibana.serverless.staging.yaml | 209 +++ oas_docs/output/kibana.serverless.yaml | 209 +++ oas_docs/output/kibana.staging.yaml | 945 +++++++++++ oas_docs/output/kibana.yaml | 945 +++++++++++ .../core/http/core-http-browser/src/types.ts | 5 +- .../services/security/role.ts | 22 +- .../tsconfig.json | 2 +- .../functional_tests/run_tests/run_tests.ts | 5 + x-pack/plugins/security/common/constants.ts | 11 + .../roles/edit_role/edit_role_page.tsx | 3 +- .../management/roles/roles_api_client.ts | 13 +- .../routes/authorization/roles/delete.test.ts | 8 +- .../routes/authorization/roles/delete.ts | 46 +- .../routes/authorization/roles/get.test.ts | 8 +- .../server/routes/authorization/roles/get.ts | 78 +- .../authorization/roles/get_all.test.ts | 8 +- .../routes/authorization/roles/get_all.ts | 78 +- .../routes/authorization/roles/post.test.ts | 9 +- .../server/routes/authorization/roles/post.ts | 162 +- .../routes/authorization/roles/put.test.ts | 17 +- .../server/routes/authorization/roles/put.ts | 129 +- x-pack/plugins/security/tsconfig.json | 1 + x-pack/plugins/spaces/common/constants.ts | 9 + x-pack/plugins/spaces/common/index.ts | 1 + .../public/spaces_manager/spaces_manager.ts | 16 +- .../server/routes/api/external/delete.test.ts | 11 +- .../server/routes/api/external/delete.ts | 72 +- .../server/routes/api/external/get.test.ts | 9 +- .../spaces/server/routes/api/external/get.ts | 59 +- .../routes/api/external/get_all.test.ts | 18 +- .../server/routes/api/external/get_all.ts | 97 +- .../server/routes/api/external/post.test.ts | 11 +- .../spaces/server/routes/api/external/post.ts | 62 +- .../server/routes/api/external/put.test.ts | 10 +- .../spaces/server/routes/api/external/put.ts | 59 +- x-pack/plugins/spaces/tsconfig.json | 2 +- x-pack/test/common/services/spaces.ts | 4 +- .../management/multiple_spaces_enabled.ts | 190 ++- .../test_suites/common/management/spaces.ts | 256 +-- .../common/platform_security/authorization.ts | 89 +- .../roles_routes_feature_flag.ts | 47 +- 44 files changed, 5066 insertions(+), 618 deletions(-) diff --git a/.buildkite/scripts/steps/capture_oas_snapshot.sh b/.buildkite/scripts/steps/capture_oas_snapshot.sh index 79dfdd96c8fc6..3c82fca5013c5 100755 --- a/.buildkite/scripts/steps/capture_oas_snapshot.sh +++ b/.buildkite/scripts/steps/capture_oas_snapshot.sh @@ -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 diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 5fd690a2f6ceb..7c27f050640ec 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -6263,6 +6263,1427 @@ ] } }, + "/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, + "properties": { + "description": { + "maxLength": 2048, + "type": "string" + }, + "elasticsearch": { + "additionalProperties": false, + "properties": { + "cluster": { + "items": { + "type": "string" + }, + "type": "array" + }, + "indices": { + "items": { + "additionalProperties": false, + "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": "object" + }, + "type": "array" + }, + "remote_cluster": { + "items": { + "additionalProperties": false, + "properties": { + "clusters": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "privileges": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "privileges", + "clusters" + ], + "type": "object" + }, + "type": "array" + }, + "remote_indices": { + "items": { + "additionalProperties": false, + "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": "object" + }, + "type": "array" + }, + "run_as": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "kibana": { + "items": { + "additionalProperties": false, + "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": "object" + }, + "type": "array" + }, + "metadata": { + "additionalProperties": {}, + "type": "object" + } + }, + "required": [ + "elasticsearch" + ], + "type": "object" + } + } + } + }, + "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, + "properties": { + "roles": { + "additionalProperties": { + "additionalProperties": false, + "properties": { + "description": { + "maxLength": 2048, + "type": "string" + }, + "elasticsearch": { + "additionalProperties": false, + "properties": { + "cluster": { + "items": { + "type": "string" + }, + "type": "array" + }, + "indices": { + "items": { + "additionalProperties": false, + "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": "object" + }, + "type": "array" + }, + "remote_cluster": { + "items": { + "additionalProperties": false, + "properties": { + "clusters": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "privileges": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "privileges", + "clusters" + ], + "type": "object" + }, + "type": "array" + }, + "remote_indices": { + "items": { + "additionalProperties": false, + "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": "object" + }, + "type": "array" + }, + "run_as": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "kibana": { + "items": { + "additionalProperties": false, + "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": "object" + }, + "type": "array" + }, + "metadata": { + "additionalProperties": {}, + "type": "object" + } + }, + "required": [ + "elasticsearch" + ], + "type": "object" + }, + "type": "object" + } + }, + "required": [ + "roles" + ], + "type": "object" + } + } + } + }, + "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, + "properties": { + "compatibilityMode": { + "default": false, + "type": "boolean" + }, + "createNewCopies": { + "default": true, + "type": "boolean" + }, + "includeReferences": { + "default": false, + "type": "boolean" + }, + "objects": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ], + "type": "object" + }, + "type": "array" + }, + "overwrite": { + "default": false, + "type": "boolean" + }, + "spaces": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "spaces", + "objects" + ], + "type": "object" + } + } + } + }, + "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, + "properties": { + "aliases": { + "items": { + "additionalProperties": false, + "properties": { + "sourceId": { + "type": "string" + }, + "targetSpace": { + "type": "string" + }, + "targetType": { + "type": "string" + } + }, + "required": [ + "targetSpace", + "targetType", + "sourceId" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "aliases" + ], + "type": "object" + } + } + } + }, + "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, + "properties": { + "objects": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "objects" + ], + "type": "object" + } + } + } + }, + "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, + "properties": { + "compatibilityMode": { + "default": false, + "type": "boolean" + }, + "createNewCopies": { + "default": true, + "type": "boolean" + }, + "includeReferences": { + "default": false, + "type": "boolean" + }, + "objects": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ], + "type": "object" + }, + "type": "array" + }, + "retries": { + "additionalProperties": { + "items": { + "additionalProperties": false, + "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": "object" + }, + "type": "array" + }, + "type": "object" + } + }, + "required": [ + "retries", + "objects" + ], + "type": "object" + } + } + } + }, + "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, + "properties": { + "objects": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ], + "type": "object" + }, + "type": "array" + }, + "spacesToAdd": { + "items": { + "type": "string" + }, + "type": "array" + }, + "spacesToRemove": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "objects", + "spacesToAdd", + "spacesToRemove" + ], + "type": "object" + } + } + } + }, + "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, + "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" + ], + "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" + }, + "solution": { + "enum": [ + "security", + "oblt", + "es", + "classic" + ], + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + } + } + } + }, + "responses": {}, + "summary": "", + "tags": [ + "spaces" + ] + } + }, "/api/status": { "get": { "operationId": "%2Fapi%2Fstatus#0", @@ -6360,6 +7781,12 @@ { "name": "connectors" }, + { + "name": "roles" + }, + { + "name": "spaces" + }, { "name": "system" } diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 5fd690a2f6ceb..c58eef641d6d7 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -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" } diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index 17de64aabc4ef..39819e8307e75 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -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 diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 1b4209e9eec0a..c30f683b3bf97 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -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 diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index 5830ef5ce40f7..3114d0bb7622a 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -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 diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 35a446f538a6a..da28a9a3ade65 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -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 diff --git a/packages/core/http/core-http-browser/src/types.ts b/packages/core/http/core-http-browser/src/types.ts index 0d607274fce4a..6a1e0a7c358e0 100644 --- a/packages/core/http/core-http-browser/src/types.ts +++ b/packages/core/http/core-http-browser/src/types.ts @@ -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; } diff --git a/packages/kbn-ftr-common-functional-ui-services/services/security/role.ts b/packages/kbn-ftr-common-functional-ui-services/services/security/role.ts index 88ea94439984f..7f3ca86d8248c 100644 --- a/packages/kbn-ftr-common-functional-ui-services/services/security/role.ts +++ b/packages/kbn-ftr-common-functional-ui-services/services/security/role.ts @@ -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)}` diff --git a/packages/kbn-ftr-common-functional-ui-services/tsconfig.json b/packages/kbn-ftr-common-functional-ui-services/tsconfig.json index 555e696f0ca51..f1436196d3f21 100644 --- a/packages/kbn-ftr-common-functional-ui-services/tsconfig.json +++ b/packages/kbn-ftr-common-functional-ui-services/tsconfig.json @@ -13,6 +13,6 @@ "@kbn/test-subj-selector", "@kbn/ftr-common-functional-services", "@kbn/std", - "@kbn/expect" + "@kbn/expect", ] } diff --git a/packages/kbn-test/src/functional_tests/run_tests/run_tests.ts b/packages/kbn-test/src/functional_tests/run_tests/run_tests.ts index 785d6efc085ff..930fc91037d1a 100644 --- a/packages/kbn-test/src/functional_tests/run_tests/run_tests.ts +++ b/packages/kbn-test/src/functional_tests/run_tests/run_tests.ts @@ -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) { diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index 8d0237916bb5a..3a9b20bbb0bd7 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -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', + }, + }, +}; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 270ed24e1ad2d..9c0adb9e0d782 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -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('/api/spaces/space').then( + http.get('/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. diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts index d6dcab658d21c..5c3970e82c516 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -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('/api/security/role'); + return await this.http.get('/api/security/role', { version }); }; public getRole = async (roleName: string) => { - return await this.http.get(`/api/security/role/${encodeURIComponent(roleName)}`); + return await this.http.get(`/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 => { return await this.http.post('/api/security/roles', { + version, body: JSON.stringify({ roles: Object.fromEntries( rolesUpdate.map((role) => [role.name, this.transformRoleForSave(copyRole(role))]) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts index 223949843fee5..5c6f3bff716fc 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts @@ -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({ diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts index 022e574181425..fe7c97b32d27b 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts @@ -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 }) }), + }) + .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, - }); + 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)); - } - }) - ); + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); } diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts index b09743fd077e2..732fecb6b5372 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts @@ -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({ diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index ec2208341dc16..f36c785758976 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -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 }) }), + }) + .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; + 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 [features, elasticsearchRoles] = await Promise.all([ + getFeatures(), + await esClient.asCurrentUser.security.getRole({ + name: request.params.name, + }), + ]); - const elasticsearchRole = elasticsearchRoles[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 - ), - }); - } + 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)); - } - }) - ); + return response.notFound(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); } diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts index 3fe91ded3342d..26f48e9230b4a 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts @@ -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({ diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index ed31aedba7f31..07e9a953be8fb 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -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(), - ]); + }) + .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 + // 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)); - } - }) - ); + .filter((role) => { + return !hideReservedRoles || !role.metadata?._reserved; + }) + .sort(compareRolesByName), + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); } diff --git a/x-pack/plugins/security/server/routes/authorization/roles/post.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/post.test.ts index 0ef752423606d..c28a036a676b6 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/post.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/post.test.ts @@ -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, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/post.ts b/x-pack/plugins/security/server/routes/authorization/roles/post.ts index 37967d208bf3a..0fe918ee5cc3e 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/post.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/post.ts @@ -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), - }; - }), + }) + .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), + }; + }), + }, + }, }, - }, - createLicensedRouteHandler(async (context, request, response) => { - try { - const esClient = (await context.core).elasticsearch.client; - const features = await getFeatures(); + 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 = {}; + const { roles } = request.body; + const validatedRolesNames = []; + const kibanaErrors: RolesErrorsDetails = {}; - for (const [roleName, role] of Object.entries(roles)) { - const { validationErrors } = validateKibanaPrivileges(features, role.kibana); + 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 - )}`, - }; + if (validationErrors.length) { + kibanaErrors[roleName] = { + type: 'kibana_privilege_validation_exception', + reason: `Role cannot be updated due to validation errors: ${JSON.stringify( + validationErrors + )}`, + }; - continue; - } + continue; + } - validatedRolesNames.push(roleName); - } + validatedRolesNames.push(roleName); + } - const rawRoles = await esClient.asCurrentUser.security.getRole( - { name: validatedRolesNames.join(',') }, - { ignore: [404] } - ); + 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 esRolesPayload = Object.fromEntries( + validatedRolesNames.map((roleName) => [ + roleName, + transformPutPayloadToElasticsearchRole( + roles[roleName], + authz.applicationName, + rawRoles[roleName] ? rawRoles[roleName].applications : [] + ), + ]) + ); - const esResponse = await esClient.asCurrentUser.transport.request({ - method: 'POST', - path: '/_security/role', - body: { roles: esRolesPayload }, - }); + const esResponse = await esClient.asCurrentUser.transport.request({ + 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(); + 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; + 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)); - } - }) - ); + return response.ok({ + body: { + created, + noop, + updated, + ...(hasAnyErrors && { + errors: { ...kibanaErrors, ...(esErrors?.details ?? {}) }, + }), + }, + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); } diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index 642aec90c4748..50665e0494b81 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -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; 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', () => { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index 6175ba6f4d64f..16e2ab819e781 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -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), - }; - }), + }) + .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; + 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 [features, rawRoles] = await Promise.all([ + getFeatures(), + esClient.asCurrentUser.security.getRole( + { name: request.params.name }, + { ignore: [404] } + ), + ]); - const { validationErrors } = validateKibanaPrivileges(features, request.body.kibana); + 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 (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}`, - }, - }); - } + 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 : [] - ); + const body = transformPutPayloadToElasticsearchRole( + request.body, + authz.applicationName, + rawRoles[name] ? rawRoles[name].applications : [] + ); - await esClient.asCurrentUser.security.putRole({ - name: request.params.name, - body, - }); + await esClient.asCurrentUser.security.putRole({ + name: request.params.name, + body, + }); - if (roleGrantsSubFeaturePrivileges(features, request.body)) { - getFeatureUsageService().recordSubFeaturePrivilegeUsage(); - } + if (roleGrantsSubFeaturePrivileges(features, request.body)) { + getFeatureUsageService().recordSubFeaturePrivilegeUsage(); + } - return response.noContent(); - } catch (error) { - return response.customError(wrapIntoCustomErrorResponse(error)); - } - }) - ); + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); } diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json index 21a32edcec212..535e221f8e5fb 100644 --- a/x-pack/plugins/security/tsconfig.json +++ b/x-pack/plugins/security/tsconfig.json @@ -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/**/*", diff --git a/x-pack/plugins/spaces/common/constants.ts b/x-pack/plugins/spaces/common/constants.ts index bbbe38451fedf..14932a93a06b7 100644 --- a/x-pack/plugins/spaces/common/constants.ts +++ b/x-pack/plugins/spaces/common/constants.ts @@ -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', + }, +}; diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index 65342bf2e43f4..21fee91bf979d 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -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 { diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts index 962f02ca2bd79..d2f2681bd1809 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -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 = new BehaviorSubject(null); @@ -49,11 +55,11 @@ export class SpacesManager { public async getSpaces(options: GetAllSpacesOptions = {}): Promise { 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 { - 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[]) { diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index f50b73d7f8513..c3c13eeff04bb 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -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, }; diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.ts index 6ede0fa220043..515f8811e5dcf 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.ts @@ -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(), - }), + }) + .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')) - ); + 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.customError(wrapError(error)); - } - return response.noContent(); - }) - ); + return response.noContent(); + }) + ); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts index 3b5774284f19f..38f63202bc08b 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts @@ -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, }; }; diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.ts b/x-pack/plugins/spaces/server/routes/api/external/get.ts index dce169449c99a..8d4e3c0c359ef 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.ts @@ -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(), - }), + }) + .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(); + 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)); } - return response.customError(wrapError(error)); - } - }) - ); + }) + ); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts index d2f8162a3f236..a1cbc729c999d 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts @@ -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; const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts index 603dc3dfe45ba..baa47ca6956dc 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts @@ -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()) - ), - }), + }) + .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`); + createLicensedRouteHandler(async (context, request, response) => { + log.debug(`Inside GET /api/spaces/space`); - const { purpose, include_authorized_purposes: includeAuthorizedPurposes } = request.query; + const { purpose, include_authorized_purposes: includeAuthorizedPurposes } = request.query; - const spacesClient = getSpacesService().createSpacesClient(request); + const spacesClient = getSpacesService().createSpacesClient(request); - let spaces: Space[]; + 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)); - } + 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 }); - }) - ); + return response.ok({ body: spaces }); + }) + ); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index 9b017839fb25c..984d684762159 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -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, }; }; diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.ts b/x-pack/plugins/spaces/server/routes/api/external/post.ts index db3cc2c1cdcae..c47ea4ad5f9bf 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.ts @@ -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), + }) + .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 }); + 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)); } - log.debug(`Error creating space: ${error}`); - return response.customError(wrapError(error)); - } - }) - ); + }) + ); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index fd255a8aadc2b..8aa71d30fc4bb 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -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, }; }; diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.ts b/x-pack/plugins/spaces/server/routes/api/external/put.ts index 4612aedb4c151..10374dc94f600 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.ts @@ -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), + }) + .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); + createLicensedRouteHandler(async (context, request, response) => { + const spacesClient = getSpacesService().createSpacesClient(request); - const space = request.body; - const id = request.params.id; + 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(); + 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.customError(wrapError(error)); - } - return response.ok({ body: result }); - }) - ); + return response.ok({ body: result }); + }) + ); } diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index 20d3f7d3175d8..b669f97f0b8a1 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -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/**/*", diff --git a/x-pack/test/common/services/spaces.ts b/x-pack/test/common/services/spaces.ts index a1657996239ac..98cc54e456200 100644 --- a/x-pack/test/common/services/spaces.ts +++ b/x-pack/test/common/services/spaces.ts @@ -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 diff --git a/x-pack/test_serverless/api_integration/test_suites/common/management/multiple_spaces_enabled.ts b/x-pack/test_serverless/api_integration/test_suites/common/management/multiple_spaces_enabled.ts index e5c4053b97e73..f90539f0cbfef 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/management/multiple_spaces_enabled.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/management/multiple_spaces_enabled.ts @@ -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' + ), + }); + 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.', }); - svlCommonApi.assertResponseStatusCode(200, status, body); + }); + + 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); + }); }); }); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/management/spaces.ts b/x-pack/test_serverless/api_integration/test_suites/common/management/spaces.ts index 7014dcd6100df..4bde897e714af 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/management/spaces.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/management/spaces.ts @@ -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,93 +35,82 @@ 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: [], + // Should fail due to maximum spaces limit, not because of lacking internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: + 'Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting', }); + }); - svlCommonApi.assertResponseStatusCode(200, status, body); - }); - - it('#copyToSpace', async () => { - const { body, status } = await supertestAdminWithApiKey - .post('/api/spaces/_copy_saved_objects') - .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(status).toBe(200); + }); - svlCommonApi.assertResponseStatusCode(400, status, body); - }); + 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([ + expect.objectContaining({ + id: 'default', + }), + ]) + ); + expect(status).toBe(200); + }); - it('#resolveCopyToSpaceErrors', async () => { - const { body, status } = await supertestAdminWithApiKey - .post('/api/spaces/_resolve_copy_saved_objects_errors') - .set(samlAuth.getInternalRequestHeader()); + it('#update', async () => { + const { body, status } = await supertestAdminWithApiKey + .put('/api/spaces/space/default') + .send({ + id: 'default', + name: 'UPDATED!', + disabledFeatures: [], + }); - svlCommonApi.assertResponseStatusCode(400, status, body); - }); + svlCommonApi.assertResponseStatusCode(200, status, body); + }); - it('#updateObjectsSpaces', async () => { - const { body, status } = await supertestAdminWithApiKey - .post('/api/spaces/_update_objects_spaces') - .set(samlAuth.getInternalRequestHeader()); + it('#delete', async () => { + const { body, status } = await supertestAdminWithApiKey.delete( + '/api/spaces/space/default' + ); - svlCommonApi.assertResponseStatusCode(400, status, body); - }); + 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' }], + // 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.', }); - - 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 () => { + it('#getActiveSpace requires internal header', async () => { let body: any; let status: number; - ({ body, status } = await supertestAdminWithApiKey - .get('/api/spaces/space/default') + ({ body, status } = await supertestAdminWithCookieCredentials + .get('/internal/spaces/_active_space') .set(samlAuth.getCommonRequestHeader())); // expect a rejection because we're not using the internal header expect(body).toEqual({ @@ -131,8 +122,8 @@ export default function ({ getService }: FtrProviderContext) { }); expect(status).toBe(400); - ({ body, status } = await supertestAdminWithApiKey - .get('/api/spaces/space/default') + ({ body, status } = await supertestAdminWithCookieCredentials + .get('/internal/spaces/_active_space') .set(samlAuth.getInternalRequestHeader())); // expect success because we're using the internal header expect(body).toEqual( @@ -143,64 +134,131 @@ export default function ({ getService }: FtrProviderContext) { expect(status).toBe(200); }); - it('#getAll requires internal header', async () => { + 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 - .get('/api/spaces/space') - .set(samlAuth.getCommonRequestHeader())); + .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 [get] exists but is not available with the current configuration' + 'method [post] exists but is not available with the current configuration' ), }); - expect(status).toBe(400); ({ body, status } = await supertestAdminWithApiKey - .get('/api/spaces/space') + .post('/api/spaces/_resolve_copy_saved_objects_errors') .set(samlAuth.getInternalRequestHeader())); - // expect success because we're using the internal header - expect(body).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: 'default', - }), - ]) - ); - expect(status).toBe(200); + + 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('#getActiveSpace requires internal header', async () => { + it('#updateObjectsSpaces requires internal header', async () => { let body: any; let status: number; - ({ body, status } = await supertestAdminWithCookieCredentials - .get('/internal/spaces/_active_space') - .set(samlAuth.getCommonRequestHeader())); + ({ 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 [get] exists but is not available with the current configuration' + 'method [post] exists but is not available with the current configuration' ), }); - expect(status).toBe(400); - ({ body, status } = await supertestAdminWithCookieCredentials - .get('/internal/spaces/_active_space') + ({ body, status } = await supertestAdminWithApiKey + .post('/api/spaces/_update_objects_spaces') .set(samlAuth.getInternalRequestHeader())); - // expect success because we're using the internal header - expect(body).toEqual( - expect.objectContaining({ - id: 'default', - }) - ); - expect(status).toBe(200); + + 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); }); }); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts index 4c77607d7d844..bc01b14848eff 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts @@ -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); }); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/roles_routes_feature_flag.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/roles_routes_feature_flag.ts index da03ad14047b1..7f2237eda4b44 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/roles_routes_feature_flag.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/roles_routes_feature_flag.ts @@ -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); + }); }); }); });