diff --git a/x-pack/platform/packages/shared/kbn-uiam-api-keys-provisioning-status/index.ts b/x-pack/platform/packages/shared/kbn-uiam-api-keys-provisioning-status/index.ts index 0629e9f64f94d..29e24e3481ba5 100644 --- a/x-pack/platform/packages/shared/kbn-uiam-api-keys-provisioning-status/index.ts +++ b/x-pack/platform/packages/shared/kbn-uiam-api-keys-provisioning-status/index.ts @@ -9,3 +9,9 @@ export { UiamApiKeyProvisioningEntityType, UiamApiKeyProvisioningStatus, } from './src/uiam_api_key_provisioning_status'; + +export { + API_KEY_CREATOR_NOT_ORG_MEMBER_ERROR_CODE, + NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE, + PERMANENT_UIAM_CONVERSION_ERROR_CODES, +} from './src/uiam_conversion_error_codes'; diff --git a/x-pack/platform/packages/shared/kbn-uiam-api-keys-provisioning-status/src/uiam_conversion_error_codes.ts b/x-pack/platform/packages/shared/kbn-uiam-api-keys-provisioning-status/src/uiam_conversion_error_codes.ts new file mode 100644 index 0000000000000..ba177364c9dd9 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-uiam-api-keys-provisioning-status/src/uiam_conversion_error_codes.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * UIAM convert error codes that indicate a permanent failure: rules or tasks + * whose `uiam_api_keys_provisioning_status` doc carries any of these codes are + * excluded from future provisioning attempts. + * + * Source: https://github.com/elastic/uiam/blob/main/modules/domain/src/main/java/co/elastic/cloud/uiam/domain/errors/ErrorCode.java + */ +export const NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE = '0x357391'; +export const API_KEY_CREATOR_NOT_ORG_MEMBER_ERROR_CODE = '0xBE2B58'; + +export const PERMANENT_UIAM_CONVERSION_ERROR_CODES: readonly string[] = [ + NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE, + API_KEY_CREATOR_NOT_ORG_MEMBER_ERROR_CODE, +]; diff --git a/x-pack/platform/plugins/shared/alerting/server/provisioning/constants.ts b/x-pack/platform/plugins/shared/alerting/server/provisioning/constants.ts index de5974491ff99..f6d7287ac1508 100644 --- a/x-pack/platform/plugins/shared/alerting/server/provisioning/constants.ts +++ b/x-pack/platform/plugins/shared/alerting/server/provisioning/constants.ts @@ -18,7 +18,6 @@ export const TAGS = ['serverless', 'alerting', 'uiam-api-key-provisioning', 'bac export const GET_RULES_BATCH_SIZE = 300; export const GET_STATUS_BATCH_SIZE = 500; -export const NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE = '0x357391'; /** * Max number of rule IDs per KQL `or` clause when building the exclude filter. * Keeps each bool.should below Elasticsearch's indices.query.bool.max_clause_count (default 4096). diff --git a/x-pack/platform/plugins/shared/alerting/server/provisioning/lib/get_exclude_rules_filter.test.ts b/x-pack/platform/plugins/shared/alerting/server/provisioning/lib/get_exclude_rules_filter.test.ts index 98b98c435ffe4..99ccc6b245c73 100644 --- a/x-pack/platform/plugins/shared/alerting/server/provisioning/lib/get_exclude_rules_filter.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/provisioning/lib/get_exclude_rules_filter.test.ts @@ -6,8 +6,12 @@ */ import { savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; +import { + API_KEY_CREATOR_NOT_ORG_MEMBER_ERROR_CODE, + NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE, + PERMANENT_UIAM_CONVERSION_ERROR_CODES, +} from '@kbn/uiam-api-keys-provisioning-status'; import { UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE } from '../../saved_objects'; -import { NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE } from '../constants'; import { UiamApiKeyProvisioningStatus, UiamApiKeyProvisioningEntityType, @@ -67,7 +71,64 @@ describe('getExcludeRulesFilter', () => { expect(orNode.arguments).toHaveLength(2); }); - it('includes failed non-Cloud-user conversion errors in the exclusion query', async () => { + it.each(PERMANENT_UIAM_CONVERSION_ERROR_CODES)( + 'includes failed permanent UIAM conversion error code %s in the exclusion query', + async (errorCode) => { + const client = savedObjectsRepositoryMock.create(); + client.find.mockResolvedValue({ + saved_objects: [ + createStatusSavedObject( + 'rule-1', + UiamApiKeyProvisioningStatus.FAILED, + undefined, + errorCode + ), + ], + total: 1, + per_page: 500, + page: 1, + }); + + const result = await getExcludeRulesFilter(client); + + expect(result).toBeDefined(); + const findFilter = client.find.mock.calls[0][0].filter; + expect(findFilter.function).toBe('and'); + expect(findFilter.arguments[0].function).toBe('or'); + expect(findFilter.arguments[0].arguments).toContainEqual( + expect.objectContaining({ + function: 'and', + arguments: expect.arrayContaining([ + expect.objectContaining({ + function: 'is', + arguments: expect.arrayContaining([ + expect.objectContaining({ + value: `${UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE}.attributes.status`, + }), + expect.objectContaining({ value: UiamApiKeyProvisioningStatus.FAILED }), + ]), + }), + expect.objectContaining({ + function: 'or', + arguments: expect.arrayContaining([ + expect.objectContaining({ + function: 'is', + arguments: expect.arrayContaining([ + expect.objectContaining({ + value: `${UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE}.attributes.errorCode`, + }), + expect.objectContaining({ value: errorCode }), + ]), + }), + ]), + }), + ]), + }) + ); + } + ); + + it('ors all permanent UIAM conversion error codes inside the failed branch', async () => { const client = savedObjectsRepositoryMock.create(); client.find.mockResolvedValue({ saved_objects: [ @@ -77,45 +138,31 @@ describe('getExcludeRulesFilter', () => { undefined, NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE ), + createStatusSavedObject( + 'rule-2', + UiamApiKeyProvisioningStatus.FAILED, + undefined, + API_KEY_CREATOR_NOT_ORG_MEMBER_ERROR_CODE + ), ], - total: 1, + total: 2, per_page: 500, page: 1, }); - const result = await getExcludeRulesFilter(client); - - expect(result).toBeDefined(); + await getExcludeRulesFilter(client); const findFilter = client.find.mock.calls[0][0].filter; - expect(findFilter.function).toBe('and'); - expect(findFilter.arguments[0].function).toBe('or'); - expect(findFilter.arguments[0].arguments).toContainEqual( - expect.objectContaining({ - function: 'and', - arguments: expect.arrayContaining([ - expect.objectContaining({ - function: 'is', - arguments: expect.arrayContaining([ - expect.objectContaining({ - value: `${UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE}.attributes.status`, - }), - expect.objectContaining({ value: UiamApiKeyProvisioningStatus.FAILED }), - ]), - }), - expect.objectContaining({ - function: 'is', - arguments: expect.arrayContaining([ - expect.objectContaining({ - value: `${UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE}.attributes.errorCode`, - }), - expect.objectContaining({ - value: NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE, - }), - ]), - }), - ]), - }) + const failedBranch = findFilter.arguments[0].arguments.find( + (arg: { function: string }) => arg.function === 'and' + ); + const errorCodeOrNode = failedBranch.arguments.find( + (arg: { function: string }) => arg.function === 'or' + ); + expect(errorCodeOrNode.arguments).toHaveLength(PERMANENT_UIAM_CONVERSION_ERROR_CODES.length); + const codes = errorCodeOrNode.arguments.map( + (arg: { arguments: Array<{ value: string }> }) => arg.arguments[1].value ); + expect(codes.sort()).toEqual([...PERMANENT_UIAM_CONVERSION_ERROR_CODES].sort()); }); it('paginates through multiple pages of status docs', async () => { diff --git a/x-pack/platform/plugins/shared/alerting/server/provisioning/lib/get_exclude_rules_filter.ts b/x-pack/platform/plugins/shared/alerting/server/provisioning/lib/get_exclude_rules_filter.ts index 818e1bed0ec7a..e80f894e513f0 100644 --- a/x-pack/platform/plugins/shared/alerting/server/provisioning/lib/get_exclude_rules_filter.ts +++ b/x-pack/platform/plugins/shared/alerting/server/provisioning/lib/get_exclude_rules_filter.ts @@ -7,19 +7,20 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import { nodeBuilder, nodeTypes, type KueryNode } from '@kbn/es-query'; +import { PERMANENT_UIAM_CONVERSION_ERROR_CODES } from '@kbn/uiam-api-keys-provisioning-status'; import { UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE } from '../../saved_objects'; -import { NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE } from '../constants'; +import { EXCLUDE_FILTER_CLAUSE_BATCH_SIZE, GET_STATUS_BATCH_SIZE } from '../constants'; import { - UiamApiKeyProvisioningStatus, UiamApiKeyProvisioningEntityType, + UiamApiKeyProvisioningStatus, } from '../../saved_objects/schemas/raw_uiam_api_keys_provisioning_status'; import { convertRuleIdsToKueryNode } from '../../lib/convert_rule_ids_to_kuery_node'; -import { GET_STATUS_BATCH_SIZE, EXCLUDE_FILTER_CLAUSE_BATCH_SIZE } from '../constants'; /** * Returns a KQL filter that excludes rules which already have provisioning status - * COMPLETED/SKIPPED or failed due to non-Cloud user API key creator code. - * Returns undefined when there are no such rules (no filter applied). + * COMPLETED/SKIPPED or failed with a permanent UIAM conversion error code (see + * {@link PERMANENT_UIAM_CONVERSION_ERROR_CODES}). Returns undefined when there + * are no such rules (no filter applied). */ export const getExcludeRulesFilter = async ( savedObjectsClient: SavedObjectsClientContract @@ -31,7 +32,11 @@ export const getExcludeRulesFilter = async ( nodeBuilder.is(`${statusAttr}.status`, UiamApiKeyProvisioningStatus.SKIPPED), nodeBuilder.and([ nodeBuilder.is(`${statusAttr}.status`, UiamApiKeyProvisioningStatus.FAILED), - nodeBuilder.is(`${statusAttr}.errorCode`, NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE), + nodeBuilder.or( + PERMANENT_UIAM_CONVERSION_ERROR_CODES.map((code) => + nodeBuilder.is(`${statusAttr}.errorCode`, code) + ) + ), ]), ]), nodeBuilder.is(`${statusAttr}.entityType`, UiamApiKeyProvisioningEntityType.RULE), diff --git a/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/constants.ts b/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/constants.ts index d0a871c3592c7..aec6133b27518 100644 --- a/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/constants.ts +++ b/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/constants.ts @@ -31,10 +31,4 @@ export const FETCH_BATCH_SIZE = 500; */ export const GET_STATUS_BATCH_SIZE = 500; -/** - * UIAM convert error code returned when the Elasticsearch API key creator is not a Cloud user. - * Source: https://github.com/elastic/uiam/blob/main/modules/domain/src/main/java/co/elastic/cloud/uiam/domain/errors/ErrorCode.java - */ -export const NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE = '0x357391'; - export const TAGS = ['serverless', 'task-manager', 'uiam-api-key-provisioning', 'background-task']; diff --git a/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/lib/get_exclude_tasks_filter.test.ts b/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/lib/get_exclude_tasks_filter.test.ts index 00a26db2c39c3..af1b2a166a922 100644 --- a/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/lib/get_exclude_tasks_filter.test.ts +++ b/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/lib/get_exclude_tasks_filter.test.ts @@ -7,10 +7,13 @@ import { savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { + API_KEY_CREATOR_NOT_ORG_MEMBER_ERROR_CODE, + NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE, + PERMANENT_UIAM_CONVERSION_ERROR_CODES, UiamApiKeyProvisioningEntityType, UiamApiKeyProvisioningStatus, } from '@kbn/uiam-api-keys-provisioning-status'; -import { NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE, GET_STATUS_BATCH_SIZE } from '../constants'; +import { GET_STATUS_BATCH_SIZE } from '../constants'; import { UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE } from '../uiam_api_keys_provisioning_status_saved_object'; import { getExcludeTasksFilter } from './get_exclude_tasks_filter'; @@ -66,7 +69,64 @@ describe('getExcludeTasksFilter', () => { expect(result.sort()).toEqual(['t1', 't2']); }); - it('includes failed non-Cloud-user conversion errors in the exclusion query', async () => { + it.each(PERMANENT_UIAM_CONVERSION_ERROR_CODES)( + 'includes failed permanent UIAM conversion error code %s in the exclusion query', + async (errorCode) => { + const client = savedObjectsRepositoryMock.create(); + client.find.mockResolvedValue({ + saved_objects: [ + createStatusSavedObject( + 'task-1', + UiamApiKeyProvisioningStatus.FAILED, + undefined, + errorCode + ), + ], + total: 1, + per_page: 500, + page: 1, + }); + + const result = await getExcludeTasksFilter(client); + expect(result).toBeDefined(); + + const findFilter = client.find.mock.calls[0][0].filter; + expect(findFilter.function).toBe('and'); + expect(findFilter.arguments[0].function).toBe('or'); + expect(findFilter.arguments[0].arguments).toContainEqual( + expect.objectContaining({ + function: 'and', + arguments: expect.arrayContaining([ + expect.objectContaining({ + function: 'is', + arguments: expect.arrayContaining([ + expect.objectContaining({ + value: `${UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE}.attributes.status`, + }), + expect.objectContaining({ value: UiamApiKeyProvisioningStatus.FAILED }), + ]), + }), + expect.objectContaining({ + function: 'or', + arguments: expect.arrayContaining([ + expect.objectContaining({ + function: 'is', + arguments: expect.arrayContaining([ + expect.objectContaining({ + value: `${UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE}.attributes.errorCode`, + }), + expect.objectContaining({ value: errorCode }), + ]), + }), + ]), + }), + ]), + }) + ); + } + ); + + it('ors all permanent UIAM conversion error codes inside the failed branch', async () => { const client = savedObjectsRepositoryMock.create(); client.find.mockResolvedValue({ saved_objects: [ @@ -76,43 +136,31 @@ describe('getExcludeTasksFilter', () => { undefined, NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE ), + createStatusSavedObject( + 'task-2', + UiamApiKeyProvisioningStatus.FAILED, + undefined, + API_KEY_CREATOR_NOT_ORG_MEMBER_ERROR_CODE + ), ], - total: 1, + total: 2, per_page: 500, page: 1, }); - const result = await getExcludeTasksFilter(client); - expect(result).toBeDefined(); - + await getExcludeTasksFilter(client); const findFilter = client.find.mock.calls[0][0].filter; - expect(findFilter.function).toBe('and'); - expect(findFilter.arguments[0].function).toBe('or'); - expect(findFilter.arguments[0].arguments).toContainEqual( - expect.objectContaining({ - function: 'and', - arguments: expect.arrayContaining([ - expect.objectContaining({ - function: 'is', - arguments: expect.arrayContaining([ - expect.objectContaining({ - value: `${UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE}.attributes.status`, - }), - expect.objectContaining({ value: UiamApiKeyProvisioningStatus.FAILED }), - ]), - }), - expect.objectContaining({ - function: 'is', - arguments: expect.arrayContaining([ - expect.objectContaining({ - value: `${UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE}.attributes.errorCode`, - }), - expect.objectContaining({ value: NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE }), - ]), - }), - ]), - }) + const failedBranch = findFilter.arguments[0].arguments.find( + (arg: { function: string }) => arg.function === 'and' + ); + const errorCodeOrNode = failedBranch.arguments.find( + (arg: { function: string }) => arg.function === 'or' + ); + expect(errorCodeOrNode.arguments).toHaveLength(PERMANENT_UIAM_CONVERSION_ERROR_CODES.length); + const codes = errorCodeOrNode.arguments.map( + (arg: { arguments: Array<{ value: string }> }) => arg.arguments[1].value ); + expect(codes.sort()).toEqual([...PERMANENT_UIAM_CONVERSION_ERROR_CODES].sort()); }); it('paginates through multiple pages of status docs', async () => { diff --git a/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/lib/get_exclude_tasks_filter.ts b/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/lib/get_exclude_tasks_filter.ts index 59e9af0ae7674..57b4e557137d0 100644 --- a/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/lib/get_exclude_tasks_filter.ts +++ b/x-pack/platform/plugins/shared/task_manager/server/uiam_api_key_provisioning/lib/get_exclude_tasks_filter.ts @@ -8,16 +8,18 @@ import type { ISavedObjectsRepository } from '@kbn/core/server'; import { nodeBuilder } from '@kbn/es-query'; import { + PERMANENT_UIAM_CONVERSION_ERROR_CODES, UiamApiKeyProvisioningEntityType, UiamApiKeyProvisioningStatus, } from '@kbn/uiam-api-keys-provisioning-status'; -import { GET_STATUS_BATCH_SIZE, NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE } from '../constants'; +import { GET_STATUS_BATCH_SIZE } from '../constants'; import { UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE } from '../uiam_api_keys_provisioning_status_saved_object'; /** * Returns task `entityId`s that already have a final UIAM provisioning status - * (completed, skipped, or failed due to non-Cloud user API key creator code) so - * the provisioning fetch can exclude them. + * (completed, skipped, or failed with a permanent UIAM conversion error code, + * see {@link PERMANENT_UIAM_CONVERSION_ERROR_CODES}) so the provisioning fetch + * can exclude them. * * Mirrors {@link getExcludeRulesFilter} in * `x-pack/.../alerting/server/provisioning/lib/get_exclude_rules_filter.ts`. @@ -32,7 +34,11 @@ export const getExcludeTasksFilter = async ( nodeBuilder.is(`${statusAttr}.status`, UiamApiKeyProvisioningStatus.SKIPPED), nodeBuilder.and([ nodeBuilder.is(`${statusAttr}.status`, UiamApiKeyProvisioningStatus.FAILED), - nodeBuilder.is(`${statusAttr}.errorCode`, NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE), + nodeBuilder.or( + PERMANENT_UIAM_CONVERSION_ERROR_CODES.map((code) => + nodeBuilder.is(`${statusAttr}.errorCode`, code) + ) + ), ]), ]), nodeBuilder.is(`${statusAttr}.entityType`, UiamApiKeyProvisioningEntityType.TASK),