Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete legacy URL aliases when objects are deleted or unshared #117056

Conversation

jportner
Copy link
Contributor

@jportner jportner commented Nov 1, 2021

Resolves #116235.

⚠️ This PR is best reviewed on a per-commit basis! ⚠️

Overview

Whenever a Saved Objects Client operation causes an existing legacy URL alias to no longer having a valid target, the alias should be deleted.

There are three situations where a saved object can be removed from a space in Kibana:

  1. The entire space is deleted (Improve space deletion cleanup #115422)
  2. The object is deleted
  3. The object is unshared from the space

This PR aims to address (2) and (3) above.

Implementation

I introduced a new deleteLegacyUrlAliases module in Core. This has to be able to delete an alias given an object type, ID, and the space(s) that the object has been removed from. However, if an object is shared, it may have previously existed in numerous spaces and/or "*" (all current and future spaces).

Therefore the alias deletion cannot be accomplished with a simple delete or even a bulk operation, we need to query for any "inbound aliases" for a given object in a given set of spaces. The implementation uses the Elasticsearch updateByQuery API under the hood, which supports three operations: update, delete, and noop. We only need to use delete and noop.

There are two consumers of this new module:

  1. SavedObjectsRepository.delete
  2. updateObjectsSpaces

This module is optimized in a few ways:

  1. The query uses a single painless script which is cached by Elasticsearch
  2. The query does not wait for an index refresh, as this is not important; the index will be refreshed on its own within ~30s which is enough for our needs (we just want to make sure that the alias isn't around so that it doesn't interfere with later create/share API calls)
  3. In the updateObjectsSpaces module, we may be potentially updating spaces for dozens or hundreds of different saved objects at the same time, but we potentially have to call deleteLegacyUrlAliases for each one; as such, we use p-map to limit the concurrency of deleteLegacyUrlAliases calls to 10 at a time (we've done this before in the import API)

Testing

Functional tests have been added to test both "inclusive" and "exclusive" alias deletion in both consumers listed above.

Manual testing for this case is not too straightforward, but you could accomplish it by 1. generating staging data for objects and aliases, 2. adding it to Kibana, 3. calling the HTTP APIs for delete or updateObjectsSpaces for some of the objects, then 4. use Dev Tools to check and confirm the aliases were deleted as expected.

@jportner jportner added Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! v8.0.0 release_note:skip Skip the PR/issue when compiling release notes v8.1.0 Feature:Security/Sharing Saved Objects Platform Security - Sharing Saved Objects feature labels Nov 1, 2021
Also needed to update the legacy-url-alias type mapping to make the
query work.
@jportner jportner force-pushed the issue-116235-disable-aliases-after-delete-or-unshare branch from bd79e83 to 1be71e8 Compare November 2, 2021 21:08
@jportner jportner force-pushed the issue-116235-disable-aliases-after-delete-or-unshare branch from 1be71e8 to 4236bb9 Compare November 2, 2021 21:10
@jportner jportner marked this pull request as ready for review November 3, 2021 02:53
@jportner jportner requested review from a team as code owners November 3, 2021 02:53
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-security (Team:Security)

Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. A few minor comments

Comment on lines +59 to +62
// Legacy URL aliases cannot exist in the default space; filter that out
const filteredNamespaces = namespaces.filter(
(namespace) => namespace !== DEFAULT_NAMESPACE_STRING
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't even aware of that tbh...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, aliases are only created when objects in non-default spaces are converted, and I also designed resolve with that assumption in mind. Technically we could have chosen to support aliases in the default space (with a default: prefix in the raw ID) but there wasn't any practical reason to do so.

Comment on lines +297 to +305
deleteLegacyUrlAliases({
mappings,
registry,
client,
getIndexForType,
type,
id,
namespaces,
deleteBehavior,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a shame we can't bulkDeleteLegacyUrlAliases, but given that namespaces and deleteBehavior can be different per-object, I'm not even sure this would be doable with a single query/script.

Copy link
Contributor Author

@jportner jportner Nov 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I thought about it for a while and I just don't see a way to do this with a single query, unless we passed in tons of painless script parameters (a map that has different values for each object type:id), and I'm not sure how that would perform.

Since deleting and unsharing objects should happen infrequently, I think it's OK to make individual calls to deleteLegacyUrlAliases, and a simple query and script is much safer and more reliable than a big complex one.

@@ -53,8 +53,8 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S
// @ts-expect-error @elastic/elasticsearch doesn't defined `count.buckets`.
const buckets = response.aggregations?.count.buckets;

// The test fixture contains three legacy URL aliases:
// (1) one for "space_1", (2) one for "space_2", and (3) one for "other_space", which is a non-existent space.
// The test fixture contains six legacy URL aliases:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ron-burgundy-escalated-quickly


if (expectAliasDifference !== undefined) {
// if we deleted an object that had an alias pointing to it, the alias should have been deleted as well
await es.indices.refresh({ index: '.kibana' }); // alias deletion uses refresh: false, so we need to manually refresh the index before searching
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT/optional: I think we could refresh only once if objects.find((obj) => obj.expectAliasDifference !== undefined)

{ ignore: [404] }
);
} catch (err) {
const { error } = err.body;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're handling expected ES errors here, but an unexpected error may not have a body property, or that body property may not have an error property. If we encounter an error during this error handling, then we'll lose the origial error context and have a difficult time tracking down exactly what happened

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, I found this function that the core ES client module provides:

/**
* Returns a debug message from an Elasticsearch error in the following format:
* [error type] error reason
*/
export function getErrorMessage(error: errors.ElasticsearchClientError): string {
if (error instanceof errors.ResponseError) {
const errorBody = error.meta.body as ElasticsearchErrorDetails;
return `[${errorBody?.error?.type}]: ${errorBody?.error?.reason ?? error.message}`;
}
return `[${error.name}]: ${error.message}`;
}

I'll reuse it here

}).catch((err) => {
// The object has already been unshared, but we caught an error when attempting to delete aliases.
// A consumer cannot attempt to unshare the object again, so just log the error and swallow it.
logger.error(err.message);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this error have more context?

Suggested change
logger.error(err.message);
logger.error(`Failed to delete legacy URL aliases: ${err.message}`);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error originates from deleteLegacyUrlAliases, and those error messages are pretty detailed.
Are you suggesting we should change this message to mention that this is happening during updateObjectsSpaces?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error originates from deleteLegacyUrlAliases, and those error messages are pretty detailed. Are you suggesting we should change this message to mention that this is happening during updateObjectsSpaces?

I agree that the expected error messages are pretty detailed, and there is currently little room for unexpected errors to occur the way that deleteLegacyUrlAliases is constructed. Yeah I think mentioning that this is happening during updateObjectsSpaces would be sufficient. It gives us another piece of "grepable" text in the error message should we ever have to trace this in production

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, done for both consumers in ed54e64

Copy link
Member

@legrego legrego left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM on green CI!

@jportner jportner added the auto-backport Deprecated - use backport:version if exact versions are needed label Nov 4, 2021
@jportner jportner enabled auto-merge (squash) November 4, 2021 01:33
@jportner jportner merged commit 2e9d0c0 into elastic:main Nov 4, 2021
@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Saved Objects .kibana field count

Every field in each saved object type adds overhead to Elasticsearch. Kibana needs to keep the total field count below Elasticsearch's default limit of 1000 fields. Only specify field mappings for the fields you wish to search on or query. See https://www.elastic.co/guide/en/kibana/master/development-plugin-saved-objects.html#_mappings

id before after diff
legacy-url-alias 6 7 +1

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@kibanamachine
Copy link
Contributor

💚 Backport successful

Status Branch Result
8.0

This backport PR will be merged automatically after passing CI.

kibanamachine added a commit that referenced this pull request Nov 4, 2021
@jportner jportner deleted the issue-116235-disable-aliases-after-delete-or-unshare branch November 9, 2021 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-backport Deprecated - use backport:version if exact versions are needed Feature:Security/Sharing Saved Objects Platform Security - Sharing Saved Objects feature release_note:skip Skip the PR/issue when compiling release notes Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! v8.0.0 v8.1.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Disable aliases when an object is deleted or unshared
6 participants