diff --git a/.buildkite/scripts/steps/es_serverless/promote_es_serverless_image.sh b/.buildkite/scripts/steps/es_serverless/promote_es_serverless_image.sh index df58d534bb4f9..f1c7de0f81396 100755 --- a/.buildkite/scripts/steps/es_serverless/promote_es_serverless_image.sh +++ b/.buildkite/scripts/steps/es_serverless/promote_es_serverless_image.sh @@ -78,4 +78,5 @@ steps: build: env: IMAGES_CONFIG="kibana/images.yml" + RETRY="1" EOF diff --git a/.buildkite/scripts/steps/es_snapshots/promote.sh b/.buildkite/scripts/steps/es_snapshots/promote.sh index 7028cafd6e3f7..cb717ac5c9229 100755 --- a/.buildkite/scripts/steps/es_snapshots/promote.sh +++ b/.buildkite/scripts/steps/es_snapshots/promote.sh @@ -24,5 +24,6 @@ steps: build: env: IMAGES_CONFIG="kibana/images.yml" + RETRY="1" EOF fi diff --git a/docs/canvas/canvas-expression-lifecycle.asciidoc b/docs/canvas/canvas-expression-lifecycle.asciidoc deleted file mode 100644 index a20181c4b3808..0000000000000 --- a/docs/canvas/canvas-expression-lifecycle.asciidoc +++ /dev/null @@ -1,263 +0,0 @@ -[role="xpack"] -[[canvas-expression-lifecycle]] -== Canvas expression lifecycle - -Elements in Canvas are all created using an *expression language* that defines how to retrieve, manipulate, and ultimately visualize data. The goal is to allow you to do most of what you need without understanding the *expression language*, but learning how it works unlocks a lot of Canvas's power. - - -[[canvas-expressions-always-start-with-a-function]] -=== Expressions always start with a function - -Expressions simply execute <> in a specific order, which produce some output value. That output can then be inserted into another function, and another after that, until it produces the output you need. - -To use demo dataset available in Canvas to produce a table, run the following expression: - -[source,text] ----- -/* Simple demo table */ -filters -| demodata -| table -| render ----- - -This expression starts out with the <> function, which provides the value of any time filters or dropdown filters in the workpad. This is then inserted into <>, a function that returns exactly what you expect, demo data. Because the <> function receives the filter information from the <> function before it, it applies those filters to reduce the set of data it returns. We call the output from the previous function _context_. - -The filtered <> becomes the _context_ of the next function, <>, which creates a table visualization from this data set. The <> function isn’t strictly required, but by being explicit, you have the option of providing arguments to control things like the font used in the table. The output of the <> function becomes the _context_ of the <> function. Like the <>, the <> function isn’t required either, but it allows access to other arguments, such as styling the border of the element or injecting custom CSS. - -It is possible to add comments to the expression by starting them with a `//` sequence or by using `/*` and `*/` to enclose multi-line comments. - -[[canvas-function-arguments]] -=== Function arguments - -Let’s look at another expression, which uses the same <> function, but instead produces a pie chart. - -image::images/canvas-functions-can-take-arguments-pie-chart.png[Pie chart showing output of demodata function] -[source,text] ----- -filters -| demodata -| pointseries color="state" size="max(price)" -| pie -| render ----- - -To produce a filtered set of random data, the expression uses the <> and <> functions. This time, however, the output becomes the context for the <> function, which is a way to aggregate your data, similar to how Elasticsearch works, but more generalized. In this case, the data is split up using the `color` and `size` dimensions, using arguments on the <> function. Each unique value in the state column will have an associated size value, which in this case, will be the maximum value of the price column. - -If the expression stopped there, it would produce a `pointseries` data type as the output of this expression. But instead of looking at the raw values, the result is inserted into the <> function, which will produce an output that will render a pie visualization. And just like before, this is inserted into the <> function, which is useful for its arguments. - -The end result is a simple pie chart that uses the default color palette, but the <> function can take additional arguments that control how it gets rendered. For example, you can provide a `hole` argument to turn your pie chart into a donut chart by changing the expression to: - - -image::images/canvas-functions-can-take-arguments-donut-chart.png[Alternative output as donut chart] -[source,text] ----- -filters -| demodata -| pointseries color="state" size="max(price)" -| pie hole=50 -| render ----- - - -[[canvas-aliases-and-unnamed-arguments]] -=== Aliases and unnamed arguments - -Argument definitions have one canonical name, which is always provided in the underlying code. When argument definitions are used in an expression, they often include aliases that make them easier or faster to type. - -For example, the <> function has 2 arguments: - -* `expression` - Produces a calculated value. -* `name` - The name of column. - -The `expression` argument includes some aliases, namely `exp`, `fn`, and `function`. That means that you can use any of those four options to provide that argument’s value. - -So `mapColumn name=newColumn fn={string example}` is equal to `mapColumn name=newColumn expression={string example}`. - -There’s also a special type of alias which allows you to leave off the argument’s name entirely. The alias for this is an underscore, which indicates that the argument is an _unnamed_ argument and can be provided without explicitly naming it in the expression. The `name` argument here uses the _unnamed_ alias, which means that you can further simplify our example to `mapColumn newColumn fn={string example}`. - -NOTE: There can only be one _unnamed_ argument for each function. - - -[[canvas-change-your-expression-change-your-output]] -=== Change your expression, change your output -You can substitute one function for another to change the output. For example, you could change the visualization by swapping out the <> function for another renderer, a function that returns a `render` data type. - -Let’s change that last pie chart into a bubble chart by replacing the <> function with the <> function. This is possible because both functions can accept a `pointseries` data type as their _context_. Switching the functions will work, but it won’t produce a useful visualization on its own since you don’t have the x-axis and y-axis defined. You will also need to modify the <> function to change its output. In this case, you can change the `size` argument to `y`, so the maximum price values are plotted on the y-axis, and add an `x` argument using the `@timestamp` field in the data to plot those values over time. This leaves you with the following expression and produces a bubble chart showing the max price of each state over time: - -image::images/canvas-change-your-expression-chart.png[Bubble Chart, with price along x axis, and time along y axis] -[source,text] ----- -filters -| demodata -| pointseries color="state" y="max(price)" x="@timestamp" -| plot -| render ----- - -Similar to the <> function, the <> function takes arguments that control the design elements of the visualization. As one example, passing a `legend` argument with a value of `false` to the function will hide the legend on the chart. - -image::images/canvas-change-your-expression-chart-no-legend.png[Bubble Chart Without Legend] -[source,text,subs=+quotes] ----- -filters -| demodata -| pointseries color="state" y="max(price)" x="@timestamp" -| plot *legend=false* -| render ----- - - -[[canvas-fetch-and-manipulate-data]] -=== Fetch and manipulate data -So far, you have only seen expressions as a way to produce visualizations, but that’s not really what’s happening. Expressions only produce data, which is then used to create something, which in the case of Canvas, means rendering an element. An element can be a visualization, driven by data, but it can also be something much simpler, like a static image. Either way, an expression is used to produce an output that is used to render the desired result. For example, here’s an expression that shows an image: - -[source,text] ----- -image dataurl=https://placekitten.com/160/160 mode="cover" ----- - -But as mentioned, this doesn’t actually _render that image_, but instead it _produces some output that can be used to render that image_. That’s an important distinction, and you can see the actual output by adding in the render function and telling it to produce debug output. For example: - -[source,text] ----- -image dataurl=https://placekitten.com/160/160 mode="cover" -| render as=debug ----- - -The follow appears as JSON output: - -[source,JSON] ----- -{ - "type": "image", - "mode": "cover", - "dataurl": "https://placekitten.com/160/160" -} ----- - -NOTE: You may need to expand the element’s size to see the whole output. - -Canvas uses this output’s data type to map to a specific renderer and passes the entire output into it. It’s up to the image render function to produce an image on the workpad’s page. In this case, the expression produces some JSON output, but expressions can also produce other, simpler data, like a string or a number. Typically, useful results use JSON. - -Canvas uses the output to render an element, but other applications can use expressions to do pretty much anything. As stated previously, expressions simply execute functions, and the functions are all written in Javascript. That means if you can do something in Javascript, you can do it with an expression. - -This can include: - -* Sending emails -* Sending notifications -* Reading from a file -* Writing to a file -* Controlling devices with WebUSB or Web Bluetooth -* Consuming external APIs - -If your Javascript works in the environment where the code will run, such as in Node.js or in a browser, you can do it with an expression. - -[[canvas-expressions-compose-functions-with-subexpressions]] -=== Compose functions with sub-expressions - -You may have noticed another syntax in examples from other sections, namely expressions inside of curly brackets. These are called sub-expressions, and they can be used to provide a calculated value to another expression, instead of just a static one. - -A simple example of this is when you upload your own images to a Canvas workpad. That upload becomes an asset, and that asset can be retrieved using the `asset` function. Usually you’ll just do this from the UI, adding an image element to the page and uploading your image from the control in the sidebar, or picking an existing asset from there as well. In both cases, the system will consume that asset via the `asset` function, and you’ll end up with an expression similar to this: - -[source,text] ----- -image dataurl={asset 3cb3ec3a-84d7-48fa-8709-274ad5cc9e0b} ----- - -Sub-expressions are executed before the function that uses them is executed. In this case, `asset` will be run first, it will produce a value, the base64-encoded value of the image and that value will be used as the value for the `dataurl` argument in the <> function. After the asset function executes, you will get the following output: - -[source,text] ----- -image dataurl="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0" ----- - -Since all of the sub-expressions are now resolved into actual values, the <> function can be executed to produce its JSON output, just as it’s explained previously. In the case of images, the ability to nest sub-expressions is particularly useful to show one of several images conditionally. For example, you could swap between two images based on some calculated value by mixing in the <> function, like in this example expression: - -[source,text] ----- -demodata -| image dataurl={ - if condition={getCell price | gte 100} - then={asset "asset-3cb3ec3a-84d7-48fa-8709-274ad5cc9e0b"} - else={asset "asset-cbc11a1f-8f25-4163-94b4-2c3a060192e7"} -} ----- - -NOTE: The examples in this section can’t be copy and pasted directly, since the values used throughout will not exist in your workpad. - -Here, the expression to use for the value of the `condition` argument, `getCell price | gte 100`, runs first since it is nested deeper. - -The expression does the following: - -* Retrieves the value from the *price* column in the first row of the `demodata` data table -* Inputs the value to the `gte` function -* Compares the value to `100` -* Returns `true` if the value is 100 or greater, and `false` if the value is 100 or less - -That boolean value becomes the value for the `condition` argument. The output from the `then` expression is used as the output when `condition` is `true`. The output from the `else` expression is used when `condition` is false. In both cases, a base64-encoded image will be returned, and one of the two images will be displayed. - -You might be wondering how the <> function in the sub-expression accessed the data from the <> function, even though <> was not being directly inserted into <>. The answer is simple, but important to understand. When nested sub-expressions are executed, they automatically receive the same _context_, or output of the previous function that its parent function receives. In this specific expression, demodata’s data table is automatically provided to the nested expression’s `getCell` function, which allows that expression to pull out a value and compare it to another value. - -The passing of the _context_ is automatic, and it happens no matter how deeply you nest your sub-expressions. To demonstrate this, let’s modify the expression slightly to compare the value of the price against multiple conditions using the <> function. - -[source,text] ----- -demodata -| image dataurl={ - if condition={getCell price | all {gte 100} {neq 105}} - then={asset 3cb3ec3a-84d7-48fa-8709-274ad5cc9e0b} - else={asset cbc11a1f-8f25-4163-94b4-2c3a060192e7} -} ----- - -This time, `getCell price` is run, and the result is passed into the next function as the context. Then, each sub-expression of the <> function is run, with the context given to their parent, which in this case is the result of `getCell price`. If `all` of these sub-expressions evaluate to `true`, then the `if` condition argument will be true. - -Sub-expressions can seem a little foreign, especially if you aren’t a developer, but they’re worth getting familiar with, since they provide a ton of power and flexibility. Since you can nest any expression you want, you can also use this behavior to mix data from multiple indices, or even data from multiple sources. As an example, you could query an API for a value to use as part of the query provided to <>. - -This whole section is really just scratching the surface, but hopefully after reading it, you at least understand how to read expressions and make sense of what they are doing. With a little practice, you’ll get the hang of mixing _context_ and sub-expressions together to turn any input into your desired output. - -[[canvas-handling-context-and-argument-types]] -=== Handling context and argument types -If you look through the <>, you may notice that all of them define what a function accepts and what it returns. Additionally, every argument includes a type property that specifies the kind of data that can be used. These two types of values are actually the same, and can be used as a guide for how to deal with piping to other functions and using subexpressions for argument values. - -To explain how this works, consider the following expression from the previous section: - -[source,text] ----- -image dataurl={asset 3cb3ec3a-84d7-48fa-8709-274ad5cc9e0b} ----- - -If you <> for the `image` function, you’ll see that it accepts the `null` data type and returns an `image` data type. Accepting `null` effectively means that it does not use context at all, so if you insert anything to `image`, the value that was produced previously will be ignored. When the function executes, it will produce an `image` output, which is simply an object of type `image` that contains the information required to render an image. - -NOTE: The function does not render an image itself. - -As explained in the "<>" section, the output of an expression is just data. So the `image` type here is just a specific shape of data, not an actual image. - -Next, let’s take a look at the `asset` function. Like `image`, it accepts `null`, but it returns something different, a `string` in this case. Because `asset` will produce a string, its output can be used as the input for any function or argument that accepts a string. - -<> for the `dataurl` argument, its type is `string`, meaning it will accept any kind of string. There are some rules about the value of the string that the function itself enforces, but as far as the interpreter is concerned, that expression is valid because the argument accepts a string and the output of `asset` is a string. - -The interpreter also attempts to cast some input types into others, which allows you to use a string input even when the function or argument calls for a number. Keep in mind that it’s not able to convert any string value, but if the string is a number, it can easily be cast into a `number` type. Take the following expression for example: - -[source,text] ----- -string "0.4" -| revealImage image={asset asset-06511b39-ec44-408a-a5f3-abe2da44a426} ----- - -If you <> for the `revealImage` function, you’ll see that it accepts a `number` but the `string` function returns a `string` type. In this case, because the string value is a number, it can be converted into a `number` type and used without you having to do anything else. - -Most `primitive` types can be converted automatically, as you might expect. You just saw that a `string` can be cast into a `number`, but you can also pretty easily cast things into `boolean` too, and you can cast anything to `null`. - -There are other useful type casting options available. For example, something of type `datatable` can be cast to a type `pointseries` simply by only preserving specific columns from the data (namely x, y, size, color, and text). This allows you to treat your source data, which is generally of type `datatable`, like a `pointseries` type simply by convention. - -You can fetch data from Elasticsearch using `essql`, which allows you to aggregate the data, provide a custom name for the value, and insert that data directly to another function that only accepts `pointseries` even though `essql` will output a `datatable` type. This makes the following example expression valid: - -[source,text] ----- -essql "SELECT user AS x, sum(cost) AS y FROM index GROUP BY user" -| plot ----- - -In the docs you can see that `essql` returns a `datatable` type, but `plot` expects a `pointseries` context. This works because the `datatable` output will have the columns `x` and `y` as a result of using `AS` in the sql statement to name them. Because the data follows the convention of the `pointseries` data type, casting it into `pointseries` is possible, and it can be passed directly to `plot` as a result. diff --git a/docs/upgrade-notes.asciidoc b/docs/upgrade-notes.asciidoc index 98f7feeac2d6a..1b1a76ccfcbaa 100644 --- a/docs/upgrade-notes.asciidoc +++ b/docs/upgrade-notes.asciidoc @@ -1432,6 +1432,35 @@ The `/agent_status` Fleet API now returns the following statuses: * `active` — All active ==== +[discrete] +[[kibana-150267]] +.Deprecated Saved objects APIs. (8.7) +[%collapsible] +==== +*Details* + +The following saved objects APIs have been deprecated. + +[source,text] +-- +/api/saved_objects/{type}/{id} +/api/saved_objects/resolve/{type}/{id} +/api/saved_objects/{type}/{id?} +/api/saved_objects/{type}/{id} +/api/saved_objects/_find +/api/saved_objects/{type}/{id} +/api/saved_objects/_bulk_get +/api/saved_objects/_bulk_create +/api/saved_objects/_bulk_resolve +/api/saved_objects/_bulk_update +/api/saved_objects/_bulk_delete +-- + +For more information, refer to ({kibana-pull}150267[#150267]). + +*Impact* + +Use dedicated public APIs instead, for example use <> to manage Data Views. +==== + [discrete] [[deprecation-119494]] .Updates Fleet API to improve consistency. (8.0) diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index e7b4fdaf20921..3767e59c56b74 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -185,8 +185,6 @@ include::{kibana-root}/docs/canvas/canvas-present-workpad.asciidoc[] include::{kibana-root}/docs/canvas/canvas-tutorial.asciidoc[] -include::{kibana-root}/docs/canvas/canvas-expression-lifecycle.asciidoc[] - include::{kibana-root}/docs/canvas/canvas-function-reference.asciidoc[] include::{kibana-root}/docs/canvas/canvas-tinymath-functions.asciidoc[] diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 78c8541059d26..9c9c3797d7dbc 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -50524,7 +50524,7 @@ tags: name: ml - name: roles - description: > - Export sets of saved objects that you want to import into {kib}, resolve + Export sets of saved objects that you want to import into Kibana, resolve import errors, and rotate an encryption key for encrypted saved objects with the saved objects APIs. diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 64d227b91979d..afa0c850be734 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -37229,7 +37229,7 @@ paths: description: > Indicates a successful call. NOTE: This HTTP response code indicates that the bulk operation succeeded. Errors pertaining to individual - objects will be returned in the response body. + objects will be returned in the response body. '400': content: application/json; Elastic-Api-Version=2023-10-31: @@ -37263,7 +37263,7 @@ paths: description: > Indicates a successful call. NOTE: This HTTP response code indicates that the bulk operation succeeded. Errors pertaining to individual - objects will be returned in the response body. + objects will be returned in the response body. '400': content: application/json; Elastic-Api-Version=2023-10-31: @@ -58900,7 +58900,7 @@ tags: name: ml - name: roles - description: > - Export sets of saved objects that you want to import into {kib}, resolve + Export sets of saved objects that you want to import into Kibana, resolve import errors, and rotate an encryption key for encrypted saved objects with the saved objects APIs. diff --git a/oas_docs/overlays/kibana.overlays.serverless.yaml b/oas_docs/overlays/kibana.overlays.serverless.yaml index 64040383ae38c..1054f774fe11e 100644 --- a/oas_docs/overlays/kibana.overlays.serverless.yaml +++ b/oas_docs/overlays/kibana.overlays.serverless.yaml @@ -4,7 +4,7 @@ info: title: Overlays for the Kibana API document version: 0.0.1 actions: - # Clean up server definitions +# Clean up server definitions - target: '$.servers.*' description: Remove all servers so we can add our own. remove: true @@ -15,12 +15,12 @@ actions: variables: kibana_url: default: localhost:5601 - # Mark all operations as beta +# Mark all operations as beta - target: "$.paths[*]['get','put','post','delete','options','head','patch','trace']" description: Add x-beta update: x-beta: true - # Add some tag descriptions and displayNames +# Add some tag descriptions and displayNames - target: '$.tags[?(@.name=="alerting")]' description: Change tag description and displayName update: @@ -50,6 +50,14 @@ actions: description: Change displayName update: x-displayName: "Machine learning" + - target: '$.tags[?(@.name=="roles")]' + description: Change displayName and description + update: + x-displayName: "Roles" + description: Manage the roles that grant Elasticsearch and Kibana privileges. + externalDocs: + description: Kibana role management + url: https://www.elastic.co/guide/en/kibana/master/kibana-role-management.html - target: '$.tags[?(@.name=="slo")]' description: Change displayName update: @@ -65,7 +73,7 @@ actions: x-displayName: "System" description: > Get information about the system status, resource usage, and installed plugins. - # Remove extra tags from operations +# Remove extra tags from operations - target: "$.paths[*][*].tags[1:]" description: Remove all but first tag from operations remove: true \ No newline at end of file diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/routes/post_validation_handler.ts b/packages/core/deprecations/core-deprecations-server-internal/src/routes/post_validation_handler.ts index b93c17af2f536..6719f8b99ff50 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/routes/post_validation_handler.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/routes/post_validation_handler.ts @@ -11,7 +11,7 @@ import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-server-int import type { CoreKibanaRequest } from '@kbn/core-http-router-server-internal'; import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal'; import { isObject } from 'lodash'; -import { RouteDeprecationInfo } from '@kbn/core-http-server/src/router/route'; +import { RouteDeprecationInfo } from '@kbn/core-http-server/src/router/route'; // shouldn't use deep imports import { buildApiDeprecationId } from '../deprecations'; interface Dependencies { diff --git a/packages/core/http/core-http-server/index.ts b/packages/core/http/core-http-server/index.ts index 9c12f6a09ac45..d2c4cfb5c16f7 100644 --- a/packages/core/http/core-http-server/index.ts +++ b/packages/core/http/core-http-server/index.ts @@ -120,6 +120,7 @@ export type { RouteSecurity, RouteSecurityGetter, InternalRouteSecurity, + RouteDeprecationInfo, } from './src/router'; export { validBodyOutput, diff --git a/packages/core/http/core-http-server/src/router/index.ts b/packages/core/http/core-http-server/src/router/index.ts index 8e2b9373c43bd..30a1b6a5c9cc1 100644 --- a/packages/core/http/core-http-server/src/router/index.ts +++ b/packages/core/http/core-http-server/src/router/index.ts @@ -64,6 +64,7 @@ export type { RouteSecurity, Privilege, PrivilegeSet, + RouteDeprecationInfo, } from './route'; export { validBodyOutput, ReservedPrivilegesSet } from './route'; diff --git a/packages/core/root/core-root-server-internal/src/server.ts b/packages/core/root/core-root-server-internal/src/server.ts index 5082a27930e87..d38a52b73494b 100644 --- a/packages/core/root/core-root-server-internal/src/server.ts +++ b/packages/core/root/core-root-server-internal/src/server.ts @@ -309,6 +309,7 @@ export class Server { elasticsearch: elasticsearchServiceSetup, deprecations: deprecationsSetup, coreUsageData: coreUsageDataSetup, + docLinks: docLinksSetup, }); const uiSettingsSetup = await this.uiSettings.setup({ diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts index c9a8656b3f753..c0df8b57094eb 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts @@ -8,7 +8,7 @@ */ import { schema } from '@kbn/config-schema'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { Logger } from '@kbn/logging'; @@ -24,11 +24,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerBulkCreateRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const { allowHttpApiAccess } = config; router.post( @@ -38,8 +39,7 @@ export const registerBulkCreateRoute = ( summary: `Create saved objects`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, }, validate: { query: schema.object({ diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts index 65209a6072748..21ea532cae170 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts @@ -8,7 +8,7 @@ */ import { schema } from '@kbn/config-schema'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { Logger } from '@kbn/logging'; @@ -24,11 +24,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerBulkDeleteRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const { allowHttpApiAccess } = config; router.post( @@ -38,8 +39,7 @@ export const registerBulkDeleteRoute = ( summary: `Delete saved objects`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, }, validate: { body: schema.arrayOf( diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts index 3f87ca12248ae..1a97377f872f6 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts @@ -8,7 +8,7 @@ */ import { schema } from '@kbn/config-schema'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { Logger } from '@kbn/logging'; @@ -24,11 +24,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerBulkGetRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const { allowHttpApiAccess } = config; router.post( @@ -38,8 +39,7 @@ export const registerBulkGetRoute = ( summary: `Get saved objects`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, }, validate: { body: schema.arrayOf( diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts index 8e19114e798e0..b464e06ac15c8 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts @@ -8,7 +8,7 @@ */ import { schema } from '@kbn/config-schema'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { Logger } from '@kbn/logging'; @@ -24,11 +24,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerBulkResolveRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const { allowHttpApiAccess } = config; router.post( @@ -38,8 +39,7 @@ export const registerBulkResolveRoute = ( summary: `Resolve saved objects`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, description: `Retrieve multiple Kibana saved objects by ID, using any legacy URL aliases if they exist. Under certain circumstances, when Kibana is upgraded, saved object migrations may necessitate regenerating some object IDs to enable new features. When an object's ID is regenerated, a legacy URL alias is created for that object, preserving its old ID. In such a scenario, that object can be retrieved with the bulk resolve API using either its new ID or its old ID.`, }, diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts index 825a5f95482c0..7793484e01819 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts @@ -8,7 +8,7 @@ */ import { schema } from '@kbn/config-schema'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { Logger } from '@kbn/logging'; @@ -24,11 +24,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerBulkUpdateRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const { allowHttpApiAccess } = config; router.put( @@ -38,8 +39,7 @@ export const registerBulkUpdateRoute = ( summary: `Update saved objects`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, }, validate: { body: schema.arrayOf( diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts index 57f4a10ed9377..4b6a58b107f12 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts @@ -8,7 +8,7 @@ */ import { schema } from '@kbn/config-schema'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { Logger } from '@kbn/logging'; @@ -24,11 +24,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerCreateRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const { allowHttpApiAccess } = config; router.post( @@ -38,8 +39,7 @@ export const registerCreateRoute = ( summary: `Create a saved object`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, }, validate: { params: schema.object({ diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts index 69287821d8049..b3e1bdab6da47 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts @@ -8,7 +8,7 @@ */ import { schema } from '@kbn/config-schema'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { Logger } from '@kbn/logging'; @@ -24,11 +24,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerDeleteRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const { allowHttpApiAccess } = config; router.delete( @@ -38,8 +39,7 @@ export const registerDeleteRoute = ( summary: `Delete a saved object`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, }, validate: { params: schema.object({ diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts index 884ba1ed5c423..534d765080be2 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts @@ -8,7 +8,7 @@ */ import { schema } from '@kbn/config-schema'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { Logger } from '@kbn/logging'; @@ -21,11 +21,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerFindRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const referenceSchema = schema.object({ type: schema.string(), @@ -42,8 +43,7 @@ export const registerFindRoute = ( summary: `Search for saved objects`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, }, validate: { query: schema.object({ diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts index 9fe3aa8ff20c7..12c9c774ae7b4 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts @@ -8,7 +8,7 @@ */ import { schema } from '@kbn/config-schema'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { Logger } from '@kbn/logging'; @@ -24,11 +24,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerGetRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const { allowHttpApiAccess } = config; router.get( @@ -38,8 +39,7 @@ export const registerGetRoute = ( summary: `Get a saved object`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, }, validate: { params: schema.object({ diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts index 48ac75e045148..a9a83d55a0daa 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts @@ -14,6 +14,8 @@ import type { IKibanaMigrator, } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import { DocLinksServiceSetup } from '@kbn/core-doc-links-server'; +import { RouteDeprecationInfo } from '@kbn/core-http-server'; import type { InternalSavedObjectsRequestHandlerContext } from '../internal_types'; import { registerGetRoute } from './get'; import { registerResolveRoute } from './resolve'; @@ -43,6 +45,7 @@ export function registerRoutes({ kibanaVersion, kibanaIndex, isServerless, + docLinks, }: { http: InternalHttpServiceSetup; coreUsageData: InternalCoreUsageDataSetup; @@ -52,28 +55,105 @@ export function registerRoutes({ kibanaVersion: string; kibanaIndex: string; isServerless: boolean; + docLinks: DocLinksServiceSetup; }) { const router = http.createRouter('/api/saved_objects/'); const internalOnServerless = isServerless ? 'internal' : 'public'; + const deprecationInfo: RouteDeprecationInfo = { + documentationUrl: `${docLinks.links.management.savedObjectsApiList}`, + severity: 'warning' as const, // will not break deployment upon upgrade + reason: { + type: 'deprecate' as const, + }, + }; - registerGetRoute(router, { config, coreUsageData, logger, access: internalOnServerless }); - registerResolveRoute(router, { config, coreUsageData, logger, access: internalOnServerless }); + const legacyDeprecationInfo = { + documentationUrl: `${docLinks.links.kibana.dashboardImportExport}`, + severity: 'warning' as const, // will not break deployment upon upgrade + reason: { + type: 'remove' as const, // no alternative for posting `.json`, requires file format change to `.ndjson` + }, + }; + + registerGetRoute(router, { + config, + coreUsageData, + logger, + access: internalOnServerless, + deprecationInfo, + }); + registerResolveRoute(router, { + config, + coreUsageData, + logger, + access: internalOnServerless, + deprecationInfo, + }); registerCreateRoute(router, { config, coreUsageData, logger, access: internalOnServerless, + deprecationInfo, + }); + registerDeleteRoute(router, { + config, + coreUsageData, + logger, + access: internalOnServerless, + deprecationInfo, + }); + registerFindRoute(router, { + config, + coreUsageData, + logger, + access: internalOnServerless, + deprecationInfo, + }); + registerUpdateRoute(router, { + config, + coreUsageData, + logger, + access: internalOnServerless, + deprecationInfo, + }); + registerBulkGetRoute(router, { + config, + coreUsageData, + logger, + access: internalOnServerless, + deprecationInfo, + }); + registerBulkCreateRoute(router, { + config, + coreUsageData, + logger, + access: internalOnServerless, + deprecationInfo, + }); + registerBulkResolveRoute(router, { + config, + coreUsageData, + logger, + access: internalOnServerless, + deprecationInfo, + }); + registerBulkUpdateRoute(router, { + config, + coreUsageData, + logger, + access: internalOnServerless, + deprecationInfo, + }); + registerBulkDeleteRoute(router, { + config, + coreUsageData, + logger, + access: internalOnServerless, + deprecationInfo, }); - registerDeleteRoute(router, { config, coreUsageData, logger, access: internalOnServerless }); - registerFindRoute(router, { config, coreUsageData, logger, access: internalOnServerless }); - registerUpdateRoute(router, { config, coreUsageData, logger, access: internalOnServerless }); - registerBulkGetRoute(router, { config, coreUsageData, logger, access: internalOnServerless }); - registerBulkCreateRoute(router, { config, coreUsageData, logger, access: internalOnServerless }); - registerBulkResolveRoute(router, { config, coreUsageData, logger, access: internalOnServerless }); - registerBulkUpdateRoute(router, { config, coreUsageData, logger, access: internalOnServerless }); - registerBulkDeleteRoute(router, { config, coreUsageData, logger, access: internalOnServerless }); registerExportRoute(router, { config, coreUsageData }); registerImportRoute(router, { config, coreUsageData }); @@ -85,12 +165,14 @@ export function registerRoutes({ coreUsageData, logger, access: internalOnServerless, + legacyDeprecationInfo, }); registerLegacyExportRoute(legacyRouter, { kibanaVersion, coreUsageData, logger, access: internalOnServerless, + legacyDeprecationInfo, }); const internalRouter = http.createRouter( diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/export.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/export.ts index f3cf776f7d977..6da4acf798e1b 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/export.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/export.ts @@ -11,7 +11,7 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; import type { Logger } from '@kbn/logging'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import type { InternalSavedObjectRouter } from '../../internal_types'; import { exportDashboards } from './lib'; @@ -22,11 +22,13 @@ export const registerLegacyExportRoute = ( coreUsageData, logger, access, + legacyDeprecationInfo, }: { kibanaVersion: string; coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + legacyDeprecationInfo: RouteDeprecationInfo; } ) => { router.get( @@ -39,6 +41,7 @@ export const registerLegacyExportRoute = ( }, options: { access, + deprecated: legacyDeprecationInfo, tags: ['api'], }, }, diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/import.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/import.ts index e45ef3205af18..ad205bf65f841 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/import.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/import.ts @@ -11,7 +11,7 @@ import { schema } from '@kbn/config-schema'; import type { Logger } from '@kbn/logging'; import type { SavedObject } from '@kbn/core-saved-objects-server'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import type { InternalSavedObjectRouter } from '../../internal_types'; import { importDashboards } from './lib'; @@ -22,11 +22,13 @@ export const registerLegacyImportRoute = ( coreUsageData, logger, access, + legacyDeprecationInfo, }: { maxImportPayloadBytes: number; coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + legacyDeprecationInfo: RouteDeprecationInfo; } ) => { router.post( @@ -50,6 +52,7 @@ export const registerLegacyImportRoute = ( body: { maxBytes: maxImportPayloadBytes, }, + deprecated: legacyDeprecationInfo, }, }, async (context, request, response) => { diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts index 28a6c82e9ffdf..debbacbb06337 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts @@ -8,7 +8,7 @@ */ import { schema } from '@kbn/config-schema'; -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { Logger } from '@kbn/logging'; @@ -20,11 +20,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerResolveRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const { allowHttpApiAccess } = config; router.get( @@ -34,8 +35,7 @@ export const registerResolveRoute = ( summary: `Resolve a saved object`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, description: `Retrieve a single Kibana saved object by ID, using any legacy URL alias if it exists. Under certain circumstances, when Kibana is upgraded, saved object migrations may necessitate regenerating some object IDs to enable new features. When an object's ID is regenerated, a legacy URL alias is created for that object, preserving its old ID. In such a scenario, that object can be retrieved with the resolve API using either its new ID or its old ID.`, }, diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts index cfedc3ce03d2a..6f372235070b4 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { RouteAccess } from '@kbn/core-http-server'; +import type { RouteAccess, RouteDeprecationInfo } from '@kbn/core-http-server'; import { schema } from '@kbn/config-schema'; import type { SavedObjectsUpdateOptions } from '@kbn/core-saved-objects-api-server'; import type { Logger } from '@kbn/logging'; @@ -25,11 +25,12 @@ interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; logger: Logger; access: RouteAccess; + deprecationInfo: RouteDeprecationInfo; } export const registerUpdateRoute = ( router: InternalSavedObjectRouter, - { config, coreUsageData, logger, access }: RouteDependencies + { config, coreUsageData, logger, access, deprecationInfo }: RouteDependencies ) => { const { allowHttpApiAccess } = config; router.put( @@ -39,8 +40,7 @@ export const registerUpdateRoute = ( summary: `Update a saved object`, tags: ['oas-tag:saved objects'], access, - // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} - deprecated: true, + deprecated: deprecationInfo, }, validate: { params: schema.object({ diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.test.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.test.ts index 667c4a5cea915..70e8fcf17c797 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.test.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.test.ts @@ -98,6 +98,7 @@ describe('SavedObjectsService', () => { elasticsearch: elasticsearchMock, deprecations: deprecationsSetup, coreUsageData: createCoreUsageDataSetupMock(), + docLinks: docLinksServiceMock.createSetupContract(), }; }; @@ -180,6 +181,18 @@ describe('SavedObjectsService', () => { expect(registerRoutesMock).toHaveBeenCalledWith(expect.objectContaining({ kibanaVersion })); }); + it('calls registerRoutes with docLinks', async () => { + const coreContext = createCoreContext(); + const mockedLinks = docLinksServiceMock.createSetupContract(); + + const soService = new SavedObjectsService(coreContext); + await soService.setup(createSetupDeps()); + + expect(registerRoutesMock).toHaveBeenCalledWith( + expect.objectContaining({ docLinks: mockedLinks }) + ); + }); + describe('#setClientFactoryProvider', () => { it('registers the factory to the clientProvider', async () => { const coreContext = createCoreContext(); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts index 522a8e1943a28..04be2f5929f0b 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts @@ -13,7 +13,7 @@ import type { Logger } from '@kbn/logging'; import { stripVersionQualifier } from '@kbn/std'; import type { ServiceStatus } from '@kbn/core-status-common'; import type { CoreContext, CoreService } from '@kbn/core-base-server-internal'; -import type { DocLinksServiceStart } from '@kbn/core-doc-links-server'; +import type { DocLinksServiceSetup, DocLinksServiceStart } from '@kbn/core-doc-links-server'; import type { KibanaRequest } from '@kbn/core-http-server'; import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal'; import type { @@ -99,6 +99,7 @@ export interface SavedObjectsSetupDeps { elasticsearch: InternalElasticsearchServiceSetup; coreUsageData: InternalCoreUsageDataSetup; deprecations: DeprecationRegistryProvider; + docLinks: DocLinksServiceSetup; } /** @internal */ @@ -135,7 +136,7 @@ export class SavedObjectsService this.logger.debug('Setting up SavedObjects service'); this.setupDeps = setupDeps; - const { http, elasticsearch, coreUsageData, deprecations } = setupDeps; + const { http, elasticsearch, coreUsageData, deprecations, docLinks } = setupDeps; const savedObjectsConfig = await firstValueFrom( this.coreContext.configService.atPath('savedObjects') @@ -164,6 +165,7 @@ export class SavedObjectsService kibanaIndex: MAIN_SAVED_OBJECT_INDEX, kibanaVersion: this.kibanaVersion, isServerless: this.coreContext.env.packageInfo.buildFlavor === 'serverless', + docLinks, }); registerCoreObjectTypes(this.typeRegistry); diff --git a/packages/core/saved-objects/docs/openapi/bundled.json b/packages/core/saved-objects/docs/openapi/bundled.json index 5b8f30b0f34c1..45cfdd7fa5055 100644 --- a/packages/core/saved-objects/docs/openapi/bundled.json +++ b/packages/core/saved-objects/docs/openapi/bundled.json @@ -21,7 +21,7 @@ { "name": "saved objects", "x-displayName": "Saved objects", - "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 with the saved objects APIs.\n\nTo manage a specific type of saved object, use the corresponding APIs.\nFor example, use:\n\n* [Data views](../group/endpoint-data-views)\n* [Spaces](https://www.elastic.co/guide/en/kibana/current/spaces-api.html)\n* [Short URLs](https://www.elastic.co/guide/en/kibana/current/short-urls-api.html)\n\nWarning: Do not write documents directly to the `.kibana` index. When you write directly to the `.kibana` index, the data becomes corrupted and permanently breaks future Kibana versions.\n" + "description": "Export sets of saved objects that you want to import into Kibana, resolve import errors, and rotate an encryption key for encrypted saved objects with the saved objects APIs.\n\nTo manage a specific type of saved object, use the corresponding APIs.\nFor example, use:\n\n* [Data views](../group/endpoint-data-views)\n* [Spaces](https://www.elastic.co/guide/en/kibana/current/spaces-api.html)\n* [Short URLs](https://www.elastic.co/guide/en/kibana/current/short-urls-api.html)\n\nWarning: Do not write documents directly to the `.kibana` index. When you write directly to the `.kibana` index, the data becomes corrupted and permanently breaks future Kibana versions.\n" } ], "paths": { @@ -1423,4 +1423,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/core/saved-objects/docs/openapi/bundled.yaml b/packages/core/saved-objects/docs/openapi/bundled.yaml index 68c79b406c2b0..9b5aad6e54958 100644 --- a/packages/core/saved-objects/docs/openapi/bundled.yaml +++ b/packages/core/saved-objects/docs/openapi/bundled.yaml @@ -14,7 +14,7 @@ tags: - name: saved objects x-displayName: Saved objects 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 with the saved objects APIs. + Export sets of saved objects that you want to import into Kibana, resolve import errors, and rotate an encryption key for encrypted saved objects with the saved objects APIs. To manage a specific type of saved object, use the corresponding APIs. For example, use: @@ -216,7 +216,7 @@ paths: responses: '200': description: | - Indicates a successful call. NOTE: This HTTP response code indicates that the bulk operation succeeded. Errors pertaining to individual objects will be returned in the response body. + Indicates a successful call. NOTE: This HTTP response code indicates that the bulk operation succeeded. Errors pertaining to individual objects will be returned in the response body. content: application/json: schema: @@ -248,7 +248,7 @@ paths: responses: '200': description: | - Indicates a successful call. NOTE: This HTTP response code indicates that the bulk operation succeeded. Errors pertaining to individual objects will be returned in the response body. + Indicates a successful call. NOTE: This HTTP response code indicates that the bulk operation succeeded. Errors pertaining to individual objects will be returned in the response body. content: application/json: schema: diff --git a/packages/core/saved-objects/docs/openapi/bundled_serverless.json b/packages/core/saved-objects/docs/openapi/bundled_serverless.json index fe4b832c495b7..67b8a710b5bcd 100644 --- a/packages/core/saved-objects/docs/openapi/bundled_serverless.json +++ b/packages/core/saved-objects/docs/openapi/bundled_serverless.json @@ -26,7 +26,7 @@ { "name": "saved objects", "x-displayName": "Saved objects", - "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 with the saved objects APIs.\n\nTo manage a specific type of saved object, use the corresponding APIs.\nFor example, use:\n\n[Data views](../group/endpoint-data-views)\n\nWarning: Do not write documents directly to the `.kibana` index. When you write directly to the `.kibana` index, the data becomes corrupted and permanently breaks future Kibana versions.\n" + "description": "Export sets of saved objects that you want to import into Kibana, resolve import errors, and rotate an encryption key for encrypted saved objects with the saved objects APIs.\n\nTo manage a specific type of saved object, use the corresponding APIs.\nFor example, use:\n\n[Data views](../group/endpoint-data-views)\n\nWarning: Do not write documents directly to the `.kibana` index. When you write directly to the `.kibana` index, the data becomes corrupted and permanently breaks future Kibana versions.\n" } ], "paths": { @@ -358,4 +358,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/core/saved-objects/docs/openapi/bundled_serverless.yaml b/packages/core/saved-objects/docs/openapi/bundled_serverless.yaml index 4517629a29a61..b874f0e32361b 100644 --- a/packages/core/saved-objects/docs/openapi/bundled_serverless.yaml +++ b/packages/core/saved-objects/docs/openapi/bundled_serverless.yaml @@ -17,7 +17,7 @@ tags: - name: saved objects x-displayName: Saved objects 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 with the saved objects APIs. + Export sets of saved objects that you want to import into Kibana, resolve import errors, and rotate an encryption key for encrypted saved objects with the saved objects APIs. To manage a specific type of saved object, use the corresponding APIs. For example, use: diff --git a/packages/core/saved-objects/docs/openapi/entrypoint.yaml b/packages/core/saved-objects/docs/openapi/entrypoint.yaml index 5cd9039988186..adb960a06dc4e 100644 --- a/packages/core/saved-objects/docs/openapi/entrypoint.yaml +++ b/packages/core/saved-objects/docs/openapi/entrypoint.yaml @@ -12,8 +12,8 @@ tags: - name: saved objects x-displayName: Saved objects 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 with the saved objects APIs. - + Export sets of saved objects that you want to import into Kibana, resolve import errors, and rotate an encryption key for encrypted saved objects with the saved objects APIs. + To manage a specific type of saved object, use the corresponding APIs. For example, use: diff --git a/packages/core/saved-objects/docs/openapi/entrypoint_serverless.yaml b/packages/core/saved-objects/docs/openapi/entrypoint_serverless.yaml index 69c742a8d7acd..f0d0be2ccf76b 100644 --- a/packages/core/saved-objects/docs/openapi/entrypoint_serverless.yaml +++ b/packages/core/saved-objects/docs/openapi/entrypoint_serverless.yaml @@ -12,8 +12,8 @@ tags: - name: saved objects x-displayName: Saved objects 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 with the saved objects APIs. - + Export sets of saved objects that you want to import into Kibana, resolve import errors, and rotate an encryption key for encrypted saved objects with the saved objects APIs. + To manage a specific type of saved object, use the corresponding APIs. For example, use: diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index fa55bd4800c8b..5493b8dc3bbdb 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -1073,6 +1073,7 @@ "urls" ], "synthetics-param": [], + "synthetics-private-location": [], "synthetics-privates-locations": [], "tag": [ "color", diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 9f94d36af50f7..726b6e9e1d4c5 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -3552,6 +3552,10 @@ "dynamic": false, "properties": {} }, + "synthetics-private-location": { + "dynamic": false, + "properties": {} + }, "synthetics-privates-locations": { "dynamic": false, "properties": {} diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 1294f72b9f208..4605dabf6eb94 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -355,6 +355,8 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D autocompleteSuggestions: `${KIBANA_DOCS}kibana-concepts-analysts.html#autocomplete-suggestions`, secureSavedObject: `${KIBANA_DOCS}xpack-security-secure-saved-objects.html`, xpackSecurity: `${KIBANA_DOCS}xpack-security.html`, + restApis: `${KIBANA_DOCS}api.html`, + dashboardImportExport: `${KIBANA_DOCS}dashboard-api.html`, }, upgradeAssistant: { overview: `${KIBANA_DOCS}upgrade-assistant.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 0bfb1c69fd6bb..f1a6a8d4b578d 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -314,6 +314,7 @@ export interface DocLinks { readonly autocompleteSuggestions: string; readonly secureSavedObject: string; readonly xpackSecurity: string; + readonly dashboardImportExport: string; }; readonly upgradeAssistant: { readonly overview: string; diff --git a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts index d5483f1fe0f9f..0b6ba0be80fab 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts @@ -109,6 +109,7 @@ const STANDARD_LIST_TYPES = [ 'synthetics-monitor', 'uptime-dynamic-settings', 'synthetics-privates-locations', + 'synthetics-private-location', 'osquery-saved-query', 'osquery-pack', diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index f31ec223e3d9b..7183dd057f26f 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -165,6 +165,7 @@ describe('checking migration metadata changes on all registered SO types', () => "synthetics-dynamic-settings": "4b40a93eb3e222619bf4e7fe34a9b9e7ab91a0a7", "synthetics-monitor": "5ceb25b6249bd26902c9b34273c71c3dce06dbea", "synthetics-param": "3ebb744e5571de678b1312d5c418c8188002cf5e", + "synthetics-private-location": "8cecc9e4f39637d2f8244eb7985c0690ceab24be", "synthetics-privates-locations": "f53d799d5c9bc8454aaa32c6abc99a899b025d5c", "tag": "e2544392fe6563e215bb677abc8b01c2601ef2dc", "task": "3c89a7c918d5b896a5f8800f06e9114ad7e7aea3", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index e95a82e63d0ff..ba06073e454a9 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -139,6 +139,7 @@ const previouslyRegisteredTypes = [ 'synthetics-monitor', 'synthetics-param', 'synthetics-privates-locations', + 'synthetics-private-location', 'tag', 'task', 'telemetry', diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_create.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_create.test.ts index 8d40280806379..e824e5124f6c6 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_create.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_create.test.ts @@ -21,7 +21,7 @@ import { } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; type SetupServerReturn = Awaited>; @@ -56,7 +56,14 @@ describe('POST /api/saved_objects/_bulk_create with allowApiAccess true', () => const logger = loggerMock.create(); const config = setupConfig(true); const access = 'public'; - registerBulkCreateRoute(router, { config, coreUsageData, logger, access }); + + registerBulkCreateRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_delete.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_delete.test.ts index 26a2d22403bc1..114b682fa51b4 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_delete.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; type SetupServerReturn = Awaited>; @@ -61,7 +61,13 @@ describe('POST /api/saved_objects/_bulk_delete with allowApiAccess as true', () const config = setupConfig(true); const access = 'public'; - registerBulkDeleteRoute(router, { config, coreUsageData, logger, access }); + registerBulkDeleteRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_get.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_get.test.ts index da491110b0717..bd6caca6233c8 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_get.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_get.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; type SetupServerReturn = Awaited>; @@ -58,7 +58,14 @@ describe('POST /api/saved_objects/_bulk_get with allowApiAccess true', () => { const config = setupConfig(true); const access = 'public'; - registerBulkGetRoute(router, { config, coreUsageData, logger, access }); + + registerBulkGetRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_resolve.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_resolve.test.ts index 054f55f518a66..f297f1b6a8cb4 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_resolve.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_resolve.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; type SetupServerReturn = Awaited>; @@ -59,7 +59,14 @@ describe('POST /api/saved_objects/_bulk_resolve with allowApiAccess true', () => const config = setupConfig(true); const access = 'public'; - registerBulkResolveRoute(router, { config, coreUsageData, logger, access }); + + registerBulkResolveRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_update.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_update.test.ts index 275e803f6ceb3..7625334bbe638 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/bulk_update.test.ts @@ -19,7 +19,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; type SetupServerReturn = Awaited>; const testTypes = [ @@ -52,7 +52,14 @@ describe('PUT /api/saved_objects/_bulk_update with allowApiAccess true', () => { const config = setupConfig(true); const access = 'public'; - registerBulkUpdateRoute(router, { config, coreUsageData, logger, access }); + + registerBulkUpdateRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/create.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/create.test.ts index 478c233466727..632b41fe886b2 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/create.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/create.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; type SetupServerReturn = Awaited>; @@ -58,7 +58,14 @@ describe('POST /api/saved_objects/{type} with allowApiAccess true', () => { const logger = loggerMock.create(); const config = setupConfig(true); const access = 'public'; - registerCreateRoute(router, { config, coreUsageData, logger, access }); + + registerCreateRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => { return testTypes diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/delete.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/delete.test.ts index 83cb90ab4d8d7..a1f3ff0bc60ec 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/delete.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; type SetupServerReturn = Awaited>; @@ -55,7 +55,14 @@ describe('DELETE /api/saved_objects/{type}/{id} with allowApiAccess true', () => const logger = loggerMock.create(); const config = setupConfig(true); const access = 'public'; - registerDeleteRoute(router, { config, coreUsageData, logger, access }); + + registerDeleteRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/find.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/find.test.ts index 3f7e0b815662e..c87546ea4887a 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/find.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/find.test.ts @@ -21,7 +21,7 @@ import { registerFindRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; type SetupServerReturn = Awaited>; @@ -72,7 +72,13 @@ describe('GET /api/saved_objects/_find with allowApiAccess true', () => { const config = setupConfig(true); const access = 'public'; - registerFindRoute(router, { config, coreUsageData, logger, access }); + registerFindRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/get.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/get.test.ts index c97ae350c0386..6220de6540685 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/get.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/get.test.ts @@ -25,7 +25,7 @@ import { } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; const coreId = Symbol('core'); const testTypes = [ @@ -80,7 +80,14 @@ describe('GET /api/saved_objects/{type}/{id} with allowApiAccess true', () => { const config = setupConfig(true); const access = 'public'; - registerGetRoute(router, { config, coreUsageData, logger, access }); + + registerGetRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/resolve.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/resolve.test.ts index bc00a418a13b2..f1f7fd1d6153e 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/resolve.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/resolve.test.ts @@ -25,7 +25,7 @@ import { } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; const coreId = Symbol('core'); @@ -81,7 +81,13 @@ describe('GET /api/saved_objects/resolve/{type}/{id} with allowApiAccess true', const config = setupConfig(true); const access = 'public'; - registerResolveRoute(router, { config, coreUsageData, logger, access }); + registerResolveRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/update.test.ts b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/update.test.ts index 25da93b526285..2a086f29d75d1 100644 --- a/src/core/server/integration_tests/saved_objects/routes/allow_api_access/update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/allow_api_access/update.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from '../routes_test_utils'; +import { deprecationMock, setupConfig } from '../routes_test_utils'; type SetupServerReturn = Awaited>; @@ -56,7 +56,14 @@ describe('PUT /api/saved_objects/{type}/{id?} with allowApiAccess true', () => { const config = setupConfig(true); const access = 'public'; - registerUpdateRoute(router, { config, coreUsageData, logger, access }); + + registerUpdateRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts index 3eaf3bbdc8865..033a5570c588a 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts @@ -21,7 +21,7 @@ import { } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; type SetupServerReturn = Awaited>; @@ -37,6 +37,7 @@ describe('POST /api/saved_objects/_bulk_create', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -57,11 +58,18 @@ describe('POST /api/saved_objects/_bulk_create', () => { const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'post'); const config = setupConfig(); const access = 'public'; - registerBulkCreateRoute(router, { config, coreUsageData, logger, access }); + registerBulkCreateRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); @@ -194,4 +202,24 @@ describe('POST /api/saved_objects/_bulk_create', () => { .expect(200); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); }); + + it('passes deprecation config to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_create') + .set('x-elastic-internal-origin', 'kibana') + .send([ + { + id: 'abc1234', + type: 'index-pattern', + attributes: { + title: 'foo', + }, + references: [], + }, + ]) + .expect(200); + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts index 24f2cf29fe14f..9421d5207b211 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; type SetupServerReturn = Awaited>; @@ -36,6 +36,7 @@ describe('POST /api/saved_objects/_bulk_delete', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -59,11 +60,18 @@ describe('POST /api/saved_objects/_bulk_delete', () => { const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'post'); const config = setupConfig(); const access = 'public'; - registerBulkDeleteRoute(router, { config, coreUsageData, logger, access }); + registerBulkDeleteRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); @@ -163,4 +171,21 @@ describe('POST /api/saved_objects/_bulk_delete', () => { .expect(400); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); }); + + it('passes deprecation configuration to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_delete') + .set('x-elastic-internal-origin', 'kibana') + .send([ + { + id: 'hiddenID', + type: 'hidden-from-http', + }, + ]) + .expect(400); + + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts index 519bdbb5f6c74..8d16ca5787350 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; type SetupServerReturn = Awaited>; @@ -36,6 +36,7 @@ describe('POST /api/saved_objects/_bulk_get', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -57,11 +58,18 @@ describe('POST /api/saved_objects/_bulk_get', () => { const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'post'); const config = setupConfig(); const access = 'public'; - registerBulkGetRoute(router, { config, coreUsageData, logger, access }); + registerBulkGetRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); @@ -150,4 +158,20 @@ describe('POST /api/saved_objects/_bulk_get', () => { .expect(200); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); }); + + it('passes deprecation config to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_get') + .set('x-elastic-internal-origin', 'kibana') + .send([ + { + id: 'abc123', + type: 'index-pattern', + }, + ]) + .expect(200); + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts index 2636d38fc28b5..800fccb00324d 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; type SetupServerReturn = Awaited>; @@ -36,6 +36,7 @@ describe('POST /api/saved_objects/_bulk_resolve', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -58,11 +59,17 @@ describe('POST /api/saved_objects/_bulk_resolve', () => { const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'post'); const config = setupConfig(); const access = 'public'; - - registerBulkResolveRoute(router, { config, coreUsageData, logger, access }); + registerBulkResolveRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); @@ -154,4 +161,20 @@ describe('POST /api/saved_objects/_bulk_resolve', () => { .expect(200); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); }); + + it('passes deprecation configuration to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_resolve') + .set('x-elastic-internal-origin', 'kibana') + .send([ + { + id: 'abc123', + type: 'index-pattern', + }, + ]) + .expect(200); + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts index 8ea206b4d902e..d746d1d5b9ca8 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; type SetupServerReturn = Awaited>; const testTypes = [ @@ -37,6 +37,7 @@ describe('PUT /api/saved_objects/_bulk_update', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -56,11 +57,18 @@ describe('PUT /api/saved_objects/_bulk_update', () => { const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'put'); const config = setupConfig(); const access = 'public'; - registerBulkUpdateRoute(router, { config, coreUsageData, logger, access }); + registerBulkUpdateRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); @@ -180,6 +188,7 @@ describe('PUT /api/saved_objects/_bulk_update', () => { }, ]) .expect(400); + expect(result.body.message).toContain('Unsupported saved object type(s):'); }); @@ -205,5 +214,35 @@ describe('PUT /api/saved_objects/_bulk_update', () => { ]) .expect(200); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); + + it('passes deprecation configuration to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .put('/api/saved_objects/_bulk_update') + .set('x-elastic-internal-origin', 'kibana') + .send([ + { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + attributes: { + title: 'An existing visualization', + }, + }, + { + type: 'dashboard', + id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', + attributes: { + title: 'An existing dashboard', + }, + }, + ]) + .expect(200); + + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/create.test.ts b/src/core/server/integration_tests/saved_objects/routes/create.test.ts index 3fe9f5fcc6f05..f2471e14fc128 100644 --- a/src/core/server/integration_tests/saved_objects/routes/create.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/create.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; type SetupServerReturn = Awaited>; @@ -36,6 +36,7 @@ describe('POST /api/saved_objects/{type}', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; const clientResponse = { id: 'logstash-*', @@ -58,10 +59,18 @@ describe('POST /api/saved_objects/{type}', () => { const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'post'); + const config = setupConfig(); const access = 'public'; - registerCreateRoute(router, { config, coreUsageData, logger, access }); + registerCreateRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => { return testTypes @@ -178,4 +187,19 @@ describe('POST /api/saved_objects/{type}', () => { .expect(200); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); }); + + it('passes deprecation configuration to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/index-pattern') + .set('x-elastic-internal-origin', 'kibana') + .send({ + attributes: { + title: 'Logging test', + }, + }) + .expect(200); + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/delete.test.ts b/src/core/server/integration_tests/saved_objects/routes/delete.test.ts index ba5c797469aa3..70d811cd97521 100644 --- a/src/core/server/integration_tests/saved_objects/routes/delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/delete.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; type SetupServerReturn = Awaited>; @@ -37,6 +37,7 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -55,9 +56,17 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'delete'); + const config = setupConfig(); const access = 'public'; - registerDeleteRoute(router, { config, coreUsageData, logger, access }); + registerDeleteRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); @@ -120,4 +129,13 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { .expect(200); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); }); + + it('passes deprecation configuration to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .delete('/api/saved_objects/index-pattern/logstash-*') + .expect(200); + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/find.test.ts b/src/core/server/integration_tests/saved_objects/routes/find.test.ts index b7d193db42525..d2048ba13b634 100644 --- a/src/core/server/integration_tests/saved_objects/routes/find.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/find.test.ts @@ -22,7 +22,7 @@ import { registerFindRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; type SetupServerReturn = Awaited>; @@ -42,6 +42,7 @@ describe('GET /api/saved_objects/_find', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; const clientResponse = { total: 0, @@ -71,11 +72,18 @@ describe('GET /api/saved_objects/_find', () => { const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'get'); const config = setupConfig(); const access = 'public'; - registerFindRoute(router, { config, coreUsageData, logger, access }); + registerFindRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); @@ -471,4 +479,14 @@ describe('GET /api/saved_objects/_find', () => { .expect(200); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); }); + + it('passes deprecation configuration to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/_find?type=foo&type=bar') + .set('x-elastic-internal-origin', 'kibana') + .expect(200); + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/get.test.ts b/src/core/server/integration_tests/saved_objects/routes/get.test.ts index 97868b9cc23d2..bb748ca478e8a 100644 --- a/src/core/server/integration_tests/saved_objects/routes/get.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/get.test.ts @@ -25,7 +25,7 @@ import { } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; const coreId = Symbol('core'); const testTypes = [ @@ -41,6 +41,7 @@ describe('GET /api/saved_objects/{type}/{id}', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; beforeEach(async () => { const coreContext = createCoreContext({ coreId }); @@ -80,10 +81,18 @@ describe('GET /api/saved_objects/{type}/{id}', () => { const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'get'); + const config = setupConfig(); const access = 'public'; - registerGetRoute(router, { config, coreUsageData, logger, access }); + registerGetRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); @@ -144,4 +153,14 @@ describe('GET /api/saved_objects/{type}/{id}', () => { .expect(200); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); }); + + it('passes deprecation configuration to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/index-pattern/logstash-*') + .set('x-elastic-internal-origin', 'kibana') + .expect(200); + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/export.test.ts b/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/export.test.ts index 008ad527e03e3..73f1ce075272c 100644 --- a/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/export.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/export.test.ts @@ -41,6 +41,7 @@ import { registerLegacyExportRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { legacyDeprecationMock } from '../routes_test_utils'; type SetupServerReturn = Awaited>; let coreUsageStatsClient: jest.Mocked; @@ -58,11 +59,13 @@ describe('POST /api/dashboards/export', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementLegacyDashboardsExport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerLegacyExportRoute(router, { kibanaVersion: 'mockversion', coreUsageData, logger: loggerMock.create(), access: 'public', + legacyDeprecationInfo: legacyDeprecationMock, }); handlerContext.savedObjects.client.bulkGet diff --git a/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/import.test.ts b/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/import.test.ts index 0355ac7d39706..c96c0e1d9011a 100644 --- a/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/import.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/import.test.ts @@ -41,6 +41,7 @@ import { registerLegacyImportRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { legacyDeprecationMock } from '../routes_test_utils'; type SetupServerReturn = Awaited>; let coreUsageStatsClient: jest.Mocked; @@ -58,11 +59,13 @@ describe('POST /api/dashboards/import', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementLegacyDashboardsImport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerLegacyImportRoute(router, { maxImportPayloadBytes: 26214400, coreUsageData, logger: loggerMock.create(), access: 'public', + legacyDeprecationInfo: legacyDeprecationMock, }); handlerContext.savedObjects.client.bulkCreate.mockResolvedValueOnce({ diff --git a/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts b/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts index e96c7ee9fb089..7812081e5329c 100644 --- a/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts @@ -25,7 +25,7 @@ import { } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; const coreId = Symbol('core'); @@ -42,6 +42,7 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; beforeEach(async () => { const coreContext = createCoreContext({ coreId }); @@ -79,10 +80,18 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => { const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'get'); + const config = setupConfig(); const access = 'public'; - registerResolveRoute(router, { config, coreUsageData, logger, access }); + registerResolveRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); @@ -142,4 +151,14 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => { .expect(200); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); }); + + it('passes deprecation configuration to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/resolve/index-pattern/logstash-*') + .set('x-elastic-internal-origin', 'kibana') + .expect(200); + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/routes_test_utils.ts b/src/core/server/integration_tests/saved_objects/routes/routes_test_utils.ts index b27b2b72aba97..3c78b4e7f0e3b 100644 --- a/src/core/server/integration_tests/saved_objects/routes/routes_test_utils.ts +++ b/src/core/server/integration_tests/saved_objects/routes/routes_test_utils.ts @@ -15,3 +15,19 @@ export function setupConfig(allowAccess: boolean = false) { } as SavedObjectConfig; return config; } + +export const deprecationMock = { + documentationUrl: 'http://elastic.co', + severity: 'warning' as const, + reason: { + type: 'deprecate' as const, + }, +}; + +export const legacyDeprecationMock = { + documentationUrl: 'http://elastic.co', + severity: 'warning' as const, + reason: { + type: 'remove' as const, + }, +}; diff --git a/src/core/server/integration_tests/saved_objects/routes/update.test.ts b/src/core/server/integration_tests/saved_objects/routes/update.test.ts index 47f3ef4b73652..909121429aefb 100644 --- a/src/core/server/integration_tests/saved_objects/routes/update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/update.test.ts @@ -20,7 +20,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { loggerMock } from '@kbn/logging-mocks'; -import { setupConfig } from './routes_test_utils'; +import { deprecationMock, setupConfig } from './routes_test_utils'; type SetupServerReturn = Awaited>; @@ -37,6 +37,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; let loggerWarnSpy: jest.SpyInstance; + let registrationSpy: jest.SpyInstance; beforeEach(async () => { const clientResponse = { @@ -66,10 +67,17 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); const logger = loggerMock.create(); loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registrationSpy = jest.spyOn(router, 'put'); const config = setupConfig(); const access = 'public'; - registerUpdateRoute(router, { config, coreUsageData, logger, access }); + registerUpdateRoute(router, { + config, + coreUsageData, + logger, + access, + deprecationInfo: deprecationMock, + }); await server.start(); }); @@ -145,4 +153,15 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { .expect(200); expect(loggerWarnSpy).toHaveBeenCalledTimes(1); }); + + it('passes deprecation configuration to the router arguments', async () => { + await supertest(httpSetup.server.listener) + .put('/api/saved_objects/index-pattern/logstash-*') + .set('x-elastic-internal-origin', 'kibana') + .send({ attributes: { title: 'Logging test' }, version: 'log' }) + .expect(200); + expect(registrationSpy.mock.calls[0][0]).toMatchObject({ + options: { deprecated: deprecationMock }, + }); + }); }); diff --git a/src/plugins/interactive_setup/server/routes/configure.ts b/src/plugins/interactive_setup/server/routes/configure.ts index 1cdaf588a6cd9..bb5a85800e03b 100644 --- a/src/plugins/interactive_setup/server/routes/configure.ts +++ b/src/plugins/interactive_setup/server/routes/configure.ts @@ -37,6 +37,13 @@ export function defineConfigureRoute({ router.post( { path: '/internal/interactive_setup/configure', + security: { + authz: { + enabled: false, + reason: + 'Interactive setup is strictly a "pre-boot" feature which cannot leverage conventional authorization.', + }, + }, validate: { body: schema.object({ host: schema.uri({ scheme: ['http', 'https'] }), diff --git a/src/plugins/interactive_setup/server/routes/enroll.ts b/src/plugins/interactive_setup/server/routes/enroll.ts index 1cd0362d2790b..7ee97db592ac5 100644 --- a/src/plugins/interactive_setup/server/routes/enroll.ts +++ b/src/plugins/interactive_setup/server/routes/enroll.ts @@ -40,6 +40,13 @@ export function defineEnrollRoutes({ router.post( { path: '/internal/interactive_setup/enroll', + security: { + authz: { + enabled: false, + reason: + 'Interactive setup is strictly a "pre-boot" feature which cannot leverage conventional authorization.', + }, + }, validate: { body: schema.object({ hosts: schema.arrayOf(schema.uri({ scheme: 'https' }), { diff --git a/src/plugins/interactive_setup/server/routes/ping.ts b/src/plugins/interactive_setup/server/routes/ping.ts index 4deaeee675404..4c71d9f05bd1b 100644 --- a/src/plugins/interactive_setup/server/routes/ping.ts +++ b/src/plugins/interactive_setup/server/routes/ping.ts @@ -17,6 +17,13 @@ export function definePingRoute({ router, logger, elasticsearch, preboot }: Rout router.post( { path: '/internal/interactive_setup/ping', + security: { + authz: { + enabled: false, + reason: + 'Interactive setup is strictly a "pre-boot" feature which cannot leverage conventional authorization.', + }, + }, validate: { body: schema.object({ host: schema.uri({ scheme: ['http', 'https'] }), diff --git a/src/plugins/interactive_setup/server/routes/status.ts b/src/plugins/interactive_setup/server/routes/status.ts index 78a97ac862317..14c94411ded53 100644 --- a/src/plugins/interactive_setup/server/routes/status.ts +++ b/src/plugins/interactive_setup/server/routes/status.ts @@ -15,6 +15,13 @@ export function defineStatusRoute({ router, elasticsearch, preboot }: RouteDefin router.get( { path: '/internal/interactive_setup/status', + security: { + authz: { + enabled: false, + reason: + 'Interactive setup is strictly a "pre-boot" feature which cannot leverage conventional authorization.', + }, + }, validate: false, options: { authRequired: false }, }, diff --git a/src/plugins/interactive_setup/server/routes/verify.ts b/src/plugins/interactive_setup/server/routes/verify.ts index a40e35794fb9e..7fb5bb2e70c18 100644 --- a/src/plugins/interactive_setup/server/routes/verify.ts +++ b/src/plugins/interactive_setup/server/routes/verify.ts @@ -15,6 +15,13 @@ export function defineVerifyRoute({ router, verificationCode }: RouteDefinitionP router.post( { path: '/internal/interactive_setup/verify', + security: { + authz: { + enabled: false, + reason: + 'Interactive setup is strictly a "pre-boot" feature which cannot leverage conventional authorization.', + }, + }, validate: { body: schema.object({ code: schema.string(), diff --git a/test/functional/apps/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts index 99e97d979d281..451e546135599 100644 --- a/test/functional/apps/console/_autocomplete.ts +++ b/test/functional/apps/console/_autocomplete.ts @@ -351,7 +351,8 @@ GET _search }); }); - describe('index fields autocomplete', () => { + // FLAKY: https://github.com/elastic/kibana/issues/198109 + describe.skip('index fields autocomplete', () => { const indexName = `index_field_test-${Date.now()}-${Math.random()}`; before(async () => { diff --git a/test/functional/apps/discover/group3/_lens_vis.ts b/test/functional/apps/discover/group3/_lens_vis.ts index 0864382cad7a8..03641ee5bcb41 100644 --- a/test/functional/apps/discover/group3/_lens_vis.ts +++ b/test/functional/apps/discover/group3/_lens_vis.ts @@ -110,7 +110,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return seriesType; } - describe('discover lens vis', function () { + // FLAKY: https://github.com/elastic/kibana/issues/184600 + describe.skip('discover lens vis', function () { before(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credential_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credential_form.tsx index 638af9617e008..7d6d42c70e767 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credential_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credential_form.tsx @@ -500,7 +500,7 @@ export const GcpCredentialsForm = ({ diff --git a/x-pack/plugins/data_usage/common/test_utils/test_query_client_options.ts b/x-pack/plugins/data_usage/common/test_utils/test_query_client_options.ts new file mode 100644 index 0000000000000..c674e9b342eea --- /dev/null +++ b/x-pack/plugins/data_usage/common/test_utils/test_query_client_options.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* eslint-disable no-console */ +export const dataUsageTestQueryClientOptions = { + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + }, + logger: { + log: console.log, + warn: console.warn, + error: () => {}, + }, +}; diff --git a/x-pack/plugins/data_usage/public/app/components/charts.tsx b/x-pack/plugins/data_usage/public/app/components/charts.tsx index 8d04324fb2246..56857e7a63ff9 100644 --- a/x-pack/plugins/data_usage/public/app/components/charts.tsx +++ b/x-pack/plugins/data_usage/public/app/components/charts.tsx @@ -9,18 +9,21 @@ import { EuiFlexGroup } from '@elastic/eui'; import { MetricTypes } from '../../../common/rest_types'; import { ChartPanel } from './chart_panel'; import { UsageMetricsResponseSchemaBody } from '../../../common/rest_types'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; interface ChartsProps { data: UsageMetricsResponseSchemaBody; + 'data-test-subj'?: string; } -export const Charts: React.FC = ({ data }) => { +export const Charts: React.FC = ({ data, 'data-test-subj': dataTestSubj }) => { + const getTestId = useTestIdGenerator(dataTestSubj); const [popoverOpen, setPopoverOpen] = useState(null); const togglePopover = useCallback((streamName: string | null) => { setPopoverOpen((prev) => (prev === streamName ? null : streamName)); }, []); return ( - + {Object.entries(data.metrics).map(([metricType, series], idx) => ( { + return { + useBreadcrumbs: jest.fn(), + }; +}); + +jest.mock('../../utils/use_kibana', () => { + return { + useKibanaContextForPlugin: () => ({ + services: mockServices, + }), + }; +}); + +jest.mock('../../hooks/use_get_usage_metrics', () => { + const original = jest.requireActual('../../hooks/use_get_usage_metrics'); + return { + ...original, + useGetDataUsageMetrics: jest.fn(original.useGetDataUsageMetrics), + }; +}); + +const mockUseLocation = jest.fn(() => ({ pathname: '/' })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => mockUseLocation(), + useHistory: jest.fn().mockReturnValue({ + push: jest.fn(), + listen: jest.fn(), + location: { + search: '', + }, + }), +})); + +jest.mock('../../hooks/use_get_data_streams', () => { + const original = jest.requireActual('../../hooks/use_get_data_streams'); + return { + ...original, + useGetDataUsageDataStreams: jest.fn(original.useGetDataUsageDataStreams), + }; +}); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...original, + useKibana: () => ({ + services: { + uiSettings: { + get: jest.fn().mockImplementation((key) => { + const get = (k: 'dateFormat' | 'timepicker:quickRanges') => { + const x = { + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', + 'timepicker:quickRanges': [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, + ], + }; + return x[k]; + }; + return get(key); + }), + }, + }, + }), + }; +}); +const mockUseGetDataUsageMetrics = useGetDataUsageMetrics as jest.Mock; +const mockUseGetDataUsageDataStreams = useGetDataUsageDataStreams as jest.Mock; +const mockServices = mockCore.createStart(); + +const getBaseMockedDataStreams = () => ({ + error: undefined, + data: undefined, + isFetching: false, + refetch: jest.fn(), +}); +const getBaseMockedDataUsageMetrics = () => ({ + error: undefined, + data: undefined, + isFetching: false, + refetch: jest.fn(), +}); + +describe('DataUsageMetrics', () => { + let user: UserEvent; + const testId = 'test'; + const testIdFilter = `${testId}-filter`; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime, pointerEventsCheck: 0 }); + mockUseGetDataUsageMetrics.mockReturnValue(getBaseMockedDataUsageMetrics); + mockUseGetDataUsageDataStreams.mockReturnValue(getBaseMockedDataStreams); + }); + + it('renders', () => { + const { getByTestId } = render(); + expect(getByTestId(`${testId}`)).toBeTruthy(); + }); + + it('should show date filter', () => { + const { getByTestId } = render(); + expect(getByTestId(`${testIdFilter}-date-range`)).toBeTruthy(); + expect(getByTestId(`${testIdFilter}-date-range`).textContent).toContain('Last 24 hours'); + expect(getByTestId(`${testIdFilter}-super-refresh-button`)).toBeTruthy(); + }); + + it('should not show data streams filter while fetching API', () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + ...getBaseMockedDataStreams, + isFetching: true, + }); + const { queryByTestId } = render(); + expect(queryByTestId(`${testIdFilter}-dataStreams-popoverButton`)).not.toBeTruthy(); + }); + + it('should show data streams filter', () => { + const { getByTestId } = render(); + expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toBeTruthy(); + }); + + it('should show selected data streams on the filter', () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + error: undefined, + data: [ + { + name: '.ds-1', + storageSizeBytes: 10000, + }, + { + name: '.ds-2', + storageSizeBytes: 20000, + }, + { + name: '.ds-3', + storageSizeBytes: 10300, + }, + { + name: '.ds-4', + storageSizeBytes: 23000, + }, + { + name: '.ds-5', + storageSizeBytes: 23200, + }, + ], + isFetching: false, + }); + const { getByTestId } = render(); + expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toHaveTextContent( + 'Data streams5' + ); + }); + + it('should allow de-selecting all but one data stream option', async () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + error: undefined, + data: [ + { + name: '.ds-1', + storageSizeBytes: 10000, + }, + { + name: '.ds-2', + storageSizeBytes: 20000, + }, + { + name: '.ds-3', + storageSizeBytes: 10300, + }, + { + name: '.ds-4', + storageSizeBytes: 23000, + }, + { + name: '.ds-5', + storageSizeBytes: 23200, + }, + ], + isFetching: false, + }); + const { getByTestId, getAllByTestId } = render(); + expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toHaveTextContent( + 'Data streams5' + ); + await user.click(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)); + const allFilterOptions = getAllByTestId('dataStreams-filter-option'); + for (let i = 0; i < allFilterOptions.length - 1; i++) { + await user.click(allFilterOptions[i]); + } + + expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toHaveTextContent( + 'Data streams1' + ); + }); + + it('should not call usage metrics API if no data streams', async () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + ...getBaseMockedDataStreams, + data: [], + }); + render(); + expect(mockUseGetDataUsageMetrics).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ enabled: false }) + ); + }); + + it('should show charts loading if data usage metrics API is fetching', () => { + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + isFetching: true, + }); + const { getByTestId } = render(); + expect(getByTestId(`${testId}-charts-loading`)).toBeTruthy(); + }); + + it('should show charts', () => { + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + isFetched: true, + data: { + metrics: { + ingest_rate: [ + { + name: '.ds-1', + data: [{ x: new Date(), y: 1000 }], + }, + { + name: '.ds-10', + data: [{ x: new Date(), y: 1100 }], + }, + ], + storage_retained: [ + { + name: '.ds-2', + data: [{ x: new Date(), y: 2000 }], + }, + { + name: '.ds-20', + data: [{ x: new Date(), y: 2100 }], + }, + ], + }, + }, + }); + const { getByTestId } = render(); + expect(getByTestId(`${testId}-charts`)).toBeTruthy(); + }); + + it('should refetch usage metrics with `Refresh` button click', async () => { + const refetch = jest.fn(); + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + data: ['.ds-1', '.ds-2'], + isFetched: true, + }); + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + isFetched: true, + refetch, + }); + const { getByTestId } = render(); + const refreshButton = getByTestId(`${testIdFilter}-super-refresh-button`); + // click refresh 5 times + for (let i = 0; i < 5; i++) { + await user.click(refreshButton); + } + + expect(mockUseGetDataUsageMetrics).toHaveBeenLastCalledWith( + expect.any(Object), + expect.objectContaining({ enabled: false }) + ); + expect(refetch).toHaveBeenCalledTimes(5); + }); + + it('should show error toast if usage metrics API fails', async () => { + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + isFetched: true, + error: new Error('Uh oh!'), + }); + render(); + await waitFor(() => { + expect(mockServices.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Error getting usage metrics', + text: 'Uh oh!', + }); + }); + }); + + it('should show error toast if data streams API fails', async () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + ...getBaseMockedDataStreams, + isFetched: true, + error: new Error('Uh oh!'), + }); + render(); + await waitFor(() => { + expect(mockServices.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Error getting data streams', + text: 'Uh oh!', + }); + }); + }); +}); diff --git a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx index 929ebf7a02490..59354a1746346 100644 --- a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx +++ b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -14,11 +14,12 @@ import { useBreadcrumbs } from '../../utils/use_breadcrumbs'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; import { PLUGIN_NAME } from '../../../common'; import { useGetDataUsageMetrics } from '../../hooks/use_get_usage_metrics'; +import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams'; import { useDataUsageMetricsUrlParams } from '../hooks/use_charts_url_params'; import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from '../hooks/use_date_picker'; import { DEFAULT_METRIC_TYPES, UsageMetricsRequestBody } from '../../../common/rest_types'; import { ChartFilters, ChartFiltersProps } from './filters/charts_filters'; -import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; const EuiItemCss = css` width: 100%; @@ -28,181 +29,188 @@ const FlexItemWithCss = ({ children }: { children: React.ReactNode }) => ( {children} ); -export const DataUsageMetrics = () => { - const { - services: { chrome, appParams, notifications }, - } = useKibanaContextForPlugin(); - useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome); - - const { - metricTypes: metricTypesFromUrl, - dataStreams: dataStreamsFromUrl, - startDate: startDateFromUrl, - endDate: endDateFromUrl, - setUrlMetricTypesFilter, - setUrlDataStreamsFilter, - setUrlDateRangeFilter, - } = useDataUsageMetricsUrlParams(); - - const { - error: errorFetchingDataStreams, - data: dataStreams, - isFetching: isFetchingDataStreams, - } = useGetDataUsageDataStreams({ - selectedDataStreams: dataStreamsFromUrl, - options: { - enabled: true, - retry: false, - }, - }); - - const [metricsFilters, setMetricsFilters] = useState({ - metricTypes: [...DEFAULT_METRIC_TYPES], - dataStreams: [], - from: DEFAULT_DATE_RANGE_OPTIONS.startDate, - to: DEFAULT_DATE_RANGE_OPTIONS.endDate, - }); - - useEffect(() => { - if (!metricTypesFromUrl) { - setUrlMetricTypesFilter(metricsFilters.metricTypes.join(',')); - } - if (!dataStreamsFromUrl && dataStreams) { - setUrlDataStreamsFilter(dataStreams.map((ds) => ds.name).join(',')); - } - if (!startDateFromUrl || !endDateFromUrl) { - setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to }); - } - }, [ - dataStreams, - dataStreamsFromUrl, - endDateFromUrl, - metricTypesFromUrl, - metricsFilters.dataStreams, - metricsFilters.from, - metricsFilters.metricTypes, - metricsFilters.to, - setUrlDataStreamsFilter, - setUrlDateRangeFilter, - setUrlMetricTypesFilter, - startDateFromUrl, - ]); - - useEffect(() => { - setMetricsFilters((prevState) => ({ - ...prevState, - metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes, - dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams, - })); - }, [metricTypesFromUrl, dataStreamsFromUrl]); - - const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker(); - - const { - error: errorFetchingDataUsageMetrics, - data, - isFetching, - isFetched, - refetch: refetchDataUsageMetrics, - } = useGetDataUsageMetrics( - { - ...metricsFilters, - from: dateRangePickerState.startDate, - to: dateRangePickerState.endDate, - }, - { - retry: false, - enabled: !!metricsFilters.dataStreams.length, - } - ); - - const onRefresh = useCallback(() => { - refetchDataUsageMetrics(); - }, [refetchDataUsageMetrics]); - - const onChangeDataStreamsFilter = useCallback( - (selectedDataStreams: string[]) => { - setMetricsFilters((prevState) => ({ ...prevState, dataStreams: selectedDataStreams })); - }, - [setMetricsFilters] - ); - - const onChangeMetricTypesFilter = useCallback( - (selectedMetricTypes: string[]) => { - setMetricsFilters((prevState) => ({ ...prevState, metricTypes: selectedMetricTypes })); - }, - [setMetricsFilters] - ); - - const filterOptions: ChartFiltersProps['filterOptions'] = useMemo(() => { - const dataStreamsOptions = dataStreams?.reduce>((acc, ds) => { - acc[ds.name] = ds.storageSizeBytes; - return acc; - }, {}); - - return { - dataStreams: { - filterName: 'dataStreams', - options: dataStreamsOptions ? Object.keys(dataStreamsOptions) : metricsFilters.dataStreams, - appendOptions: dataStreamsOptions, - selectedOptions: metricsFilters.dataStreams, - onChangeFilterOptions: onChangeDataStreamsFilter, - isFilterLoading: isFetchingDataStreams, - }, - metricTypes: { - filterName: 'metricTypes', - options: metricsFilters.metricTypes, - onChangeFilterOptions: onChangeMetricTypesFilter, +export const DataUsageMetrics = memo( + ({ 'data-test-subj': dataTestSubj = 'data-usage-metrics' }: { 'data-test-subj'?: string }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + + const { + services: { chrome, appParams, notifications }, + } = useKibanaContextForPlugin(); + useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome); + + const { + metricTypes: metricTypesFromUrl, + dataStreams: dataStreamsFromUrl, + startDate: startDateFromUrl, + endDate: endDateFromUrl, + setUrlMetricTypesFilter, + setUrlDataStreamsFilter, + setUrlDateRangeFilter, + } = useDataUsageMetricsUrlParams(); + + const { + error: errorFetchingDataStreams, + data: dataStreams, + isFetching: isFetchingDataStreams, + } = useGetDataUsageDataStreams({ + selectedDataStreams: dataStreamsFromUrl, + options: { + enabled: true, + retry: false, }, - }; - }, [ - dataStreams, - isFetchingDataStreams, - metricsFilters.dataStreams, - metricsFilters.metricTypes, - onChangeDataStreamsFilter, - onChangeMetricTypesFilter, - ]); - - if (errorFetchingDataUsageMetrics) { - notifications.toasts.addDanger({ - title: i18n.translate('xpack.dataUsage.getMetrics.addFailure.toast.title', { - defaultMessage: 'Error getting usage metrics', - }), - text: errorFetchingDataUsageMetrics.message, }); - } - if (errorFetchingDataStreams) { - notifications.toasts.addDanger({ - title: i18n.translate('xpack.dataUsage.getDataStreams.addFailure.toast.title', { - defaultMessage: 'Error getting data streams', - }), - text: errorFetchingDataStreams.message, + + const [metricsFilters, setMetricsFilters] = useState({ + metricTypes: [...DEFAULT_METRIC_TYPES], + dataStreams: [], + from: DEFAULT_DATE_RANGE_OPTIONS.startDate, + to: DEFAULT_DATE_RANGE_OPTIONS.endDate, }); - } - return ( - - - - - - - {isFetched && data?.metrics ? ( - - ) : isFetching ? ( - - ) : null} - - - ); -}; + useEffect(() => { + if (!metricTypesFromUrl) { + setUrlMetricTypesFilter(metricsFilters.metricTypes.join(',')); + } + if (!dataStreamsFromUrl && dataStreams) { + setUrlDataStreamsFilter(dataStreams.map((ds) => ds.name).join(',')); + } + if (!startDateFromUrl || !endDateFromUrl) { + setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to }); + } + }, [ + dataStreams, + dataStreamsFromUrl, + endDateFromUrl, + metricTypesFromUrl, + metricsFilters.dataStreams, + metricsFilters.from, + metricsFilters.metricTypes, + metricsFilters.to, + setUrlDataStreamsFilter, + setUrlDateRangeFilter, + setUrlMetricTypesFilter, + startDateFromUrl, + ]); + + useEffect(() => { + setMetricsFilters((prevState) => ({ + ...prevState, + metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes, + dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams, + })); + }, [metricTypesFromUrl, dataStreamsFromUrl]); + + const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker(); + + const { + error: errorFetchingDataUsageMetrics, + data, + isFetching, + isFetched, + refetch: refetchDataUsageMetrics, + } = useGetDataUsageMetrics( + { + ...metricsFilters, + from: dateRangePickerState.startDate, + to: dateRangePickerState.endDate, + }, + { + retry: false, + enabled: !!metricsFilters.dataStreams.length, + } + ); + + const onRefresh = useCallback(() => { + refetchDataUsageMetrics(); + }, [refetchDataUsageMetrics]); + + const onChangeDataStreamsFilter = useCallback( + (selectedDataStreams: string[]) => { + setMetricsFilters((prevState) => ({ ...prevState, dataStreams: selectedDataStreams })); + }, + [setMetricsFilters] + ); + + const onChangeMetricTypesFilter = useCallback( + (selectedMetricTypes: string[]) => { + setMetricsFilters((prevState) => ({ ...prevState, metricTypes: selectedMetricTypes })); + }, + [setMetricsFilters] + ); + + const filterOptions: ChartFiltersProps['filterOptions'] = useMemo(() => { + const dataStreamsOptions = dataStreams?.reduce>((acc, ds) => { + acc[ds.name] = ds.storageSizeBytes; + return acc; + }, {}); + + return { + dataStreams: { + filterName: 'dataStreams', + options: dataStreamsOptions + ? Object.keys(dataStreamsOptions) + : metricsFilters.dataStreams, + appendOptions: dataStreamsOptions, + selectedOptions: metricsFilters.dataStreams, + onChangeFilterOptions: onChangeDataStreamsFilter, + isFilterLoading: isFetchingDataStreams, + }, + metricTypes: { + filterName: 'metricTypes', + options: metricsFilters.metricTypes, + onChangeFilterOptions: onChangeMetricTypesFilter, + }, + }; + }, [ + dataStreams, + isFetchingDataStreams, + metricsFilters.dataStreams, + metricsFilters.metricTypes, + onChangeDataStreamsFilter, + onChangeMetricTypesFilter, + ]); + + if (errorFetchingDataUsageMetrics) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.dataUsage.getMetrics.addFailure.toast.title', { + defaultMessage: 'Error getting usage metrics', + }), + text: errorFetchingDataUsageMetrics.message, + }); + } + if (errorFetchingDataStreams) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.dataUsage.getDataStreams.addFailure.toast.title', { + defaultMessage: 'Error getting data streams', + }), + text: errorFetchingDataStreams.message, + }); + } + + return ( + + + + + + + {isFetched && data?.metrics ? ( + + ) : isFetching ? ( + + ) : null} + + + ); + } +); diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx index 83d417565f012..6b4806537e74b 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx @@ -193,13 +193,10 @@ export const ChartsFilter = memo( > {(list, search) => { return ( -
+
{isSearchable && ( {search} diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx index 2ed96f012c497..3c0237c84a0c9 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx @@ -42,7 +42,7 @@ export const ChartsFilterPopover = memo( const button = useMemo( () => ( ( const filters = useMemo(() => { return ( <> - {showMetricsTypesFilter && } + {showMetricsTypesFilter && ( + + )} {!filterOptions.dataStreams.isFilterLoading && ( - + )} ); - }, [filterOptions, showMetricsTypesFilter]); + }, [dataTestSubj, filterOptions, showMetricsTypesFilter]); const onClickRefreshButton = useCallback(() => onClick(), [onClick]); @@ -68,6 +70,7 @@ export const ChartFilters = memo( onRefresh={onRefresh} onRefreshChange={onRefreshChange} onTimeChange={onTimeChange} + data-test-subj={dataTestSubj} /> diff --git a/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx b/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx index 4d9b280d763ce..044a036eea61f 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx @@ -15,6 +15,7 @@ import type { OnRefreshChangeProps, } from '@elastic/eui/src/components/date_picker/types'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; export interface DateRangePickerValues { autoRefreshOptions: { @@ -32,10 +33,19 @@ interface UsageMetricsDateRangePickerProps { onRefresh: () => void; onRefreshChange: (evt: OnRefreshChangeProps) => void; onTimeChange: ({ start, end }: DurationRange) => void; + 'data-test-subj'?: string; } export const UsageMetricsDateRangePicker = memo( - ({ dateRangePickerState, isDataLoading, onRefresh, onRefreshChange, onTimeChange }) => { + ({ + dateRangePickerState, + isDataLoading, + onRefresh, + onRefreshChange, + onTimeChange, + 'data-test-subj': dataTestSubj, + }) => { + const getTestId = useTestIdGenerator(dataTestSubj); const kibana = useKibana(); const { uiSettings } = kibana.services; const [commonlyUsedRanges] = useState(() => { @@ -54,6 +64,7 @@ export const UsageMetricsDateRangePicker = memo { + const getMetricTypesAsArray = (): MetricTypes[] => { + return [...METRIC_TYPE_VALUES]; + }; + + it('should not use invalid `metricTypes` values from URL params', () => { + expect(getDataUsageMetricsFiltersFromUrlParams({ metricTypes: 'bar,foo' })).toEqual({}); + }); + + it('should use valid `metricTypes` values from URL params', () => { + expect( + getDataUsageMetricsFiltersFromUrlParams({ + metricTypes: `${getMetricTypesAsArray().join()},foo,bar`, + }) + ).toEqual({ + metricTypes: getMetricTypesAsArray().sort(), + }); + }); + + it('should use given `dataStreams` values from URL params', () => { + expect( + getDataUsageMetricsFiltersFromUrlParams({ + dataStreams: 'ds-3,ds-1,ds-2', + }) + ).toEqual({ + dataStreams: ['ds-3', 'ds-1', 'ds-2'], + }); + }); + + it('should use valid `metricTypes` along with given `dataStreams` and date values from URL params', () => { + expect( + getDataUsageMetricsFiltersFromUrlParams({ + metricTypes: getMetricTypesAsArray().join(), + dataStreams: 'ds-5,ds-1,ds-2', + startDate: '2022-09-12T08:00:00.000Z', + endDate: '2022-09-12T08:30:33.140Z', + }) + ).toEqual({ + metricTypes: getMetricTypesAsArray().sort(), + endDate: '2022-09-12T08:30:33.140Z', + dataStreams: ['ds-5', 'ds-1', 'ds-2'], + startDate: '2022-09-12T08:00:00.000Z', + }); + }); + + it('should use given relative startDate and endDate values URL params', () => { + expect( + getDataUsageMetricsFiltersFromUrlParams({ + startDate: 'now-24h/h', + endDate: 'now', + }) + ).toEqual({ + endDate: 'now', + startDate: 'now-24h/h', + }); + }); + + it('should use given absolute startDate and endDate values URL params', () => { + expect( + getDataUsageMetricsFiltersFromUrlParams({ + startDate: '2022-09-12T08:00:00.000Z', + endDate: '2022-09-12T08:30:33.140Z', + }) + ).toEqual({ + endDate: '2022-09-12T08:30:33.140Z', + startDate: '2022-09-12T08:00:00.000Z', + }); + }); +}); diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx new file mode 100644 index 0000000000000..04cee589a523d --- /dev/null +++ b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactNode } from 'react'; +import { QueryClient, QueryClientProvider, useQuery as _useQuery } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; +import { useGetDataUsageDataStreams } from './use_get_data_streams'; +import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../common'; +import { coreMock as mockCore } from '@kbn/core/public/mocks'; +import { dataUsageTestQueryClientOptions } from '../../common/test_utils/test_query_client_options'; + +const useQueryMock = _useQuery as jest.Mock; + +jest.mock('@tanstack/react-query', () => { + const actualReactQueryModule = jest.requireActual('@tanstack/react-query'); + + return { + ...actualReactQueryModule, + useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)), + }; +}); + +const mockServices = mockCore.createStart(); +const createWrapper = () => { + const queryClient = new QueryClient(dataUsageTestQueryClientOptions); + return ({ children }: { children: ReactNode }) => ( + {children} + ); +}; + +jest.mock('../utils/use_kibana', () => { + return { + useKibanaContextForPlugin: () => ({ + services: mockServices, + }), + }; +}); + +const defaultDataStreamsRequestParams = { + options: { enabled: true }, +}; + +describe('useGetDataUsageDataStreams', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call the correct API', async () => { + await renderHook(() => useGetDataUsageDataStreams(defaultDataStreamsRequestParams), { + wrapper: createWrapper(), + }); + + expect(mockServices.http.get).toHaveBeenCalledWith(DATA_USAGE_DATA_STREAMS_API_ROUTE, { + signal: expect.any(AbortSignal), + version: '1', + }); + }); + + it('should not send selected data stream names provided in the param when calling the API', async () => { + await renderHook( + () => + useGetDataUsageDataStreams({ + ...defaultDataStreamsRequestParams, + selectedDataStreams: ['ds-1'], + }), + { + wrapper: createWrapper(), + } + ); + + expect(mockServices.http.get).toHaveBeenCalledWith(DATA_USAGE_DATA_STREAMS_API_ROUTE, { + signal: expect.any(AbortSignal), + version: '1', + }); + }); + + it('should not call the API if disabled', async () => { + await renderHook( + () => + useGetDataUsageDataStreams({ + ...defaultDataStreamsRequestParams, + options: { enabled: false }, + }), + { + wrapper: createWrapper(), + } + ); + + expect(mockServices.http.get).not.toHaveBeenCalled(); + }); + + it('should allow custom options to be used', async () => { + await renderHook( + () => + useGetDataUsageDataStreams({ + selectedDataStreams: undefined, + options: { + queryKey: ['test-query-key'], + enabled: true, + retry: false, + }, + }), + { + wrapper: createWrapper(), + } + ); + + expect(useQueryMock).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ['test-query-key'], + enabled: true, + retry: false, + }) + ); + }); +}); diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts index 598acca3c1faf..acb41e45f4eb6 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts +++ b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts @@ -31,15 +31,16 @@ export const useGetDataUsageDataStreams = ({ selectedDataStreams?: string[]; options?: UseQueryOptions; }): UseQueryResult => { - const http = useKibanaContextForPlugin().services.http; + const { http } = useKibanaContextForPlugin().services; return useQuery({ queryKey: ['get-data-usage-data-streams'], ...options, keepPreviousData: true, - queryFn: async () => { + queryFn: async ({ signal }) => { const dataStreamsResponse = await http .get(DATA_USAGE_DATA_STREAMS_API_ROUTE, { + signal, version: '1', }) .catch((error) => { diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx new file mode 100644 index 0000000000000..efc3d2a9f4640 --- /dev/null +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactNode } from 'react'; +import { QueryClient, QueryClientProvider, useQuery as _useQuery } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; +import { useGetDataUsageMetrics } from './use_get_usage_metrics'; +import { DATA_USAGE_METRICS_API_ROUTE } from '../../common'; +import { coreMock as mockCore } from '@kbn/core/public/mocks'; +import { dataUsageTestQueryClientOptions } from '../../common/test_utils/test_query_client_options'; + +const useQueryMock = _useQuery as jest.Mock; + +jest.mock('@tanstack/react-query', () => { + const actualReactQueryModule = jest.requireActual('@tanstack/react-query'); + + return { + ...actualReactQueryModule, + useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)), + }; +}); + +const mockServices = mockCore.createStart(); +const createWrapper = () => { + const queryClient = new QueryClient(dataUsageTestQueryClientOptions); + return ({ children }: { children: ReactNode }) => ( + {children} + ); +}; + +jest.mock('../utils/use_kibana', () => { + return { + useKibanaContextForPlugin: () => ({ + services: mockServices, + }), + }; +}); + +const defaultUsageMetricsRequestBody = { + from: 'now-15m', + to: 'now', + metricTypes: ['ingest_rate'], + dataStreams: ['ds-1'], +}; + +describe('useGetDataUsageMetrics', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call the correct API', async () => { + await renderHook( + () => useGetDataUsageMetrics(defaultUsageMetricsRequestBody, { enabled: true }), + { + wrapper: createWrapper(), + } + ); + + expect(mockServices.http.post).toHaveBeenCalledWith(DATA_USAGE_METRICS_API_ROUTE, { + signal: expect.any(AbortSignal), + version: '1', + body: JSON.stringify(defaultUsageMetricsRequestBody), + }); + }); + + it('should not call the API if disabled', async () => { + await renderHook( + () => useGetDataUsageMetrics(defaultUsageMetricsRequestBody, { enabled: false }), + { + wrapper: createWrapper(), + } + ); + + expect(mockServices.http.post).not.toHaveBeenCalled(); + }); + + it('should allow custom options to be used', async () => { + await renderHook( + () => + useGetDataUsageMetrics(defaultUsageMetricsRequestBody, { + queryKey: ['test-query-key'], + enabled: true, + retry: false, + }), + { + wrapper: createWrapper(), + } + ); + + expect(useQueryMock).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ['test-query-key'], + enabled: true, + retry: false, + }) + ); + }); +}); diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts index 7e7406d72b9c0..6b2ef5316b0f6 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts @@ -21,7 +21,7 @@ export const useGetDataUsageMetrics = ( body: UsageMetricsRequestBody, options: UseQueryOptions> = {} ): UseQueryResult> => { - const http = useKibanaContextForPlugin().services.http; + const { http } = useKibanaContextForPlugin().services; return useQuery>({ queryKey: ['get-data-usage-metrics', body], diff --git a/x-pack/plugins/data_usage/public/utils/format_bytes.test.ts b/x-pack/plugins/data_usage/public/utils/format_bytes.test.ts new file mode 100644 index 0000000000000..ccc7a4c2f0aa2 --- /dev/null +++ b/x-pack/plugins/data_usage/public/utils/format_bytes.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { formatBytes } from './format_bytes'; + +const exponentN = (number: number, exponent: number) => number ** exponent; + +describe('formatBytes', () => { + it('should format bytes to human readable format with decimal', () => { + expect(formatBytes(84 + 5)).toBe('89.0 B'); + expect(formatBytes(1024 + 256)).toBe('1.3 KB'); + expect(formatBytes(1024 + 582)).toBe('1.6 KB'); + expect(formatBytes(exponentN(1024, 2) + 582 * 1024)).toBe('1.6 MB'); + expect(formatBytes(exponentN(1024, 3) + 582 * exponentN(1024, 2))).toBe('1.6 GB'); + expect(formatBytes(exponentN(1024, 4) + 582 * exponentN(1024, 3))).toBe('1.6 TB'); + expect(formatBytes(exponentN(1024, 5) + 582 * exponentN(1024, 4))).toBe('1.6 PB'); + expect(formatBytes(exponentN(1024, 6) + 582 * exponentN(1024, 5))).toBe('1.6 EB'); + expect(formatBytes(exponentN(1024, 7) + 582 * exponentN(1024, 6))).toBe('1.6 ZB'); + expect(formatBytes(exponentN(1024, 8) + 582 * exponentN(1024, 7))).toBe('1.6 YB'); + }); +}); diff --git a/x-pack/plugins/data_usage/server/mocks/index.ts b/x-pack/plugins/data_usage/server/mocks/index.ts new file mode 100644 index 0000000000000..54260f7309fc6 --- /dev/null +++ b/x-pack/plugins/data_usage/server/mocks/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { DeepReadonly } from 'utility-types'; +import { PluginInitializerContext } from '@kbn/core/server'; +import { Observable } from 'rxjs'; +import { DataUsageContext } from '../types'; +import { DataUsageConfigType } from '../config'; + +export interface MockedDataUsageContext extends DataUsageContext { + logFactory: ReturnType['get']>; + config$?: Observable; + configInitialValue: DataUsageConfigType; + serverConfig: DeepReadonly; + kibanaInstanceId: PluginInitializerContext['env']['instanceUuid']; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + kibanaBranch: PluginInitializerContext['env']['packageInfo']['branch']; +} + +export const createMockedDataUsageContext = ( + context: PluginInitializerContext +): MockedDataUsageContext => { + return { + logFactory: loggingSystemMock.create().get(), + config$: context.config.create(), + configInitialValue: context.config.get(), + serverConfig: context.config.get(), + kibanaInstanceId: context.env.instanceUuid, + kibanaVersion: context.env.packageInfo.version, + kibanaBranch: context.env.packageInfo.branch, + }; +}; diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts new file mode 100644 index 0000000000000..7282dbc969fc7 --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MockedKeys } from '@kbn/utility-types-jest'; +import type { CoreSetup } from '@kbn/core/server'; +import { registerDataStreamsRoute } from './data_streams'; +import { coreMock } from '@kbn/core/server/mocks'; +import { httpServerMock } from '@kbn/core/server/mocks'; +import { DataUsageService } from '../../services'; +import type { + DataUsageRequestHandlerContext, + DataUsageRouter, + DataUsageServerStart, +} from '../../types'; +import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../../common'; +import { createMockedDataUsageContext } from '../../mocks'; +import { getMeteringStats } from '../../utils/get_metering_stats'; +import { CustomHttpRequestError } from '../../utils'; + +jest.mock('../../utils/get_metering_stats'); +const mockGetMeteringStats = getMeteringStats as jest.Mock; + +describe('registerDataStreamsRoute', () => { + let mockCore: MockedKeys>; + let router: DataUsageRouter; + let dataUsageService: DataUsageService; + let context: DataUsageRequestHandlerContext; + + beforeEach(() => { + mockCore = coreMock.createSetup(); + router = mockCore.http.createRouter(); + context = coreMock.createCustomRequestHandlerContext( + coreMock.createRequestHandlerContext() + ) as unknown as DataUsageRequestHandlerContext; + + const mockedDataUsageContext = createMockedDataUsageContext( + coreMock.createPluginInitializerContext() + ); + dataUsageService = new DataUsageService(mockedDataUsageContext); + registerDataStreamsRoute(router, dataUsageService); + }); + + it('should request correct API', () => { + expect(router.versioned.get).toHaveBeenCalledTimes(1); + expect(router.versioned.get).toHaveBeenCalledWith({ + access: 'internal', + path: DATA_USAGE_DATA_STREAMS_API_ROUTE, + }); + }); + + it('should correctly sort response', async () => { + mockGetMeteringStats.mockResolvedValue({ + datastreams: [ + { + name: 'datastream1', + size_in_bytes: 100, + }, + { + name: 'datastream2', + size_in_bytes: 200, + }, + ], + }); + const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.get.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ + body: [ + { + name: 'datastream2', + storageSizeBytes: 200, + }, + { + name: 'datastream1', + storageSizeBytes: 100, + }, + ], + }); + }); + + it('should return correct error if metering stats request fails', async () => { + // using custom error for test here to avoid having to import the actual error class + mockGetMeteringStats.mockRejectedValue( + new CustomHttpRequestError('Error getting metring stats!') + ); + const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.get.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.customError).toHaveBeenCalledTimes(1); + expect(mockResponse.customError).toHaveBeenCalledWith({ + body: new CustomHttpRequestError('Error getting metring stats!'), + statusCode: 500, + }); + }); + + it.each([ + ['no datastreams', {}, []], + ['empty array', { datastreams: [] }, []], + ['an empty element', { datastreams: [{}] }, [{ name: undefined, storageSizeBytes: 0 }]], + ])('should return empty array when no stats data with %s', async (_, stats, res) => { + mockGetMeteringStats.mockResolvedValue(stats); + const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.get.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ + body: res, + }); + }); +}); diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts index bc8c5e898c35e..66c2cc0df3513 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts @@ -5,27 +5,11 @@ * 2.0. */ -import { type ElasticsearchClient, RequestHandler } from '@kbn/core/server'; +import { RequestHandler } from '@kbn/core/server'; import { DataUsageRequestHandlerContext } from '../../types'; import { errorHandler } from '../error_handler'; import { DataUsageService } from '../../services'; - -export interface MeteringStats { - name: string; - num_docs: number; - size_in_bytes: number; -} - -interface MeteringStatsResponse { - datastreams: MeteringStats[]; -} - -const getMeteringStats = (client: ElasticsearchClient) => { - return client.transport.request({ - method: 'GET', - path: '/_metering/stats', - }); -}; +import { getMeteringStats } from '../../utils/get_metering_stats'; export const getDataStreamsHandler = ( dataUsageService: DataUsageService @@ -41,12 +25,15 @@ export const getDataStreamsHandler = ( core.elasticsearch.client.asSecondaryAuthUser ); - const body = meteringStats - .sort((a, b) => b.size_in_bytes - a.size_in_bytes) - .map((stat) => ({ - name: stat.name, - storageSizeBytes: stat.size_in_bytes ?? 0, - })); + const body = + meteringStats && !!meteringStats.length + ? meteringStats + .sort((a, b) => b.size_in_bytes - a.size_in_bytes) + .map((stat) => ({ + name: stat.name, + storageSizeBytes: stat.size_in_bytes ?? 0, + })) + : []; return response.ok({ body, diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts new file mode 100644 index 0000000000000..e95ffd11807a9 --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MockedKeys } from '@kbn/utility-types-jest'; +import type { CoreSetup } from '@kbn/core/server'; +import { registerUsageMetricsRoute } from './usage_metrics'; +import { coreMock } from '@kbn/core/server/mocks'; +import { httpServerMock } from '@kbn/core/server/mocks'; +import { DataUsageService } from '../../services'; +import type { + DataUsageRequestHandlerContext, + DataUsageRouter, + DataUsageServerStart, +} from '../../types'; +import { DATA_USAGE_METRICS_API_ROUTE } from '../../../common'; +import { createMockedDataUsageContext } from '../../mocks'; +import { CustomHttpRequestError } from '../../utils'; +import { AutoOpsError } from '../../services/errors'; + +describe('registerUsageMetricsRoute', () => { + let mockCore: MockedKeys>; + let router: DataUsageRouter; + let dataUsageService: DataUsageService; + let context: DataUsageRequestHandlerContext; + + beforeEach(() => { + mockCore = coreMock.createSetup(); + router = mockCore.http.createRouter(); + context = coreMock.createCustomRequestHandlerContext( + coreMock.createRequestHandlerContext() + ) as unknown as DataUsageRequestHandlerContext; + + const mockedDataUsageContext = createMockedDataUsageContext( + coreMock.createPluginInitializerContext() + ); + dataUsageService = new DataUsageService(mockedDataUsageContext); + }); + + it('should request correct API', () => { + registerUsageMetricsRoute(router, dataUsageService); + + expect(router.versioned.post).toHaveBeenCalledTimes(1); + expect(router.versioned.post).toHaveBeenCalledWith({ + access: 'internal', + path: DATA_USAGE_METRICS_API_ROUTE, + }); + }); + + it('should throw error if no data streams in the request', async () => { + registerUsageMetricsRoute(router, dataUsageService); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { + from: 'now-15m', + to: 'now', + metricTypes: ['ingest_rate'], + dataStreams: [], + }, + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.post.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.customError).toHaveBeenCalledTimes(1); + expect(mockResponse.customError).toHaveBeenCalledWith({ + body: new CustomHttpRequestError('[request body.dataStreams]: no data streams selected'), + statusCode: 400, + }); + }); + + it('should correctly transform response', async () => { + (await context.core).elasticsearch.client.asCurrentUser.indices.getDataStream = jest + .fn() + .mockResolvedValue({ + data_streams: [{ name: '.ds-1' }, { name: '.ds-2' }], + }); + + dataUsageService.getMetrics = jest.fn().mockResolvedValue({ + metrics: { + ingest_rate: [ + { + name: '.ds-1', + data: [ + [1726858530000, 13756849], + [1726862130000, 14657904], + ], + }, + { + name: '.ds-2', + data: [ + [1726858530000, 12894623], + [1726862130000, 14436905], + ], + }, + ], + storage_retained: [ + { + name: '.ds-1', + data: [ + [1726858530000, 12576413], + [1726862130000, 13956423], + ], + }, + { + name: '.ds-2', + data: [ + [1726858530000, 12894623], + [1726862130000, 14436905], + ], + }, + ], + }, + }); + + registerUsageMetricsRoute(router, dataUsageService); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { + from: 'now-15m', + to: 'now', + metricTypes: ['ingest_rate', 'storage_retained'], + dataStreams: ['.ds-1', '.ds-2'], + }, + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.post.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ + body: { + metrics: { + ingest_rate: [ + { + name: '.ds-1', + data: [ + { x: 1726858530000, y: 13756849 }, + { x: 1726862130000, y: 14657904 }, + ], + }, + { + name: '.ds-2', + data: [ + { x: 1726858530000, y: 12894623 }, + { x: 1726862130000, y: 14436905 }, + ], + }, + ], + storage_retained: [ + { + name: '.ds-1', + data: [ + { x: 1726858530000, y: 12576413 }, + { x: 1726862130000, y: 13956423 }, + ], + }, + { + name: '.ds-2', + data: [ + { x: 1726858530000, y: 12894623 }, + { x: 1726862130000, y: 14436905 }, + ], + }, + ], + }, + }, + }); + }); + + it('should throw error if error on requesting auto ops service', async () => { + (await context.core).elasticsearch.client.asCurrentUser.indices.getDataStream = jest + .fn() + .mockResolvedValue({ + data_streams: [{ name: '.ds-1' }, { name: '.ds-2' }], + }); + + dataUsageService.getMetrics = jest + .fn() + .mockRejectedValue(new AutoOpsError('Uh oh, something went wrong!')); + + registerUsageMetricsRoute(router, dataUsageService); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { + from: 'now-15m', + to: 'now', + metricTypes: ['ingest_rate'], + dataStreams: ['.ds-1', '.ds-2'], + }, + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.post.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.customError).toHaveBeenCalledTimes(1); + expect(mockResponse.customError).toHaveBeenCalledWith({ + body: new AutoOpsError('Uh oh, something went wrong!'), + statusCode: 503, + }); + }); +}); diff --git a/x-pack/plugins/data_usage/server/utils/get_metering_stats.ts b/x-pack/plugins/data_usage/server/utils/get_metering_stats.ts new file mode 100644 index 0000000000000..4ba30f5bd3601 --- /dev/null +++ b/x-pack/plugins/data_usage/server/utils/get_metering_stats.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { type ElasticsearchClient } from '@kbn/core/server'; + +export interface MeteringStats { + name: string; + num_docs: number; + size_in_bytes: number; +} + +interface MeteringStatsResponse { + datastreams: MeteringStats[]; +} + +export const getMeteringStats = (client: ElasticsearchClient) => { + return client.transport.request({ + method: 'GET', + path: '/_metering/stats', + }); +}; diff --git a/x-pack/plugins/data_usage/tsconfig.json b/x-pack/plugins/data_usage/tsconfig.json index 78c501922f239..66c8a5247858b 100644 --- a/x-pack/plugins/data_usage/tsconfig.json +++ b/x-pack/plugins/data_usage/tsconfig.json @@ -31,6 +31,7 @@ "@kbn/repo-info", "@kbn/cloud-plugin", "@kbn/server-http-tools", + "@kbn/utility-types-jest", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/integration_settings/integration_policy.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/integration_settings/integration_policy.cy.ts index da9a08339a45c..753e6476be1ed 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/integration_settings/integration_policy.cy.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/integration_settings/integration_policy.cy.ts @@ -28,7 +28,7 @@ const policyFormFields = [ }, ]; -describe('when navigating to integration page', () => { +describe.skip('when navigating to integration page', () => { beforeEach(() => { const integrationsPath = '/app/integrations/browse'; diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts index 0fc1b609b14ba..730e9c443854e 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts @@ -16,7 +16,7 @@ const timeRange = { rangeTo: end, }; // flaky -describe('Transaction details', () => { +describe.skip('Transaction details', () => { before(() => { synthtrace.index( opbeans({ diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/tutorial/tutorial.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/tutorial/tutorial.cy.ts deleted file mode 100644 index 9aa71604e6a80..0000000000000 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/tutorial/tutorial.cy.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -describe('APM tutorial', () => { - beforeEach(() => { - cy.loginAsViewerUser(); - cy.visitKibana('/app/home#/tutorial/apm'); - }); - - it('includes section for APM Server', () => { - cy.contains('APM Server'); - cy.contains('Linux DEB'); - cy.contains('Linux RPM'); - cy.contains('Other Linux'); - cy.contains('macOS'); - cy.contains('Windows'); - cy.contains('Fleet'); - }); - - it('includes section for APM Agents', () => { - cy.contains('APM agents'); - cy.contains('Java'); - cy.contains('RUM'); - cy.contains('Node.js'); - cy.contains('Django'); - cy.contains('Flask'); - cy.contains('Ruby on Rails'); - cy.contains('Rack'); - cy.contains('Go'); - cy.contains('.NET'); - cy.contains('PHP'); - }); -}); diff --git a/x-pack/plugins/observability_solution/synthetics/common/saved_objects/private_locations.ts b/x-pack/plugins/observability_solution/synthetics/common/saved_objects/private_locations.ts index bb3639e816059..1b5bb92dd7d88 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/saved_objects/private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/saved_objects/private_locations.ts @@ -5,5 +5,7 @@ * 2.0. */ -export const privateLocationsSavedObjectId = 'synthetics-privates-locations-singleton'; -export const privateLocationsSavedObjectName = 'synthetics-privates-locations'; +export const legacyPrivateLocationsSavedObjectId = 'synthetics-privates-locations-singleton'; +export const legacyPrivateLocationsSavedObjectName = 'synthetics-privates-locations'; + +export const privateLocationSavedObjectName = 'synthetics-private-location'; diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/private_locations.journey.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/private_locations.journey.ts index 9e6bb8352c35f..cdc5961991579 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/private_locations.journey.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/private_locations.journey.ts @@ -7,17 +7,14 @@ import { journey, step, before, after, expect } from '@elastic/synthetics'; import { waitForLoadingToFinish } from '@kbn/ux-plugin/e2e/journeys/utils'; +import { SyntheticsServices } from './services/synthetics_services'; import { byTestId } from '../../helpers/utils'; -import { - addTestMonitor, - cleanPrivateLocations, - cleanTestMonitors, - getPrivateLocations, -} from './services/add_monitor'; +import { addTestMonitor, cleanPrivateLocations, cleanTestMonitors } from './services/add_monitor'; import { syntheticsAppPageProvider } from '../page_objects/synthetics_app'; journey(`PrivateLocationsSettings`, async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl, params }); + const services = new SyntheticsServices(params); page.setDefaultTimeout(2 * 30000); @@ -78,16 +75,14 @@ journey(`PrivateLocationsSettings`, async ({ page, params }) => { await page.click('text=Private Locations'); await page.click('h1:has-text("Settings")'); - const privateLocations = await getPrivateLocations(params); + const privateLocations = await services.getPrivateLocations(); - const locations = privateLocations.attributes.locations; + expect(privateLocations.length).toBe(1); - expect(locations.length).toBe(1); - - locationId = locations[0].id; + locationId = privateLocations[0].id; await addTestMonitor(params.kibanaUrl, 'test-monitor', { - locations: [locations[0]], + locations: [privateLocations[0]], type: 'browser', }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor.ts index 6384179a71bb9..6a527da275eb3 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor.ts @@ -7,10 +7,7 @@ import axios from 'axios'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; -import { - privateLocationsSavedObjectId, - privateLocationsSavedObjectName, -} from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; +import { legacyPrivateLocationsSavedObjectName } from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; export const enableMonitorManagedViaApi = async (kibanaUrl: string) => { try { @@ -46,21 +43,6 @@ export const addTestMonitor = async ( } }; -export const getPrivateLocations = async (params: Record) => { - const getService = params.getService; - const server = getService('kibanaServer'); - - try { - return await server.savedObjects.get({ - id: privateLocationsSavedObjectId, - type: privateLocationsSavedObjectName, - }); - } catch (e) { - // eslint-disable-next-line no-console - console.log(e); - } -}; - export const cleanTestMonitors = async (params: Record) => { const getService = params.getService; const server = getService('kibanaServer'); @@ -79,7 +61,11 @@ export const cleanPrivateLocations = async (params: Record) => { try { await server.savedObjects.clean({ - types: [privateLocationsSavedObjectName, 'ingest-agent-policies', 'ingest-package-policies'], + types: [ + legacyPrivateLocationsSavedObjectName, + 'ingest-agent-policies', + 'ingest-package-policies', + ], }); } catch (e) { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts index 5c356492f1c24..507efe52c453f 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts @@ -10,7 +10,10 @@ import type { Client } from '@elastic/elasticsearch'; import { KbnClient } from '@kbn/test'; import pMap from 'p-map'; import { makeDownSummary, makeUpSummary } from '@kbn/observability-synthetics-test-data'; -import { SyntheticsMonitor } from '@kbn/synthetics-plugin/common/runtime_types'; +import { + SyntheticsMonitor, + SyntheticsPrivateLocations, +} from '@kbn/synthetics-plugin/common/runtime_types'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import { journeyStart, journeySummary, step1, step2 } from './data/browser_docs'; @@ -251,4 +254,12 @@ export class SyntheticsServices { }); return connector.data; } + + async getPrivateLocations(): Promise { + const response = await this.requester.request({ + path: SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, + method: 'GET', + }); + return response.data as SyntheticsPrivateLocations; + } } diff --git a/x-pack/plugins/observability_solution/synthetics/server/feature.ts b/x-pack/plugins/observability_solution/synthetics/server/feature.ts index c8b4b721a9ce1..bf86ac7b0c890 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/feature.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/feature.ts @@ -14,7 +14,10 @@ import { import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects'; import { SYNTHETICS_RULE_TYPES } from '../common/constants/synthetics_alerts'; -import { privateLocationsSavedObjectName } from '../common/saved_objects/private_locations'; +import { + legacyPrivateLocationsSavedObjectName, + privateLocationSavedObjectName, +} from '../common/saved_objects/private_locations'; import { PLUGIN } from '../common/constants/plugin'; import { syntheticsSettingsObjectType, @@ -71,7 +74,8 @@ export const syntheticsFeature = { syntheticsSettingsObjectType, syntheticsMonitorType, syntheticsApiKeyObjectType, - privateLocationsSavedObjectName, + privateLocationSavedObjectName, + legacyPrivateLocationsSavedObjectName, syntheticsParamType, // uptime settings object is also registered here since feature is shared between synthetics and uptime uptimeSettingsObjectType, @@ -102,7 +106,7 @@ export const syntheticsFeature = { syntheticsSettingsObjectType, syntheticsMonitorType, syntheticsApiKeyObjectType, - privateLocationsSavedObjectName, + legacyPrivateLocationsSavedObjectName, // uptime settings object is also registered here since feature is shared between synthetics and uptime uptimeSettingsObjectType, ], diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts index 637c0fc5c6193..cb50708c04eca 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts @@ -33,6 +33,16 @@ describe('syncEditedMonitor', () => { bulkUpdate: jest.fn(), get: jest.fn(), update: jest.fn(), + createPointInTimeFinder: jest.fn().mockImplementation(({ perPage, type: soType }) => ({ + close: jest.fn(async () => {}), + find: jest.fn().mockReturnValue({ + async *[Symbol.asyncIterator]() { + yield { + saved_objects: [], + }; + }, + }), + })), }, logger, config: { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts index ac6eff7dea90d..1feb120b2ea14 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts @@ -6,14 +6,12 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { SyntheticsRestApiRouteFactory } from '../../types'; import { getPrivateLocationsAndAgentPolicies } from './get_private_locations'; -import { - privateLocationsSavedObjectId, - privateLocationsSavedObjectName, -} from '../../../../common/saved_objects/private_locations'; +import { privateLocationSavedObjectName } from '../../../../common/saved_objects/private_locations'; import { SYNTHETICS_API_URLS } from '../../../../common/constants'; -import type { SyntheticsPrivateLocationsAttributes } from '../../../runtime_types/private_locations'; +import { PrivateLocationAttributes } from '../../../runtime_types/private_locations'; import { toClientContract, toSavedObjectContract } from './helpers'; import { PrivateLocation } from '../../../../common/runtime_types'; @@ -40,7 +38,11 @@ export const addPrivateLocationRoute: SyntheticsRestApiRouteFactory { + handler: async (routeContext) => { + await migrateLegacyPrivateLocations(routeContext); + + const { response, request, savedObjectsClient, syntheticsMonitorClient } = routeContext; + const location = request.body as PrivateLocationObject; const { locations, agentPolicies } = await getPrivateLocationsAndAgentPolicies( @@ -65,7 +67,6 @@ export const addPrivateLocationRoute: SyntheticsRestApiRouteFactory loc.id !== location.agentPolicyId); const formattedLocation = toSavedObjectContract({ ...location, id: location.agentPolicyId, @@ -80,17 +81,17 @@ export const addPrivateLocationRoute: SyntheticsRestApiRouteFactory( - privateLocationsSavedObjectName, - { locations: [...existingLocations, formattedLocation] }, + const soClient = routeContext.server.coreStart.savedObjects.createInternalRepository(); + + const result = await soClient.create( + privateLocationSavedObjectName, + formattedLocation, { - id: privateLocationsSavedObjectId, - overwrite: true, + id: location.agentPolicyId, + initialNamespaces: ['*'], } ); - const allLocations = toClientContract(result.attributes, agentPolicies); - - return allLocations.find((loc) => loc.id === location.agentPolicyId)!; + return toClientContract(result.attributes, agentPolicies); }, }); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts index 1c6ede5a2ad00..bac3907eac871 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts @@ -7,15 +7,12 @@ import { schema } from '@kbn/config-schema'; import { isEmpty } from 'lodash'; +import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { getMonitorsByLocation } from './get_location_monitors'; import { getPrivateLocationsAndAgentPolicies } from './get_private_locations'; import { SyntheticsRestApiRouteFactory } from '../../types'; import { SYNTHETICS_API_URLS } from '../../../../common/constants'; -import { - privateLocationsSavedObjectId, - privateLocationsSavedObjectName, -} from '../../../../common/saved_objects/private_locations'; -import type { SyntheticsPrivateLocationsAttributes } from '../../../runtime_types/private_locations'; +import { privateLocationSavedObjectName } from '../../../../common/saved_objects/private_locations'; export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'DELETE', @@ -28,12 +25,16 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory { + handler: async (routeContext) => { + await migrateLegacyPrivateLocations(routeContext); + + const { savedObjectsClient, syntheticsMonitorClient, request, response, server } = routeContext; const { locationId } = request.params as { locationId: string }; const { locations } = await getPrivateLocationsAndAgentPolicies( savedObjectsClient, - syntheticsMonitorClient + syntheticsMonitorClient, + true ); if (!locations.find((loc) => loc.id === locationId)) { @@ -55,17 +56,8 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory loc.id !== locationId); - - await savedObjectsClient.create( - privateLocationsSavedObjectName, - { locations: remainingLocations }, - { - id: privateLocationsSavedObjectId, - overwrite: true, - } - ); - - return; + await savedObjectsClient.delete(privateLocationSavedObjectName, locationId, { + force: true, + }); }, }); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts index f7adc1e7ac16e..d884bba5c2b0a 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts @@ -7,6 +7,7 @@ import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { schema } from '@kbn/config-schema'; +import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { AgentPolicyInfo } from '../../../../common/types'; import { SyntheticsRestApiRouteFactory } from '../../types'; import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../common/runtime_types'; @@ -14,7 +15,7 @@ import { SYNTHETICS_API_URLS } from '../../../../common/constants'; import { getPrivateLocations } from '../../../synthetics_service/get_private_locations'; import type { SyntheticsPrivateLocationsAttributes } from '../../../runtime_types/private_locations'; import { SyntheticsMonitorClient } from '../../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; -import { toClientContract } from './helpers'; +import { allLocationsToClientContract } from './helpers'; export const getPrivateLocationsRoute: SyntheticsRestApiRouteFactory< SyntheticsPrivateLocations | PrivateLocation @@ -29,14 +30,17 @@ export const getPrivateLocationsRoute: SyntheticsRestApiRouteFactory< }), }, }, - handler: async ({ savedObjectsClient, syntheticsMonitorClient, request, response }) => { + handler: async (routeContext) => { + await migrateLegacyPrivateLocations(routeContext); + + const { savedObjectsClient, syntheticsMonitorClient, request, response } = routeContext; const { id } = request.params as { id?: string }; const { locations, agentPolicies } = await getPrivateLocationsAndAgentPolicies( savedObjectsClient, syntheticsMonitorClient ); - const list = toClientContract({ locations }, agentPolicies); + const list = allLocationsToClientContract({ locations }, agentPolicies); if (!id) return list; const location = list.find((loc) => loc.id === id || loc.label === id); if (!location) { @@ -53,7 +57,7 @@ export const getPrivateLocationsRoute: SyntheticsRestApiRouteFactory< export const getPrivateLocationsAndAgentPolicies = async ( savedObjectsClient: SavedObjectsClientContract, syntheticsMonitorClient: SyntheticsMonitorClient, - excludeAgentPolicies: boolean = false + excludeAgentPolicies = false ): Promise => { try { const [privateLocations, agentPolicies] = await Promise.all([ diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.test.ts index 6055b217f8794..84c531cb9ce70 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { toClientContract } from './helpers'; +import { allLocationsToClientContract } from './helpers'; const testLocations = { locations: [ @@ -56,7 +56,7 @@ const testLocations2 = { describe('toClientContract', () => { it('formats SO attributes to client contract with falsy geo location', () => { // @ts-ignore fixtures are purposely wrong types for testing - expect(toClientContract(testLocations)).toEqual([ + expect(allLocationsToClientContract(testLocations)).toEqual([ { agentPolicyId: 'e3134290-0f73-11ee-ba15-159f4f728deb', geo: { @@ -86,7 +86,7 @@ describe('toClientContract', () => { it('formats SO attributes to client contract with truthy geo location', () => { // @ts-ignore fixtures are purposely wrong types for testing - expect(toClientContract(testLocations2)).toEqual([ + expect(allLocationsToClientContract(testLocations2)).toEqual([ { agentPolicyId: 'e3134290-0f73-11ee-ba15-159f4f728deb', geo: { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.ts index 1c6c03067a817..8df065ad3e48d 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.ts @@ -13,6 +13,22 @@ import type { import { PrivateLocation } from '../../../../common/runtime_types'; export const toClientContract = ( + location: PrivateLocationAttributes, + agentPolicies?: AgentPolicyInfo[] +): PrivateLocation => { + const agPolicy = agentPolicies?.find((policy) => policy.id === location.agentPolicyId); + return { + label: location.label, + id: location.id, + agentPolicyId: location.agentPolicyId, + isServiceManaged: false, + isInvalid: !Boolean(agPolicy), + tags: location.tags, + geo: location.geo, + }; +}; + +export const allLocationsToClientContract = ( attributes: SyntheticsPrivateLocationsAttributes, agentPolicies?: AgentPolicyInfo[] ): SyntheticsPrivateLocations => { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts new file mode 100644 index 0000000000000..2305853aab3f1 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; +import { SyntheticsServerSetup } from '../../../types'; +import { coreMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import { + type ISavedObjectsRepository, + SavedObjectsClientContract, +} from '@kbn/core-saved-objects-api-server'; + +describe('migrateLegacyPrivateLocations', () => { + let serverMock: SyntheticsServerSetup; + let savedObjectsClient: jest.Mocked; + let repositoryMock: ISavedObjectsRepository; + beforeEach(() => { + const coreStartMock = coreMock.createStart(); + serverMock = { + coreStart: coreStartMock, + logger: loggerMock.create(), + } as any; + savedObjectsClient = savedObjectsClientMock.create(); + repositoryMock = coreMock.createStart().savedObjects.createInternalRepository(); + + coreStartMock.savedObjects.createInternalRepository.mockReturnValue(repositoryMock); + }); + + it('should get the legacy private locations', async () => { + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { locations: [{ id: '1', label: 'Location 1' }] }, + } as any); + savedObjectsClient.find.mockResolvedValueOnce({ total: 1 } as any); + + await migrateLegacyPrivateLocations({ + server: serverMock, + savedObjectsClient, + } as any); + + expect(savedObjectsClient.get).toHaveBeenCalledWith( + 'synthetics-privates-locations', + 'synthetics-privates-locations-singleton' + ); + }); + + it('should log and return if an error occurs while getting legacy private locations', async () => { + const error = new Error('Get error'); + savedObjectsClient.get.mockRejectedValueOnce(error); + + await migrateLegacyPrivateLocations({ + server: serverMock, + savedObjectsClient, + } as any); + + expect(serverMock.logger.error).toHaveBeenCalledWith( + `Error getting legacy private locations: ${error}` + ); + expect(repositoryMock.bulkCreate).not.toHaveBeenCalled(); + }); + + it('should return if there are no legacy locations', async () => { + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { locations: [] }, + } as any); + + await migrateLegacyPrivateLocations({ + server: serverMock, + savedObjectsClient: savedObjectsClientMock, + } as any); + + expect(repositoryMock.bulkCreate).not.toHaveBeenCalled(); + }); + + it('should bulk create new private locations if there are legacy locations', async () => { + const legacyLocations = [{ id: '1', label: 'Location 1' }]; + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { locations: legacyLocations }, + } as any); + savedObjectsClient.find.mockResolvedValueOnce({ total: 1 } as any); + + await migrateLegacyPrivateLocations({ + server: serverMock, + savedObjectsClient, + } as any); + + expect(repositoryMock.bulkCreate).toHaveBeenCalledWith( + legacyLocations.map((location) => ({ + id: location.id, + attributes: location, + type: 'synthetics-private-location', + initialNamespaces: ['*'], + })), + { overwrite: true } + ); + }); + + it('should delete legacy private locations if bulk create count matches', async () => { + const legacyLocations = [{ id: '1', label: 'Location 1' }]; + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { locations: legacyLocations }, + } as any); + savedObjectsClient.find.mockResolvedValueOnce({ total: 1 } as any); + + await migrateLegacyPrivateLocations({ + server: serverMock, + savedObjectsClient, + } as any); + + expect(savedObjectsClient.delete).toHaveBeenCalledWith( + 'synthetics-privates-locations', + 'synthetics-privates-locations-singleton', + {} + ); + }); +}); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts new file mode 100644 index 0000000000000..cd73e27b950e3 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObject } from '@kbn/core-saved-objects-server'; +import { + type PrivateLocationAttributes, + SyntheticsPrivateLocationsAttributes, +} from '../../../runtime_types/private_locations'; +import { + legacyPrivateLocationsSavedObjectId, + legacyPrivateLocationsSavedObjectName, + privateLocationSavedObjectName, +} from '../../../../common/saved_objects/private_locations'; +import { RouteContext } from '../../types'; + +export const migrateLegacyPrivateLocations = async ({ + server, + savedObjectsClient, +}: RouteContext) => { + try { + let obj: SavedObject | undefined; + try { + obj = await savedObjectsClient.get( + legacyPrivateLocationsSavedObjectName, + legacyPrivateLocationsSavedObjectId + ); + } catch (e) { + server.logger.error(`Error getting legacy private locations: ${e}`); + return; + } + const legacyLocations = obj?.attributes.locations ?? []; + if (legacyLocations.length === 0) { + return; + } + + const soClient = server.coreStart.savedObjects.createInternalRepository(); + + await soClient.bulkCreate( + legacyLocations.map((location) => ({ + id: location.id, + attributes: location, + type: privateLocationSavedObjectName, + initialNamespaces: ['*'], + })), + { + overwrite: true, + } + ); + + const { total } = await savedObjectsClient.find({ + type: privateLocationSavedObjectName, + fields: [], + perPage: 0, + }); + + if (total === legacyLocations.length) { + await savedObjectsClient.delete( + legacyPrivateLocationsSavedObjectName, + legacyPrivateLocationsSavedObjectId, + {} + ); + } + } catch (e) { + server.logger.error(`Error migrating legacy private locations: ${e}`); + } +}; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/synthetics_service/get_service_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/synthetics_service/get_service_locations.ts index a9142170c9e26..ca704cdff1b28 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/synthetics_service/get_service_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/synthetics_service/get_service_locations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { toClientContract } from '../settings/private_locations/helpers'; +import { allLocationsToClientContract } from '../settings/private_locations/helpers'; import { getPrivateLocationsAndAgentPolicies } from '../settings/private_locations/get_private_locations'; import { SyntheticsRestApiRouteFactory } from '../types'; import { getAllLocations } from '../../synthetics_service/get_all_locations'; @@ -45,7 +45,7 @@ export const getServiceLocationsRoute: SyntheticsRestApiRouteFactory = () => ({ const { locations: privateLocations, agentPolicies } = await getPrivateLocationsAndAgentPolicies(savedObjectsClient, syntheticsMonitorClient); - const result = toClientContract({ locations: privateLocations }, agentPolicies); + const result = allLocationsToClientContract({ locations: privateLocations }, agentPolicies); return { locations: result, }; diff --git a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/migrations/private_locations/model_version_1.test.ts b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/migrations/private_locations/model_version_1.test.ts index 63a9f940143a4..dbcdea546a9f8 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/migrations/private_locations/model_version_1.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/migrations/private_locations/model_version_1.test.ts @@ -5,7 +5,7 @@ * 2.0. */ import { transformGeoProperty } from './model_version_1'; -import { privateLocationsSavedObjectName } from '../../../../common/saved_objects/private_locations'; +import { legacyPrivateLocationsSavedObjectName } from '../../../../common/saved_objects/private_locations'; describe('model version 1 migration', () => { const testLocation = { @@ -19,7 +19,7 @@ describe('model version 1 migration', () => { concurrentMonitors: 1, }; const testObject = { - type: privateLocationsSavedObjectName, + type: legacyPrivateLocationsSavedObjectName, id: 'synthetics-privates-locations-singleton', attributes: { locations: [testLocation], diff --git a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/private_locations.ts index ee7426ead23af..370c8d203dff6 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/private_locations.ts @@ -7,11 +7,33 @@ import { SavedObjectsType } from '@kbn/core/server'; import { modelVersion1 } from './migrations/private_locations/model_version_1'; -import { privateLocationsSavedObjectName } from '../../common/saved_objects/private_locations'; -export const privateLocationsSavedObjectId = 'synthetics-privates-locations-singleton'; +import { + legacyPrivateLocationsSavedObjectName, + privateLocationSavedObjectName, +} from '../../common/saved_objects/private_locations'; -export const PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE: SavedObjectsType = { - name: privateLocationsSavedObjectName, +export const PRIVATE_LOCATION_SAVED_OBJECT_TYPE: SavedObjectsType = { + name: privateLocationSavedObjectName, + hidden: false, + namespaceType: 'multiple', + mappings: { + dynamic: false, + properties: { + /* Leaving these commented to make it clear that these fields exist, even though we don't want them indexed. + When adding new fields please add them here. If they need to be searchable put them in the uncommented + part of properties. + */ + }, + }, + management: { + importableAndExportable: true, + }, +}; + +export const legacyPrivateLocationsSavedObjectId = 'synthetics-privates-locations-singleton'; + +export const LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE: SavedObjectsType = { + name: legacyPrivateLocationsSavedObjectName, hidden: false, namespaceType: 'agnostic', mappings: { diff --git a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts index 9b4a365941a7d..d59ecb507166b 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts @@ -24,7 +24,10 @@ import { SYNTHETICS_SECRET_ENCRYPTED_TYPE, syntheticsParamSavedObjectType, } from './synthetics_param'; -import { PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE } from './private_locations'; +import { + LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE, + PRIVATE_LOCATION_SAVED_OBJECT_TYPE, +} from './private_locations'; import { DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES } from '../constants/settings'; import { DynamicSettingsAttributes } from '../runtime_types/settings'; import { @@ -37,7 +40,8 @@ export const registerSyntheticsSavedObjects = ( savedObjectsService: SavedObjectsServiceSetup, encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ) => { - savedObjectsService.registerType(PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE); + savedObjectsService.registerType(LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE); + savedObjectsService.registerType(PRIVATE_LOCATION_SAVED_OBJECT_TYPE); savedObjectsService.registerType(getSyntheticsMonitorSavedObjectType(encryptedSavedObjects)); savedObjectsService.registerType(syntheticsServiceApiKey); diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_all_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_all_locations.ts index c24b28c00ca99..0d8355cebc1f6 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_all_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_all_locations.ts @@ -5,7 +5,7 @@ * 2.0. */ import { SavedObjectsClientContract } from '@kbn/core/server'; -import { toClientContract } from '../routes/settings/private_locations/helpers'; +import { allLocationsToClientContract } from '../routes/settings/private_locations/helpers'; import { getPrivateLocationsAndAgentPolicies } from '../routes/settings/private_locations/get_private_locations'; import { SyntheticsServerSetup } from '../types'; import { getServiceLocations } from './get_service_locations'; @@ -34,7 +34,10 @@ export async function getAllLocations({ ), getServicePublicLocations(server, syntheticsMonitorClient), ]); - const pvtLocations = toClientContract({ locations: privateLocations }, agentPolicies); + const pvtLocations = allLocationsToClientContract( + { locations: privateLocations }, + agentPolicies + ); return { publicLocations, privateLocations: pvtLocations, diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_private_locations.ts index a850cbf081e68..a476df9dfe038 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_private_locations.ts @@ -5,22 +5,42 @@ * 2.0. */ -import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server'; import { - privateLocationsSavedObjectId, - privateLocationsSavedObjectName, + SavedObject, + SavedObjectsClientContract, + SavedObjectsErrorHelpers, +} from '@kbn/core/server'; +import { uniqBy } from 'lodash'; +import { + legacyPrivateLocationsSavedObjectId, + legacyPrivateLocationsSavedObjectName, + privateLocationSavedObjectName, } from '../../common/saved_objects/private_locations'; -import type { SyntheticsPrivateLocationsAttributes } from '../runtime_types/private_locations'; +import { + PrivateLocationAttributes, + SyntheticsPrivateLocationsAttributes, +} from '../runtime_types/private_locations'; export const getPrivateLocations = async ( client: SavedObjectsClientContract ): Promise => { try { - const obj = await client.get( - privateLocationsSavedObjectName, - privateLocationsSavedObjectId - ); - return obj?.attributes.locations ?? []; + const finder = client.createPointInTimeFinder({ + type: privateLocationSavedObjectName, + perPage: 1000, + }); + + const results: Array> = []; + + for await (const response of finder.find()) { + results.push(...response.saved_objects); + } + + finder.close().catch((e) => {}); + + const legacyLocations = await getLegacyPrivateLocations(client); + + return uniqBy([...results.map((r) => r.attributes), ...legacyLocations], 'id'); } catch (getErr) { if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { return []; @@ -28,3 +48,15 @@ export const getPrivateLocations = async ( throw getErr; } }; + +const getLegacyPrivateLocations = async (client: SavedObjectsClientContract) => { + try { + const obj = await client.get( + legacyPrivateLocationsSavedObjectName, + legacyPrivateLocationsSavedObjectId + ); + return obj?.attributes.locations ?? []; + } catch (getErr) { + return []; + } +}; diff --git a/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts index 536935a4b3894..98db4ab754796 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts @@ -19,6 +19,7 @@ import { } from '../../tasks/live_query'; // FLAKY: https://github.com/elastic/kibana/issues/192128 +// Failing: See https://github.com/elastic/kibana/issues/192128 describe.skip('EcsMapping', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { beforeEach(() => { initializeDataViews(); diff --git a/x-pack/plugins/security/server/routes/analytics/authentication_type.ts b/x-pack/plugins/security/server/routes/analytics/authentication_type.ts index f2bf76c71b1ab..92094a65da7bb 100644 --- a/x-pack/plugins/security/server/routes/analytics/authentication_type.ts +++ b/x-pack/plugins/security/server/routes/analytics/authentication_type.ts @@ -31,6 +31,13 @@ export function defineRecordAnalyticsOnAuthTypeRoutes({ router.post( { path: '/internal/security/analytics/_record_auth_type', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the scoped ES cluster client of the internal authentication service', + }, + }, validate: { body: schema.nullable( schema.object({ signature: schema.string(), timestamp: schema.number() }) diff --git a/x-pack/plugins/security/server/routes/analytics/record_violations.ts b/x-pack/plugins/security/server/routes/analytics/record_violations.ts index 826a304f1656e..bec224a6d3eeb 100644 --- a/x-pack/plugins/security/server/routes/analytics/record_violations.ts +++ b/x-pack/plugins/security/server/routes/analytics/record_violations.ts @@ -135,6 +135,13 @@ export function defineRecordViolations({ router, analyticsService }: RouteDefini router.post( { path: '/internal/security/analytics/_record_violations', + security: { + authz: { + enabled: false, + reason: + 'This route is used by browsers to report CSP and Permission Policy violations. These requests are sent without authentication per the browser spec.', + }, + }, validate: { /** * Chrome supports CSP3 spec and sends an array of reports. Safari only sends a single diff --git a/x-pack/plugins/security/server/routes/anonymous_access/get_capabilities.ts b/x-pack/plugins/security/server/routes/anonymous_access/get_capabilities.ts index 220fb1515df46..84c8ed17e5963 100644 --- a/x-pack/plugins/security/server/routes/anonymous_access/get_capabilities.ts +++ b/x-pack/plugins/security/server/routes/anonymous_access/get_capabilities.ts @@ -15,7 +15,17 @@ export function defineAnonymousAccessGetCapabilitiesRoutes({ getAnonymousAccessService, }: RouteDefinitionParams) { router.get( - { path: '/internal/security/anonymous_access/capabilities', validate: false }, + { + path: '/internal/security/anonymous_access/capabilities', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the scoped ES cluster client of the anonymous access service', + }, + }, + validate: false, + }, async (_context, request, response) => { const anonymousAccessService = getAnonymousAccessService(); return response.ok({ body: await anonymousAccessService.getCapabilities(request) }); diff --git a/x-pack/plugins/security/server/routes/anonymous_access/get_state.ts b/x-pack/plugins/security/server/routes/anonymous_access/get_state.ts index 28745c80a5f44..8911588b72109 100644 --- a/x-pack/plugins/security/server/routes/anonymous_access/get_state.ts +++ b/x-pack/plugins/security/server/routes/anonymous_access/get_state.ts @@ -18,7 +18,16 @@ export function defineAnonymousAccessGetStateRoutes({ getAnonymousAccessService, }: RouteDefinitionParams) { router.get( - { path: '/internal/security/anonymous_access/state', validate: false }, + { + path: '/internal/security/anonymous_access/state', + security: { + authz: { + enabled: false, + reason: 'This route is used for anonymous access', + }, + }, + validate: false, + }, async (_context, _request, response) => { const anonymousAccessService = getAnonymousAccessService(); const accessURLParameters = anonymousAccessService.accessURLParameters diff --git a/x-pack/plugins/security/server/routes/api_keys/create.ts b/x-pack/plugins/security/server/routes/api_keys/create.ts index 59d743e3726aa..963e6c7ced35b 100644 --- a/x-pack/plugins/security/server/routes/api_keys/create.ts +++ b/x-pack/plugins/security/server/routes/api_keys/create.ts @@ -32,6 +32,13 @@ export function defineCreateApiKeyRoutes({ router.post( { path: '/internal/security/api_key', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the scoped ES cluster client of the internal authentication service', + }, + }, validate: { body: schema.oneOf([ restApiKeySchema, diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.ts index c94c8af61e24f..dd06c93a71e88 100644 --- a/x-pack/plugins/security/server/routes/api_keys/enabled.ts +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.ts @@ -16,6 +16,13 @@ export function defineEnabledApiKeysRoutes({ router.get( { path: '/internal/security/api_key/_enabled', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the scoped ES cluster client of the internal authentication service', + }, + }, validate: false, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/api_keys/has_active.ts b/x-pack/plugins/security/server/routes/api_keys/has_active.ts index bf432b1861045..b1cc220f802b1 100644 --- a/x-pack/plugins/security/server/routes/api_keys/has_active.ts +++ b/x-pack/plugins/security/server/routes/api_keys/has_active.ts @@ -22,6 +22,12 @@ export function defineHasApiKeysRoutes({ router.get( { path: '/internal/security/api_key/_has_active', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the scoped ES cluster client of the internal authentication service, and to Core's ES client`, + }, + }, validate: false, options: { access: 'internal', diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts index 1983dbf2344e0..f2d72185d0b1c 100644 --- a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts @@ -21,6 +21,12 @@ export function defineInvalidateApiKeysRoutes({ router }: RouteDefinitionParams) router.post( { path: '/internal/security/api_key/invalidate', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's ES client`, + }, + }, validate: { body: schema.object({ apiKeys: schema.arrayOf(schema.object({ id: schema.string(), name: schema.string() })), diff --git a/x-pack/plugins/security/server/routes/api_keys/query.ts b/x-pack/plugins/security/server/routes/api_keys/query.ts index 9fe8fdbdc734b..382d3a290aa7e 100644 --- a/x-pack/plugins/security/server/routes/api_keys/query.ts +++ b/x-pack/plugins/security/server/routes/api_keys/query.ts @@ -25,6 +25,12 @@ export function defineQueryApiKeysAndAggregationsRoute({ // on behalf of the user making the request and governed by the user's own cluster privileges. { path: '/internal/security/api_key/_query', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the scoped ES cluster client of the internal authentication service, and to Core's ES client`, + }, + }, validate: { body: schema.object({ query: schema.maybe(schema.object({}, { unknowns: 'allow' })), diff --git a/x-pack/plugins/security/server/routes/api_keys/update.ts b/x-pack/plugins/security/server/routes/api_keys/update.ts index a7fe43c46e206..364a0af0b95ad 100644 --- a/x-pack/plugins/security/server/routes/api_keys/update.ts +++ b/x-pack/plugins/security/server/routes/api_keys/update.ts @@ -34,6 +34,12 @@ export function defineUpdateApiKeyRoutes({ router.put( { path: '/internal/security/api_key', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the scoped ES cluster client of the internal authentication service`, + }, + }, validate: { body: schema.oneOf([ updateRestApiKeySchema, diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index b519171fd4fe6..0c91a6c7f3858 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -43,6 +43,12 @@ export function defineCommonRoutes({ router.get( { path, + security: { + authz: { + enabled: false, + reason: 'This route must remain accessible to 3rd-party IdPs', + }, + }, // Allow unknown query parameters as this endpoint can be hit by the 3rd-party with any // set of query string parameters (e.g. SAML/OIDC logout request/response parameters). validate: { query: schema.object({}, { unknowns: 'allow' }) }, @@ -92,7 +98,17 @@ export function defineCommonRoutes({ ]) { const deprecated = path === '/api/security/v1/me'; router.get( - { path, validate: false, options: { access: deprecated ? 'public' : 'internal' } }, + { + path, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's security service; there must be an authenticated user for this route to return information`, + }, + }, + validate: false, + options: { access: deprecated ? 'public' : 'internal' }, + }, createLicensedRouteHandler(async (context, request, response) => { if (deprecated) { logger.warn( @@ -135,10 +151,16 @@ export function defineCommonRoutes({ } // Register the login route for serverless for the time being. Note: This route will move into the buildFlavor !== 'serverless' block below. See next line. - // ToDo: In the serverless environment, we do not support API login - the only valid authentication methodology (or maybe just method or mechanism?) is SAML + // ToDo: In the serverless environment, we do not support API login - the only valid authentication type is SAML router.post( { path: '/internal/security/login', + security: { + authz: { + enabled: false, + reason: `This route provides basic and token login capbility, which is delegated to the internal authentication service`, + }, + }, validate: { body: schema.object({ providerType: schema.string(), @@ -183,7 +205,16 @@ export function defineCommonRoutes({ if (buildFlavor !== 'serverless') { // In the serverless offering, the access agreement functionality isn't available. router.post( - { path: '/internal/security/access_agreement/acknowledge', validate: false }, + { + path: '/internal/security/access_agreement/acknowledge', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the internal authentication service; there must be an authenticated user for this route to function`, + }, + }, + validate: false, + }, createLicensedRouteHandler(async (context, request, response) => { // If license doesn't allow access agreement we shouldn't handle request. if (!license.getFeatures().allowAccessAgreement) { diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts index 69c3ce1700671..bb1ed6959e690 100644 --- a/x-pack/plugins/security/server/routes/authentication/oidc.ts +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -87,6 +87,12 @@ export function defineOIDCRoutes({ router.get( { path, + security: { + authz: { + enabled: false, + reason: 'This route must remain accessible to 3rd-party OIDC providers', + }, + }, validate: { query: schema.object( { @@ -176,6 +182,12 @@ export function defineOIDCRoutes({ router.post( { path, + security: { + authz: { + enabled: false, + reason: 'This route must remain accessible to 3rd-party OIDC providers', + }, + }, validate: { body: schema.object( { @@ -221,6 +233,12 @@ export function defineOIDCRoutes({ router.get( { path: '/api/security/oidc/initiate_login', + security: { + authz: { + enabled: false, + reason: 'This route must remain accessible to 3rd-party OIDC providers', + }, + }, validate: { query: schema.object( { diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 3c72fd908e6c4..8cee1df2da88b 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -30,6 +30,12 @@ export function defineSAMLRoutes({ router.post( { path, + security: { + authz: { + enabled: false, + reason: 'This route must remain accessible to 3rd-party SAML providers', + }, + }, validate: { body: schema.object( { SAMLResponse: schema.string(), RelayState: schema.maybe(schema.string()) }, diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get.ts index b7204faaa7ca4..23fb7ccd9bf39 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get.ts @@ -14,6 +14,13 @@ export function defineGetPrivilegesRoutes({ router, authz }: RouteDefinitionPara router.get( { path: '/api/security/privileges', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it returns only the global list of Kibana privileges', + }, + }, validate: { query: schema.object({ // We don't use `schema.boolean` here, because all query string parameters are treated as diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts index 9a9c2dd6fcc71..1a35875de72e0 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts @@ -9,7 +9,16 @@ import type { RouteDefinitionParams } from '../..'; export function defineGetBuiltinPrivilegesRoutes({ router }: RouteDefinitionParams) { router.get( - { path: '/internal/security/esPrivileges/builtin', validate: false }, + { + path: '/internal/security/esPrivileges/builtin', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, + validate: false, + }, async (context, request, response) => { const esClient = (await context.core).elasticsearch.client; const privileges = await esClient.asCurrentUser.security.getBuiltinPrivileges(); 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 07f314da4232b..b4ff278db219f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts @@ -25,6 +25,12 @@ export function defineDeleteRolesRoutes({ router }: RouteDefinitionParams) { .addVersion( { version: API_VERSIONS.roles.public.v1, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { request: { params: schema.object({ name: schema.string({ minLength: 1 }) }), 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 031da53092b09..d088ada568541 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -32,6 +32,12 @@ export function defineGetRolesRoutes({ .addVersion( { version: API_VERSIONS.roles.public.v1, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { request: { params: schema.object({ 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 5979922cd64e4..9a6d1d56569c4 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 @@ -33,6 +33,12 @@ export function defineGetAllRolesRoutes({ .addVersion( { version: API_VERSIONS.roles.public.v1, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { request: { query: schema.maybe( 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 949553e960c9b..4a41533e93a85 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/post.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/post.ts @@ -52,6 +52,12 @@ export function defineBulkCreateOrUpdateRolesRoutes({ .addVersion( { version: API_VERSIONS.roles.public.v1, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { request: { body: getBulkCreateOrUpdatePayloadSchema(() => { 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 268c84ff7420e..ce0b8222d412e 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -35,6 +35,12 @@ export function definePutRolesRoutes({ .addVersion( { version: API_VERSIONS.roles.public.v1, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { request: { params: schema.object({ diff --git a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts index 536220eff03da..4c83455844a26 100644 --- a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts +++ b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts @@ -19,6 +19,12 @@ export function defineShareSavedObjectPermissionRoutes({ router.get( { path: '/internal/security/_share_saved_object_permissions', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the internal authorization service's checkPrivilegesWithRequest function`, + }, + }, validate: { query: schema.object({ type: schema.string() }) }, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts index 638a8f8a1bc7d..e465369ff0911 100644 --- a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts +++ b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts @@ -23,6 +23,12 @@ export function defineKibanaUserRoleDeprecationRoutes({ router, logger }: RouteD router.post( { path: '/internal/security/deprecations/kibana_user_role/_fix_users', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: false, }, createLicensedRouteHandler(async (context, request, response) => { @@ -88,6 +94,12 @@ export function defineKibanaUserRoleDeprecationRoutes({ router, logger }: RouteD router.post( { path: '/internal/security/deprecations/kibana_user_role/_fix_role_mappings', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: false, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/feature_check/feature_check.ts b/x-pack/plugins/security/server/routes/feature_check/feature_check.ts index b256ee77e55ff..6f4cd5b4b2654 100644 --- a/x-pack/plugins/security/server/routes/feature_check/feature_check.ts +++ b/x-pack/plugins/security/server/routes/feature_check/feature_check.ts @@ -43,6 +43,12 @@ export function defineSecurityFeatureCheckRoute({ router, logger }: RouteDefinit router.get( { path: '/internal/security/_check_security_features', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: false, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.ts b/x-pack/plugins/security/server/routes/indices/get_fields.ts index b0ec51339e080..4cfd6845e61bb 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.ts @@ -14,6 +14,12 @@ export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/fields/{query}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ query: schema.string() }) }, }, async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.ts index e305de6e4fcb4..8e331600ba490 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/delete.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.ts @@ -15,6 +15,12 @@ export function defineRoleMappingDeleteRoutes({ router }: RouteDefinitionParams) router.delete( { path: '/internal/security/role_mapping/{name}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ name: schema.string(), diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index ac6e7efaa8b0a..2b5ce017fbfb0 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -18,6 +18,12 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { router.get( { path: '/internal/security/role_mapping/{name?}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ name: schema.maybe(schema.string()), diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.ts b/x-pack/plugins/security/server/routes/role_mapping/post.ts index a9a87d4b2be51..e01dd446b6e51 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/post.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/post.ts @@ -15,6 +15,12 @@ export function defineRoleMappingPostRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/role_mapping/{name}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ name: schema.string(), diff --git a/x-pack/plugins/security/server/routes/security_checkup/get_state.ts b/x-pack/plugins/security/server/routes/security_checkup/get_state.ts index 2946c3fa5dee3..40da0959c7418 100644 --- a/x-pack/plugins/security/server/routes/security_checkup/get_state.ts +++ b/x-pack/plugins/security/server/routes/security_checkup/get_state.ts @@ -29,7 +29,16 @@ export function defineSecurityCheckupGetStateRoutes({ const doesClusterHaveUserData = createClusterDataCheck(); router.get( - { path: '/internal/security/security_checkup/state', validate: false }, + { + path: '/internal/security/security_checkup/state', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, + validate: false, + }, async (context, _request, response) => { const esClient = (await context.core).elasticsearch.client; let displayAlert = false; diff --git a/x-pack/plugins/security/server/routes/session_management/extend.ts b/x-pack/plugins/security/server/routes/session_management/extend.ts index b1626ba4660b3..1180303d48aac 100644 --- a/x-pack/plugins/security/server/routes/session_management/extend.ts +++ b/x-pack/plugins/security/server/routes/session_management/extend.ts @@ -14,6 +14,13 @@ export function defineSessionExtendRoutes({ router, basePath }: RouteDefinitionP router.post( { path: '/internal/security/session', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it only redirects to the /internal/security/session endpoint', + }, + }, validate: false, }, async (_context, _request, response) => { diff --git a/x-pack/plugins/security/server/routes/session_management/info.ts b/x-pack/plugins/security/server/routes/session_management/info.ts index 75fae27e8cb12..c49cb7575399e 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.ts @@ -14,7 +14,17 @@ import type { SessionInfo } from '../../../common/types'; */ export function defineSessionInfoRoutes({ router, getSession }: RouteDefinitionParams) { router.get( - { path: '/internal/security/session', validate: false }, + { + path: '/internal/security/session', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because a valid session is required, and it does not return sensative session information', + }, + }, + validate: false, + }, async (_context, request, response) => { const { value: sessionValue } = await getSession().get(request); if (sessionValue) { diff --git a/x-pack/plugins/security/server/routes/user_profile/get_current.ts b/x-pack/plugins/security/server/routes/user_profile/get_current.ts index 9661570e36b4e..4621d543b49ca 100644 --- a/x-pack/plugins/security/server/routes/user_profile/get_current.ts +++ b/x-pack/plugins/security/server/routes/user_profile/get_current.ts @@ -20,6 +20,13 @@ export function defineGetCurrentUserProfileRoute({ router.get( { path: '/internal/security/user_profile', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the internal authorization service; a currently authenticated user is required', + }, + }, validate: { query: schema.object({ dataPath: schema.maybe(schema.string()) }), }, diff --git a/x-pack/plugins/security/server/routes/user_profile/update.ts b/x-pack/plugins/security/server/routes/user_profile/update.ts index 9a550ada52adc..a400d0db88b89 100644 --- a/x-pack/plugins/security/server/routes/user_profile/update.ts +++ b/x-pack/plugins/security/server/routes/user_profile/update.ts @@ -27,6 +27,13 @@ export function defineUpdateUserProfileDataRoute({ router.post( { path: '/internal/security/user_profile/_data', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the internal authorization service; an authenticated user and valid session are required', + }, + }, validate: { body: schema.recordOf(schema.string(), schema.any()), }, diff --git a/x-pack/plugins/security/server/routes/users/change_password.ts b/x-pack/plugins/security/server/routes/users/change_password.ts index bd71785ab9549..964d3d6fe888b 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.ts @@ -24,6 +24,12 @@ export function defineChangeUserPasswordRoutes({ router.post( { path: '/internal/security/users/{username}/password', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the internal authorization service and the Security plugin's canUserChangePassword function`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), body: schema.object({ diff --git a/x-pack/plugins/security/server/routes/users/create_or_update.ts b/x-pack/plugins/security/server/routes/users/create_or_update.ts index de6adad78b4e8..c6c0bcbc48415 100644 --- a/x-pack/plugins/security/server/routes/users/create_or_update.ts +++ b/x-pack/plugins/security/server/routes/users/create_or_update.ts @@ -15,6 +15,12 @@ export function defineCreateOrUpdateUserRoutes({ router }: RouteDefinitionParams router.post( { path: '/internal/security/users/{username}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), body: schema.object({ diff --git a/x-pack/plugins/security/server/routes/users/delete.ts b/x-pack/plugins/security/server/routes/users/delete.ts index 429adb368574a..39f838dff7d8c 100644 --- a/x-pack/plugins/security/server/routes/users/delete.ts +++ b/x-pack/plugins/security/server/routes/users/delete.ts @@ -15,6 +15,12 @@ export function defineDeleteUserRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/internal/security/users/{username}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), }, diff --git a/x-pack/plugins/security/server/routes/users/disable.ts b/x-pack/plugins/security/server/routes/users/disable.ts index 87f61daca8c95..f2984504922b3 100644 --- a/x-pack/plugins/security/server/routes/users/disable.ts +++ b/x-pack/plugins/security/server/routes/users/disable.ts @@ -15,6 +15,12 @@ export function defineDisableUserRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/users/{username}/_disable', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), }, diff --git a/x-pack/plugins/security/server/routes/users/enable.ts b/x-pack/plugins/security/server/routes/users/enable.ts index a8a9d62bee938..18ec66683bd56 100644 --- a/x-pack/plugins/security/server/routes/users/enable.ts +++ b/x-pack/plugins/security/server/routes/users/enable.ts @@ -15,6 +15,12 @@ export function defineEnableUserRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/users/{username}/_enable', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), }, diff --git a/x-pack/plugins/security/server/routes/users/get.ts b/x-pack/plugins/security/server/routes/users/get.ts index ed18c8437627d..076c8c9beeef2 100644 --- a/x-pack/plugins/security/server/routes/users/get.ts +++ b/x-pack/plugins/security/server/routes/users/get.ts @@ -15,6 +15,12 @@ export function defineGetUserRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/users/{username}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), }, diff --git a/x-pack/plugins/security/server/routes/users/get_all.ts b/x-pack/plugins/security/server/routes/users/get_all.ts index eae0664189340..c8c340f2b2ceb 100644 --- a/x-pack/plugins/security/server/routes/users/get_all.ts +++ b/x-pack/plugins/security/server/routes/users/get_all.ts @@ -11,7 +11,16 @@ import { createLicensedRouteHandler } from '../licensed_route_handler'; export function defineGetAllUsersRoutes({ router }: RouteDefinitionParams) { router.get( - { path: '/internal/security/users', validate: false }, + { + path: '/internal/security/users', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, + validate: false, + }, createLicensedRouteHandler(async (context, request, response) => { try { const esClient = (await context.core).elasticsearch.client; diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts index 823fbb0286f33..ff6399f186610 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts @@ -35,7 +35,17 @@ export function defineAccessAgreementRoutes({ ); router.get( - { path: '/internal/security/access_agreement/state', validate: false }, + { + path: '/internal/security/access_agreement/state', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it requires only an active session in order to function', + }, + }, + validate: false, + }, createLicensedRouteHandler(async (context, request, response) => { if (!canHandleRequest()) { return response.forbidden({ diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index 8cf8459d523b8..ed3228c244b51 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -57,7 +57,18 @@ export function defineLoginRoutes({ ); router.get( - { path: '/internal/security/login_state', validate: false, options: { authRequired: false } }, + { + path: '/internal/security/login_state', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it only provides non-sensative information about authentication provider configuration', + }, + }, + validate: false, + options: { authRequired: false }, + }, async (context, request, response) => { const { allowLogin, layout = 'form' } = license.getFeatures(); const { sortedProviders, selector } = config.authc; diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/release.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/release.cy.ts index d11b7210713a8..5ad7395efbe21 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/release.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/release.cy.ts @@ -27,7 +27,8 @@ import { enableAllPolicyProtections } from '../../../tasks/endpoint_policy'; import { createEndpointHost } from '../../../tasks/create_endpoint_host'; import { deleteAllLoadedEndpointData } from '../../../tasks/delete_all_endpoint_data'; -describe('Response console', { tags: ['@ess', '@serverless'] }, () => { +// FLAKY: https://github.com/elastic/kibana/issues/172326 +describe.skip('Response console', { tags: ['@ess', '@serverless'] }, () => { let indexedPolicy: IndexedFleetEndpointPolicyResponse; let policy: PolicyData; let createdHost: CreateAndEnrollEndpointHostResponse; diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index 688f8297271a3..33c32acfef011 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -392,14 +392,12 @@ describe('capabilitiesSwitcher', () => { { space.solution = 'oblt'; - // It should disable enterpriseSearch and securitySolution features - // which correspond to feature_1 and feature_3 + // It should disable securitySolution features + // which corresponds to feature_3 const result = await switcher(request, capabilities, false); const expectedCapabilities = buildCapabilities(); - expectedCapabilities.feature_1.bar = false; - expectedCapabilities.feature_1.foo = false; expectedCapabilities.feature_3.bar = false; expectedCapabilities.feature_3.foo = false; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 24a94b43029e0..9da144facf4f4 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -49,9 +49,21 @@ describe.skip('onPostAuthInterceptor', () => { */ function initKbnServer(router: IRouter, basePath: IBasePath) { - router.get({ path: '/api/np_test/foo', validate: false }, (context, req, h) => { - return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); - }); + router.get( + { + path: '/api/np_test/foo', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, + validate: false, + }, + (context, req, h) => { + return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); + } + ); } async function request( diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index bf3d0a57ccae2..3a5ce95ec3341 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -38,14 +38,32 @@ describe.skip('onRequestInterceptor', () => { function initKbnServer(router: IRouter, basePath: IBasePath) { router.get( - { path: '/np_foo', validate: false }, + { + path: '/np_foo', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, + validate: false, + }, (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); } ); router.get( - { path: '/some/path/s/np_foo/bar', validate: false }, + { + path: '/some/path/s/np_foo/bar', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, + validate: false, + }, (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); } @@ -54,6 +72,12 @@ describe.skip('onRequestInterceptor', () => { router.get( { path: '/i/love/np_spaces', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: { query: schema.object({ queryParam: schema.string({ diff --git a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts index 908a4ee2ced57..f19b4d585dc22 100644 --- a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts +++ b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts @@ -58,7 +58,7 @@ describe('#withSpaceSolutionDisabledFeatures', () => { }); describe('when the space solution is "oblt"', () => { - test('it removes the "search" and "security" features', () => { + test('it removes the "security" features', () => { const spaceDisabledFeatures: string[] = []; const spaceSolution = 'oblt'; @@ -68,7 +68,7 @@ describe('#withSpaceSolutionDisabledFeatures', () => { spaceSolution ); - expect(result).toEqual(['feature2', 'feature3']); + expect(result).toEqual(['feature3']); }); }); diff --git a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts index 6d30645325535..2682daf3a1c54 100644 --- a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts +++ b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts @@ -62,7 +62,6 @@ export function withSpaceSolutionDisabledFeatures( ]).filter((featureId) => !enabledFeaturesPerSolution.es.includes(featureId)); } else if (spaceSolution === 'oblt') { disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ - 'enterpriseSearch', 'securitySolution', ]).filter((featureId) => !enabledFeaturesPerSolution.oblt.includes(featureId)); } else if (spaceSolution === 'security') { 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 06bef75774aa0..4908f1a747b74 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.ts @@ -31,6 +31,13 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) { .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, validate: { request: { params: schema.object({ diff --git a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts index a1610bbfed975..2703e7c36f0cd 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts @@ -18,6 +18,13 @@ export function initDisableLegacyUrlAliasesApi(deps: ExternalRouteDeps) { router.post( { path: '/api/spaces/_disable_legacy_url_aliases', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, options: { access: isServerless ? 'internal' : 'public', summary: 'Disable legacy URL aliases', 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 b1ab2dc575774..3c9871e44490c 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.ts @@ -28,6 +28,13 @@ export function initGetSpaceApi(deps: ExternalRouteDeps) { .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, validate: { request: { params: schema.object({ 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 746735bb3736e..f7a0c4592387c 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 @@ -27,6 +27,13 @@ export function initGetAllSpacesApi(deps: ExternalRouteDeps) { .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, validate: { request: { query: schema.object({ diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts index f49070be66fe2..98dab60cd9c95 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts @@ -17,6 +17,13 @@ export function initGetShareableReferencesApi(deps: ExternalRouteDeps) { router.post( { path: '/api/spaces/_get_shareable_references', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, options: { access: isServerless ? 'internal' : 'public', summary: `Get shareable references`, 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 de1ec53aaee44..2ecd70828d570 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.ts @@ -30,6 +30,13 @@ export function initPostSpacesApi(deps: ExternalRouteDeps) { .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, validate: { request: { body: getSpaceSchema(isServerless), 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 740e81bac446e..abdac1f0977d1 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.ts @@ -29,6 +29,13 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) { .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, validate: { request: { params: schema.object({ diff --git a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts index 9fb2a8626a841..fb9137a834349 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts @@ -40,6 +40,13 @@ export function initUpdateObjectsSpacesApi(deps: ExternalRouteDeps) { router.post( { path: '/api/spaces/_update_objects_spaces', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, options: { access: isServerless ? 'internal' : 'public', summary: `Update saved objects in spaces`, diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts index 2996e7dbc4ed1..2480b0c003fee 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts @@ -15,6 +15,13 @@ export function initGetActiveSpaceApi(deps: InternalRouteDeps) { router.get( { path: '/internal/spaces/_active_space', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service getActiveSpace API, which uses a scoped spaces client', + }, + }, validate: false, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.ts b/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.ts index 6732a8520946d..cfe14705a4e22 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.ts @@ -37,6 +37,13 @@ export function initSetSolutionSpaceApi(deps: InternalRouteDeps) { router.put( { path: '/internal/spaces/space/{id}/solution', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, options: { description: `Update solution for a space`, }, diff --git a/x-pack/test/alerting_api_integration/observability/synthetics/private_location_test_service.ts b/x-pack/test/alerting_api_integration/observability/synthetics/private_location_test_service.ts deleted file mode 100644 index e9ac7237dca52..0000000000000 --- a/x-pack/test/alerting_api_integration/observability/synthetics/private_location_test_service.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { v4 as uuidv4 } from 'uuid'; -import { privateLocationsSavedObjectName } from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; -import { privateLocationsSavedObjectId } from '@kbn/synthetics-plugin/server/saved_objects/private_locations'; -import { SyntheticsPrivateLocations } from '@kbn/synthetics-plugin/common/runtime_types'; -import { Agent as SuperTestAgent } from 'supertest'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export const INSTALLED_VERSION = '1.1.1'; - -export class PrivateLocationTestService { - private supertest: SuperTestAgent; - private readonly getService: FtrProviderContext['getService']; - - constructor(getService: FtrProviderContext['getService']) { - this.supertest = getService('supertest'); - this.getService = getService; - } - - async installSyntheticsPackage() { - await this.supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); - const response = await this.supertest - .get(`/api/fleet/epm/packages/synthetics/${INSTALLED_VERSION}`) - .set('kbn-xsrf', 'true') - .expect(200); - if (response.body.item.status !== 'installed') { - await this.supertest - .post(`/api/fleet/epm/packages/synthetics/${INSTALLED_VERSION}`) - .set('kbn-xsrf', 'true') - .send({ force: true }) - .expect(200); - } - } - - async addTestPrivateLocation() { - const apiResponse = await this.addFleetPolicy(uuidv4()); - const testPolicyId = apiResponse.body.item.id; - return (await this.setTestLocations([testPolicyId]))[0]; - } - - async addFleetPolicy(name: string) { - return this.supertest - .post('/api/fleet/agent_policies?sys_monitoring=true') - .set('kbn-xsrf', 'true') - .send({ - name, - description: '', - namespace: 'default', - monitoring_enabled: [], - }) - .expect(200); - } - - async setTestLocations(testFleetPolicyIds: string[]) { - const server = this.getService('kibanaServer'); - - const locations: SyntheticsPrivateLocations = testFleetPolicyIds.map((id, index) => ({ - label: 'Test private location ' + index, - agentPolicyId: id, - id, - geo: { - lat: 0, - lon: 0, - }, - isServiceManaged: false, - })); - - await server.savedObjects.create({ - type: privateLocationsSavedObjectName, - id: privateLocationsSavedObjectId, - attributes: { - locations, - }, - overwrite: true, - }); - return locations; - } -} diff --git a/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts b/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts index 2915cb5ee5d3b..a2da1c849945f 100644 --- a/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts +++ b/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts @@ -16,9 +16,9 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { Agent as SuperTestAgent } from 'supertest'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import expect from '@kbn/expect'; +import { PrivateLocationTestService } from '../../../api_integration/apis/synthetics/services/private_location_test_service'; import { waitForAlertInIndex } from '../helpers/alerting_wait_for_helpers'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { PrivateLocationTestService } from './private_location_test_service'; import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; export const SYNTHETICS_ALERT_ACTION_INDEX = 'alert-action-synthetics'; @@ -172,11 +172,6 @@ export class SyntheticsRuleHelper { return result.body as EncryptedSyntheticsSavedMonitor; } - async addPrivateLocation() { - await this.locService.installSyntheticsPackage(); - return this.locService.addTestPrivateLocation(); - } - async waitForStatusAlert({ ruleId, filters, diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts index 051ae14396687..5b0c967601638 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts @@ -37,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); let testFleetPolicyID: string; + let pvtLoc: PrivateLocation; const testPolicyName = 'Fleet test server policy' + Date.now(); let _httpMonitorJson: HTTPFields; @@ -68,29 +69,15 @@ export default function ({ getService }: FtrProviderContext) { httpMonitorJson = _httpMonitorJson; }); - it('adds a test fleet policy', async () => { - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testFleetPolicyID = apiResponse.body.item.id; - }); - it('add a test private location', async () => { - await testPrivateLocations.setTestLocations([testFleetPolicyID]); + pvtLoc = await testPrivateLocations.addPrivateLocation(); + testFleetPolicyID = pvtLoc.id; const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS); const testResponse: Array = [ ...getDevLocation('mockDevUrl'), - { - id: testFleetPolicyID, - isServiceManaged: false, - isInvalid: false, - label: 'Test private location 0', - geo: { - lat: 0, - lon: 0, - }, - agentPolicyId: testFleetPolicyID, - }, + { ...pvtLoc, isInvalid: false }, ]; expect(apiResponse.body.locations).eql(testResponse); @@ -137,16 +124,7 @@ export default function ({ getService }: FtrProviderContext) { it('adds a monitor in private location', async () => { const newMonitor = httpMonitorJson; - newMonitor.locations.push({ - id: testFleetPolicyID, - agentPolicyId: testFleetPolicyID, - label: 'Test private location 0', - isServiceManaged: false, - geo: { - lat: 0, - lon: 0, - }, - }); + newMonitor.locations.push(pvtLoc); const { body, rawBody } = await addMonitorAPI(newMonitor); @@ -182,19 +160,13 @@ export default function ({ getService }: FtrProviderContext) { const resPolicy = await testPrivateLocations.addFleetPolicy(testPolicyName + 1); testFleetPolicyID2 = resPolicy.body.item.id; - await testPrivateLocations.setTestLocations([testFleetPolicyID, testFleetPolicyID2]); - - httpMonitorJson.locations.push({ - id: testFleetPolicyID2, - agentPolicyId: testFleetPolicyID2, - label: 'Test private location ' + 1, - isServiceManaged: false, - geo: { - lat: 0, - lon: 0, - }, + const pvtLoc2 = await testPrivateLocations.addPrivateLocation({ + policyId: testFleetPolicyID2, + label: 'Test private location 1', }); + httpMonitorJson.locations.push(pvtLoc2); + const apiResponse = await supertestAPI .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId) .set('kbn-xsrf', 'true') @@ -308,54 +280,23 @@ export default function ({ getService }: FtrProviderContext) { }); it('handles spaces', async () => { - const username = 'admin'; - const password = `${username}-password`; - const roleName = 'uptime-role'; - const SPACE_ID = `test-space-${uuidv4()}`; - const SPACE_NAME = `test-space-name ${uuidv4()}`; + const { username, password, SPACE_ID, roleName } = await monitorTestService.addsNewSpace(); + let monitorId = ''; const monitor = { ...httpMonitorJson, name: `Test monitor ${uuidv4()}`, [ConfigKey.NAMESPACE]: 'default', - locations: [ - { - id: testFleetPolicyID, - agentPolicyId: testFleetPolicyID, - label: 'Test private location 0', - isServiceManaged: false, - geo: { - lat: 0, - lon: 0, - }, - }, - ], + locations: [pvtLoc], }; try { - await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); - await security.role.create(roleName, { - kibana: [ - { - feature: { - uptime: ['all'], - actions: ['all'], - }, - spaces: ['*'], - }, - ], - }); - await security.user.create(username, { - password, - roles: [roleName], - full_name: 'a kibana user', - }); const apiResponse = await supertestWithoutAuth .post(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .auth(username, password) .set('kbn-xsrf', 'true') - .send(monitor) - .expect(200); + .send(monitor); + expect(apiResponse.status).eql(200, JSON.stringify(apiResponse.body)); const { created_at: createdAt, updated_at: updatedAt } = apiResponse.body; expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]); diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts index ea8821901c9e9..5518988ff78c7 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -46,8 +46,6 @@ export default function ({ getService }: FtrProviderContext) { let icmpProjectMonitors: ProjectMonitorsRequest; let testPolicyId = ''; - const testPolicyName = 'Fleet test server policy' + Date.now(); - const setUniqueIds = (request: ProjectMonitorsRequest) => { return { ...request, @@ -87,9 +85,8 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); await testPrivateLocations.installSyntheticsPackage(); - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + const loc = await testPrivateLocations.addPrivateLocation(); + testPolicyId = loc.id; await supertest .post(SYNTHETICS_API_URLS.PARAMS) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts index 8ab44a6615890..c9c6c293d6130 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts @@ -24,9 +24,6 @@ export default function ({ getService }: FtrProviderContext) { let projectMonitors: ProjectMonitorsRequest; const monitorTestService = new SyntheticsMonitorTestService(getService); - - let testPolicyId = ''; - const testPolicyName = 'Fleet test server policy' + Date.now(); const testPrivateLocations = new PrivateLocationTestService(getService); const setUniqueIds = (request: ProjectMonitorsRequest) => { @@ -42,10 +39,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); await testPrivateLocations.installSyntheticsPackage(); - - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + await testPrivateLocations.addPrivateLocation(); }); after(async () => { diff --git a/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts b/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts index f8781295e8005..4192529654a28 100644 --- a/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts @@ -59,10 +59,8 @@ export default function ({ getService }: FtrProviderContext) { await testPrivateLocations.installSyntheticsPackage(); - const testPolicyName = 'Fleet test server policy' + Date.now(); - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + const loc = await testPrivateLocations.addPrivateLocation(); + testPolicyId = loc.id; }); beforeEach(() => { diff --git a/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts index ecd824a1052e7..0cb982ec90cc6 100644 --- a/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts @@ -37,11 +37,10 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); await testPrivateLocations.installSyntheticsPackage(); - const testPolicyName = 'Fleet test server policy' + Date.now(); - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + const loc = await testPrivateLocations.addPrivateLocation(); + testPolicyId = loc.id; }); beforeEach(() => { diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts index 7c23c4981c9cf..9767d1e447927 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts @@ -79,10 +79,8 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); - const testPolicyName = 'Fleet test server policy' + Date.now(); - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + const loc = await testPrivateLocations.addPrivateLocation(); + testPolicyId = loc.id; }); after(async () => { diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts index e8112627397c4..aeb0eaa0299b3 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts @@ -240,7 +240,7 @@ export default function ({ getService }: FtrProviderContext) { it('can add private location to existing monitor', async () => { await testPrivateLocations.installSyntheticsPackage(); - pvtLoc = await testPrivateLocations.addTestPrivateLocation(); + pvtLoc = await testPrivateLocations.addPrivateLocation(); expect(pvtLoc).not.empty(); diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts index d996d0181df6b..53089d2bec2d3 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts @@ -22,13 +22,13 @@ export default function ({ getService }: FtrProviderContext) { this.tags('skipCloud'); const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); let projectMonitors: LegacyProjectMonitorsRequest; let httpProjectMonitors: LegacyProjectMonitorsRequest; let tcpProjectMonitors: LegacyProjectMonitorsRequest; let icmpProjectMonitors: LegacyProjectMonitorsRequest; - let testPolicyId = ''; const testPrivateLocations = new PrivateLocationTestService(getService); const setUniqueIds = (request: LegacyProjectMonitorsRequest) => { @@ -39,12 +39,10 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); await testPrivateLocations.installSyntheticsPackage(); - const testPolicyName = 'Fleet test server policy' + Date.now(); - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + await testPrivateLocations.addPrivateLocation(); }); beforeEach(() => { diff --git a/x-pack/test/api_integration/apis/synthetics/index.ts b/x-pack/test/api_integration/apis/synthetics/index.ts index a8b39893570c2..15e76126e9555 100644 --- a/x-pack/test/api_integration/apis/synthetics/index.ts +++ b/x-pack/test/api_integration/apis/synthetics/index.ts @@ -35,5 +35,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./inspect_monitor')); loadTestFile(require.resolve('./test_now_monitor')); loadTestFile(require.resolve('./suggestions')); + loadTestFile(require.resolve('./private_location_apis')); }); } diff --git a/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts b/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts index 71132965ee520..7889f4ad37dfc 100644 --- a/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts @@ -171,7 +171,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('inspect http monitor in private location', async () => { - const location = await testPrivateLocations.addTestPrivateLocation(); + const location = await testPrivateLocations.addPrivateLocation(); const apiResponse = await monitorTestService.inspectMonitor({ ..._monitors[0], locations: [ diff --git a/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts b/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts new file mode 100644 index 0000000000000..415c91af28347 --- /dev/null +++ b/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { + legacyPrivateLocationsSavedObjectId, + legacyPrivateLocationsSavedObjectName, + privateLocationSavedObjectName, +} from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { PrivateLocationTestService } from './services/private_location_test_service'; + +export default function ({ getService }: FtrProviderContext) { + describe('PrivateLocationAPI', function () { + this.tags('skipCloud'); + + const kServer = getService('kibanaServer'); + + const testPrivateLocations = new PrivateLocationTestService(getService); + + before(async () => { + await testPrivateLocations.installSyntheticsPackage(); + + await kServer.savedObjects.clean({ + types: [legacyPrivateLocationsSavedObjectName, privateLocationSavedObjectName], + }); + }); + + it('adds a test legacy private location', async () => { + const locs = await testPrivateLocations.addLegacyPrivateLocations(); + expect(locs.length).to.be(2); + }); + + it('adds a test private location', async () => { + await testPrivateLocations.addPrivateLocation(); + }); + + it('list all locations', async () => { + const locs = await testPrivateLocations.fetchAll(); + expect(locs.body.length).to.be(3); + }); + + it('migrates to new saved objet type', async () => { + const newData = await kServer.savedObjects.find({ + type: privateLocationSavedObjectName, + }); + + expect(newData.saved_objects.length).to.be(3); + + try { + await kServer.savedObjects.get({ + type: legacyPrivateLocationsSavedObjectName, + id: legacyPrivateLocationsSavedObjectId, + }); + } catch (e) { + expect(e.response.status).to.be(404); + } + }); + }); +} diff --git a/x-pack/test/api_integration/apis/synthetics/services/private_location_test_service.ts b/x-pack/test/api_integration/apis/synthetics/services/private_location_test_service.ts index f08a708358a2f..f923a5dd887c1 100644 --- a/x-pack/test/api_integration/apis/synthetics/services/private_location_test_service.ts +++ b/x-pack/test/api_integration/apis/synthetics/services/private_location_test_service.ts @@ -4,11 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { v4 as uuidv4 } from 'uuid'; -import { privateLocationsSavedObjectName } from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; -import { privateLocationsSavedObjectId } from '@kbn/synthetics-plugin/server/saved_objects/private_locations'; -import { SyntheticsPrivateLocations } from '@kbn/synthetics-plugin/common/runtime_types'; +import expect from '@kbn/expect'; +import { PrivateLocation } from '@kbn/synthetics-plugin/common/runtime_types'; import { KibanaSupertestProvider } from '@kbn/ftr-common-functional-services'; +import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { + legacyPrivateLocationsSavedObjectId, + legacyPrivateLocationsSavedObjectName, +} from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; import { FtrProviderContext } from '../../../ftr_provider_context'; export const INSTALLED_VERSION = '1.1.1'; @@ -37,18 +40,12 @@ export class PrivateLocationTestService { } } - async addTestPrivateLocation() { - const apiResponse = await this.addFleetPolicy(uuidv4()); - const testPolicyId = apiResponse.body.item.id; - return (await this.setTestLocations([testPolicyId]))[0]; - } - - async addFleetPolicy(name: string) { + async addFleetPolicy(name?: string) { return this.supertest .post('/api/fleet/agent_policies?sys_monitoring=true') .set('kbn-xsrf', 'true') .send({ - name, + name: name ?? 'Fleet test server policy' + Date.now(), description: '', namespace: 'default', monitoring_enabled: [], @@ -56,28 +53,72 @@ export class PrivateLocationTestService { .expect(200); } - async setTestLocations(testFleetPolicyIds: string[]) { - const server = this.getService('kibanaServer'); + async addPrivateLocation({ policyId, label }: { policyId?: string; label?: string } = {}) { + let agentPolicyId = policyId; - const locations: SyntheticsPrivateLocations = testFleetPolicyIds.map((id, index) => ({ - label: 'Test private location ' + index, - agentPolicyId: id, - id, + if (!agentPolicyId) { + const apiResponse = await this.addFleetPolicy(); + agentPolicyId = apiResponse.body.item.id; + } + + const location: Omit = { + label: label ?? 'Test private location 0', + agentPolicyId: agentPolicyId!, geo: { lat: 0, lon: 0, }, - isServiceManaged: false, - })); + }; + + const response = await this.supertest + .post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS) + .set('kbn-xsrf', 'true') + .send(location); + + expect(response.status).to.be(200); + + const { isInvalid, ...loc } = response.body; + + return loc; + } + + async addLegacyPrivateLocations() { + const server = this.getService('kibanaServer'); + const fleetPolicy = await this.addFleetPolicy(); + const fleetPolicy2 = await this.addFleetPolicy(); + + const locs = [ + { + id: fleetPolicy.body.item.id, + agentPolicyId: fleetPolicy.body.item.id, + name: 'Test private location 1', + lat: 0, + lon: 0, + }, + { + id: fleetPolicy2.body.item.id, + agentPolicyId: fleetPolicy2.body.item.id, + name: 'Test private location 2', + lat: 0, + lon: 0, + }, + ]; await server.savedObjects.create({ - type: privateLocationsSavedObjectName, - id: privateLocationsSavedObjectId, + type: legacyPrivateLocationsSavedObjectName, + id: legacyPrivateLocationsSavedObjectId, attributes: { - locations, + locations: locs, }, overwrite: true, }); - return locations; + return locs; + } + + async fetchAll() { + return this.supertest + .get(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS) + .set('kbn-xsrf', 'true') + .expect(200); } } diff --git a/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts b/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts index c0c15024b5401..498da8c6b1800 100644 --- a/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts +++ b/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts @@ -202,7 +202,7 @@ export class SyntheticsMonitorTestService { full_name: 'a kibana user', }); - return { username, password, SPACE_ID }; + return { username, password, SPACE_ID, roleName }; } async deleteMonitor(monitorId?: string | string[], statusCode = 200, spaceId?: string) { diff --git a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts index e0a79a8905ee8..44cd5b19d6697 100644 --- a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts +++ b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts @@ -60,15 +60,9 @@ export default function ({ getService }: FtrProviderContext) { httpMonitorJson = _httpMonitorJson; }); - const testPolicyName = 'Fleet test server policy' + Date.now(); - - it('adds a test fleet policy', async () => { - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testFleetPolicyID = apiResponse.body.item.id; - }); - it('add a test private location', async () => { - await testPrivateLocations.setTestLocations([testFleetPolicyID]); + const loc = await testPrivateLocations.addPrivateLocation(); + testFleetPolicyID = loc.id; const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS); diff --git a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts index 0dcb52e348f8d..f6a98bf77e4fe 100644 --- a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts +++ b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts @@ -48,6 +48,15 @@ export default function ({ getService }: FtrProviderContext) { ); }; + before(async () => { + // clean up all api keys + const { body } = await esSupertest.get(`/_security/api_key`).query({ with_limited_by: true }); + const apiKeys = body.api_keys || []; + for (const apiKey of apiKeys) { + await esSupertest.delete(`/_security/api_key`).send({ ids: [apiKey.id] }); + } + }); + describe('[PUT] /internal/uptime/service/enablement', () => { beforeEach(async () => { const apiKeys = await getApiKeys(); diff --git a/x-pack/test/security_solution_api_integration/test_suites/explore/network/trial_license_complete_tier/tests/network_top_n_flow.ts b/x-pack/test/security_solution_api_integration/test_suites/explore/network/trial_license_complete_tier/tests/network_top_n_flow.ts index 31eabe76ee0e1..4c316e774ebbd 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/explore/network/trial_license_complete_tier/tests/network_top_n_flow.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/explore/network/trial_license_complete_tier/tests/network_top_n_flow.ts @@ -25,7 +25,8 @@ export default function ({ getService }: FtrProviderContextWithSpaces) { const esArchiver = getService('esArchiver'); const utils = getService('securitySolutionUtils'); - describe('Network Top N Flow', () => { + // FLAKY: https://github.com/elastic/kibana/issues/199363 + describe.skip('Network Top N Flow', () => { let supertest: TestAgent; let search: SearchService; describe('With filebeat', () => { diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts index d4cca44448676..1e3ca0eecfafe 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts @@ -8728,6 +8728,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:uptime-synthetics-api-key/delete", "saved_object:uptime-synthetics-api-key/bulk_delete", "saved_object:uptime-synthetics-api-key/share_to_space", + "saved_object:synthetics-private-location/bulk_get", + "saved_object:synthetics-private-location/get", + "saved_object:synthetics-private-location/find", + "saved_object:synthetics-private-location/open_point_in_time", + "saved_object:synthetics-private-location/close_point_in_time", + "saved_object:synthetics-private-location/create", + "saved_object:synthetics-private-location/bulk_create", + "saved_object:synthetics-private-location/update", + "saved_object:synthetics-private-location/bulk_update", + "saved_object:synthetics-private-location/delete", + "saved_object:synthetics-private-location/bulk_delete", + "saved_object:synthetics-private-location/share_to_space", "saved_object:synthetics-privates-locations/bulk_get", "saved_object:synthetics-privates-locations/get", "saved_object:synthetics-privates-locations/find", @@ -9417,6 +9429,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:uptime-synthetics-api-key/delete", "saved_object:uptime-synthetics-api-key/bulk_delete", "saved_object:uptime-synthetics-api-key/share_to_space", + "saved_object:synthetics-private-location/bulk_get", + "saved_object:synthetics-private-location/get", + "saved_object:synthetics-private-location/find", + "saved_object:synthetics-private-location/open_point_in_time", + "saved_object:synthetics-private-location/close_point_in_time", + "saved_object:synthetics-private-location/create", + "saved_object:synthetics-private-location/bulk_create", + "saved_object:synthetics-private-location/update", + "saved_object:synthetics-private-location/bulk_update", + "saved_object:synthetics-private-location/delete", + "saved_object:synthetics-private-location/bulk_delete", + "saved_object:synthetics-private-location/share_to_space", "saved_object:synthetics-privates-locations/bulk_get", "saved_object:synthetics-privates-locations/get", "saved_object:synthetics-privates-locations/find",