[Security Solution] Add new fields to the data quality dashboard persistence (#180691)

## Summary

issue: https://github.com/elastic/kibana/issues/180680

Adds the `checkedBy` and `indexPattern` properties to the data quality
dashboard data stream mapping.
- The `checkedBy` information is retrieved from the `currentUser` uuid
in the server API.
- The `indexPattern` is sent from the UI.

This is a preparation PR for the Historical data quality checks page we
plan to introduce in 8.15. We will need these new fields stored in the
results data stream to implement the new page. So we should start
indexing these entries as soon as possible.

The `indexPattern` will be needed to group individual index results, and
the `checkedBy` user information will be displayed in the summary.

### Screenshots

Before:


![before](3660a1fa-6c62-4ed4-a2a7-c668f6687dad)

After:

![after](4337942c-8a33-4068-bf85-d1c5b413d414)

The `indexPattern` is set according to the pattern used by the UI to
group the indices:


![pattern](75ab100c-898c-4bd6-9d76-cab3f8621451)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Sergi Massaneda 2024-04-15 19:44:32 +02:00 committed by GitHub
parent b41748436a
commit 9a9fd497c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 56 additions and 10 deletions

View file

@ -461,6 +461,7 @@ export const RESULTS_API_ROUTE = '/internal/ecs_data_quality_dashboard/results';
export interface StorageResult {
batchId: string;
indexName: string;
indexPattern: string;
isCheckAll: boolean;
checkedAt: number;
docsCount: number;
@ -491,6 +492,7 @@ export const formatStorageResult = ({
}): StorageResult => ({
batchId: report.batchId,
indexName: result.indexName,
indexPattern: result.pattern,
isCheckAll: report.isCheckAll,
checkedAt: result.checkedAt ?? Date.now(),
docsCount: result.docsCount ?? 0,

View file

@ -10,8 +10,10 @@ import type { FieldMap } from '@kbn/data-stream-adapter';
export const resultsFieldMap: FieldMap = {
batchId: { type: 'keyword', required: true },
indexName: { type: 'keyword', required: true },
indexPattern: { type: 'keyword', required: true },
isCheckAll: { type: 'boolean', required: true },
checkedAt: { type: 'date', required: true },
checkedBy: { type: 'keyword', required: true },
docsCount: { type: 'long', required: true },
totalFieldCount: { type: 'long', required: true },
ecsFieldCount: { type: 'long', required: true },

View file

@ -14,6 +14,8 @@ import { loggerMock, type MockedLogger } from '@kbn/logging-mocks';
import type { WriteResponseBase } from '@elastic/elasticsearch/lib/api/types';
import { resultDocument } from './results.mock';
import type { CheckIndicesPrivilegesParam } from './privileges';
import type { AuthenticatedUser } from '@kbn/core-security-common';
import { API_CURRENT_USER_ERROR_MESSAGE } from '../../translations';
const mockCheckIndicesPrivileges = jest.fn(({ indices }: CheckIndicesPrivilegesParam) =>
Promise.resolve(Object.fromEntries(indices.map((index) => [index, true])))
@ -23,6 +25,8 @@ jest.mock('./privileges', () => ({
mockCheckIndicesPrivileges(params),
}));
const USER_PROFILE_UID = 'mocked_profile_uid';
describe('postResultsRoute route', () => {
describe('indexation', () => {
let server: ReturnType<typeof serverMock.create>;
@ -46,6 +50,9 @@ describe('postResultsRoute route', () => {
context.core.elasticsearch.client.asInternalUser.indices.get.mockResolvedValue({
[resultDocument.indexName]: {},
});
context.core.security.authc.getCurrentUser.mockReturnValue({
profile_uid: USER_PROFILE_UID,
} as AuthenticatedUser);
postResultsRoute(server.router, logger);
});
@ -55,7 +62,7 @@ describe('postResultsRoute route', () => {
const response = await server.inject(req, requestContextMock.convertContext(context));
expect(mockIndex).toHaveBeenCalledWith({
body: { ...resultDocument, '@timestamp': expect.any(Number) },
body: { ...resultDocument, '@timestamp': expect.any(Number), checkedBy: USER_PROFILE_UID },
index: await context.dataQualityDashboard.getResultsIndexName(),
});
@ -86,6 +93,14 @@ describe('postResultsRoute route', () => {
expect(response.status).toEqual(500);
expect(response.body).toEqual({ message: errorMessage, status_code: 500 });
});
it('handles current user retrieval error', async () => {
context.core.security.authc.getCurrentUser.mockReturnValueOnce(null);
const response = await server.inject(req, requestContextMock.convertContext(context));
expect(response.status).toEqual(500);
expect(response.body).toEqual({ message: API_CURRENT_USER_ERROR_MESSAGE, status_code: 500 });
});
});
describe('request index authorization', () => {
@ -107,6 +122,9 @@ describe('postResultsRoute route', () => {
({ context } = requestContextMock.createTools());
context.core.security.authc.getCurrentUser.mockReturnValue({
profile_uid: USER_PROFILE_UID,
} as AuthenticatedUser);
context.core.elasticsearch.client.asInternalUser.indices.get.mockResolvedValue({
[resultDocument.indexName]: {},
});

View file

@ -11,7 +11,7 @@ import { RESULTS_ROUTE_PATH, INTERNAL_API_VERSION } from '../../../common/consta
import { buildResponse } from '../../lib/build_response';
import { buildRouteValidation } from '../../schemas/common';
import { PostResultBody } from '../../schemas/result';
import { API_DEFAULT_ERROR_MESSAGE } from '../../translations';
import { API_CURRENT_USER_ERROR_MESSAGE, API_DEFAULT_ERROR_MESSAGE } from '../../translations';
import type { DataQualityDashboardRequestHandlerContext } from '../../types';
import { checkIndicesPrivileges } from './privileges';
import { API_RESULTS_INDEX_NOT_AVAILABLE } from './translations';
@ -33,6 +33,7 @@ export const postResultsRoute = (
},
async (context, request, response) => {
const services = await context.resolve(['core', 'dataQualityDashboard']);
const resp = buildResponse(response);
let index: string;
@ -46,6 +47,14 @@ export const postResultsRoute = (
});
}
const currentUser = services.core.security.authc.getCurrentUser();
if (!currentUser) {
return resp.error({
body: API_CURRENT_USER_ERROR_MESSAGE,
statusCode: 500,
});
}
try {
const { client } = services.core.elasticsearch;
const { indexName } = request.body;
@ -70,7 +79,11 @@ export const postResultsRoute = (
}
// Index the result
const body = { '@timestamp': Date.now(), ...request.body };
const body = {
...request.body,
'@timestamp': Date.now(),
checkedBy: currentUser.profile_uid,
};
const outcome = await client.asInternalUser.index({ index, body });
return response.ok({ body: { result: outcome.result } });

View file

@ -9,12 +9,6 @@ import type { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/a
import { requestContextMock } from '../../__mocks__/request_context';
import { checkIndicesPrivileges } from './privileges';
// const mockHasPrivileges =
// context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges;
// mockHasPrivileges.mockResolvedValueOnce({
// has_all_requested: true,
// } as unknown as SecurityHasPrivilegesResponse);
describe('checkIndicesPrivileges', () => {
const { context } = requestContextMock.createTools();
const { client } = context.core.elasticsearch;

View file

@ -10,8 +10,10 @@ import type { ResultDocument } from '../../schemas/result';
export const resultDocument: ResultDocument = {
batchId: '33d95427-1fd3-43c3-bdeb-74324533a31e',
indexName: '.ds-logs-endpoint.alerts-default-2023.11.23-000001',
indexPattern: 'logs-endpoint.alerts-*',
isCheckAll: false,
checkedAt: 1706526408000,
checkedBy: 'user_uuid_1',
docsCount: 100,
totalFieldCount: 1582,
ecsFieldCount: 677,

View file

@ -7,7 +7,7 @@
import * as t from 'io-ts';
export const ResultDocument = t.type({
const ResultDocumentInterface = t.interface({
batchId: t.string,
indexName: t.string,
isCheckAll: t.boolean,
@ -28,6 +28,13 @@ export const ResultDocument = t.type({
indexId: t.string,
error: t.union([t.string, t.null]),
});
const ResultDocumentOptional = t.partial({
indexPattern: t.string,
checkedBy: t.string,
});
export const ResultDocument = t.intersection([ResultDocumentInterface, ResultDocumentOptional]);
export type ResultDocument = t.TypeOf<typeof ResultDocument>;
export const PostResultBody = ResultDocument;

View file

@ -13,3 +13,10 @@ export const API_DEFAULT_ERROR_MESSAGE = i18n.translate(
defaultMessage: 'Internal Server Error',
}
);
export const API_CURRENT_USER_ERROR_MESSAGE = i18n.translate(
'xpack.ecsDataQualityDashboard.api.currentUserErrorMessage',
{
defaultMessage: 'Unable to retrieve current user',
}
);

View file

@ -25,6 +25,7 @@
"@kbn/spaces-plugin",
"@kbn/core-elasticsearch-server-mocks",
"@kbn/core-elasticsearch-server",
"@kbn/core-security-common",
],
"exclude": [
"target/**/*",