Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,20 @@ export const TAGS = ['serverless', 'alerting', 'uiam-api-key-provisioning', 'bac

export const GET_RULES_BATCH_SIZE = 300;
export const GET_STATUS_BATCH_SIZE = 500;

/**
* UIAM convert error codes that indicate a permanent failure: rules with any of
* these codes persisted on their `uiam_api_keys_provisioning_status` doc 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,
];
/**
* 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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

import { savedObjectsRepositoryMock } from '@kbn/core/server/mocks';
import { UIAM_API_KEYS_PROVISIONING_STATUS_SAVED_OBJECT_TYPE } from '../../saved_objects';
import { NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE } from '../constants';
import {
API_KEY_CREATOR_NOT_ORG_MEMBER_ERROR_CODE,
NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE,
PERMANENT_UIAM_CONVERSION_ERROR_CODES,
} from '../constants';
import {
UiamApiKeyProvisioningStatus,
UiamApiKeyProvisioningEntityType,
Expand Down Expand Up @@ -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: [
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@
import type { SavedObjectsClientContract } from '@kbn/core/server';
import { nodeBuilder, nodeTypes, type KueryNode } from '@kbn/es-query';
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,
EXCLUDE_FILTER_CLAUSE_BATCH_SIZE,
GET_STATUS_BATCH_SIZE,
PERMANENT_UIAM_CONVERSION_ERROR_CODES,
} from '../constants';
import {
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
Expand All @@ -31,7 +35,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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ 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.
* UIAM convert error codes that indicate a permanent failure: tasks with any of
* these codes persisted on their `uiam_api_keys_provisioning_status` doc 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,
];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we already have a package @kbn/uiam-api-keys-provisioning-status, shall we consolidate these codes there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — done in ba5d286. The two codes plus PERMANENT_UIAM_CONVERSION_ERROR_CODES now live in @kbn/uiam-api-keys-provisioning-status alongside the status/entity enums; the alerting and task_manager plugins import them from there.


export const TAGS = ['serverless', 'task-manager', 'uiam-api-key-provisioning', 'background-task'];
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import {
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 {
API_KEY_CREATOR_NOT_ORG_MEMBER_ERROR_CODE,
GET_STATUS_BATCH_SIZE,
NON_CLOUD_USER_API_KEY_CREATOR_ERROR_CODE,
PERMANENT_UIAM_CONVERSION_ERROR_CODES,
} 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';

Expand Down Expand Up @@ -66,7 +71,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: [
Expand All @@ -76,43 +138,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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import {
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, PERMANENT_UIAM_CONVERSION_ERROR_CODES } 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`.
Expand All @@ -32,7 +33,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),
Expand Down
Loading