mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:  After:  The `indexPattern` is set according to the pattern used by the UI to group the indices:  --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b41748436a
commit
9a9fd497c1
9 changed files with 56 additions and 10 deletions
|
@ -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,
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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]: {},
|
||||
});
|
||||
|
|
|
@ -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 } });
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"@kbn/spaces-plugin",
|
||||
"@kbn/core-elasticsearch-server-mocks",
|
||||
"@kbn/core-elasticsearch-server",
|
||||
"@kbn/core-security-common",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue