-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Beats Management] APIs: Remove tag(s) from beat(s) (#19440)
* Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Renaming * Use destructuring * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Implementing `POST /api/beats/agents_tags/removals` API * Updating ES archive * Use destructuring * Moving start of script to own line to increase readability * Nothing to remove if there are no existing tags! * Updating tests to match changes in bulk update painless script * Use destructuring
- Loading branch information
1 parent
035f527
commit 2fa2611
Showing
7 changed files
with
427 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
x-pack/plugins/beats/server/routes/api/register_remove_tags_from_beats_route.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import Joi from 'joi'; | ||
import { | ||
get, | ||
flatten, | ||
uniq | ||
} from 'lodash'; | ||
import { INDEX_NAMES } from '../../../common/constants'; | ||
import { callWithRequestFactory } from '../../lib/client'; | ||
import { wrapEsError } from '../../lib/error_wrappers'; | ||
|
||
async function getDocs(callWithRequest, ids) { | ||
const params = { | ||
index: INDEX_NAMES.BEATS, | ||
type: '_doc', | ||
body: { ids }, | ||
_source: false | ||
}; | ||
|
||
const response = await callWithRequest('mget', params); | ||
return get(response, 'docs', []); | ||
} | ||
|
||
function getBeats(callWithRequest, beatIds) { | ||
const ids = beatIds.map(beatId => `beat:${beatId}`); | ||
return getDocs(callWithRequest, ids); | ||
} | ||
|
||
function getTags(callWithRequest, tags) { | ||
const ids = tags.map(tag => `tag:${tag}`); | ||
return getDocs(callWithRequest, ids); | ||
} | ||
|
||
async function findNonExistentItems(callWithRequest, items, getFn) { | ||
const itemsFromEs = await getFn.call(null, callWithRequest, items); | ||
return itemsFromEs.reduce((nonExistentItems, itemFromEs, idx) => { | ||
if (!itemFromEs.found) { | ||
nonExistentItems.push(items[idx]); | ||
} | ||
return nonExistentItems; | ||
}, []); | ||
} | ||
|
||
function findNonExistentBeatIds(callWithRequest, beatIds) { | ||
return findNonExistentItems(callWithRequest, beatIds, getBeats); | ||
} | ||
|
||
function findNonExistentTags(callWithRequest, tags) { | ||
return findNonExistentItems(callWithRequest, tags, getTags); | ||
} | ||
|
||
async function persistRemovals(callWithRequest, removals) { | ||
const body = flatten(removals.map(({ beatId, tag }) => { | ||
const script = '' | ||
+ 'def beat = ctx._source.beat; ' | ||
+ 'if (beat.tags != null) { ' | ||
+ ' beat.tags.removeAll([params.tag]); ' | ||
+ '}'; | ||
|
||
return [ | ||
{ update: { _id: `beat:${beatId}` } }, | ||
{ script: { source: script, params: { tag } } } | ||
]; | ||
})); | ||
|
||
const params = { | ||
index: INDEX_NAMES.BEATS, | ||
type: '_doc', | ||
body, | ||
refresh: 'wait_for' | ||
}; | ||
|
||
const response = await callWithRequest('bulk', params); | ||
return get(response, 'items', []) | ||
.map((item, resultIdx) => ({ | ||
status: item.update.status, | ||
result: item.update.result, | ||
idxInRequest: removals[resultIdx].idxInRequest | ||
})); | ||
} | ||
|
||
function addNonExistentItemRemovalsToResponse(response, removals, nonExistentBeatIds, nonExistentTags) { | ||
removals.forEach(({ beat_id: beatId, tag }, idx) => { | ||
const isBeatNonExistent = nonExistentBeatIds.includes(beatId); | ||
const isTagNonExistent = nonExistentTags.includes(tag); | ||
|
||
if (isBeatNonExistent && isTagNonExistent) { | ||
response.removals[idx].status = 404; | ||
response.removals[idx].result = `Beat ${beatId} and tag ${tag} not found`; | ||
} else if (isBeatNonExistent) { | ||
response.removals[idx].status = 404; | ||
response.removals[idx].result = `Beat ${beatId} not found`; | ||
} else if (isTagNonExistent) { | ||
response.removals[idx].status = 404; | ||
response.removals[idx].result = `Tag ${tag} not found`; | ||
} | ||
}); | ||
} | ||
|
||
function addRemovalResultsToResponse(response, removalResults) { | ||
removalResults.forEach(removalResult => { | ||
const { idxInRequest, status, result } = removalResult; | ||
response.removals[idxInRequest].status = status; | ||
response.removals[idxInRequest].result = result; | ||
}); | ||
} | ||
|
||
// TODO: add license check pre-hook | ||
// TODO: write to Kibana audit log file | ||
export function registerRemoveTagsFromBeatsRoute(server) { | ||
server.route({ | ||
method: 'POST', | ||
path: '/api/beats/agents_tags/removals', | ||
config: { | ||
validate: { | ||
payload: Joi.object({ | ||
removals: Joi.array().items(Joi.object({ | ||
beat_id: Joi.string().required(), | ||
tag: Joi.string().required() | ||
})) | ||
}).required() | ||
} | ||
}, | ||
handler: async (request, reply) => { | ||
const callWithRequest = callWithRequestFactory(server, request); | ||
|
||
const { removals } = request.payload; | ||
const beatIds = uniq(removals.map(removal => removal.beat_id)); | ||
const tags = uniq(removals.map(removal => removal.tag)); | ||
|
||
const response = { | ||
removals: removals.map(() => ({ status: null })) | ||
}; | ||
|
||
try { | ||
// Handle removals containing non-existing beat IDs or tags | ||
const nonExistentBeatIds = await findNonExistentBeatIds(callWithRequest, beatIds); | ||
const nonExistentTags = await findNonExistentTags(callWithRequest, tags); | ||
|
||
addNonExistentItemRemovalsToResponse(response, removals, nonExistentBeatIds, nonExistentTags); | ||
|
||
const validRemovals = removals | ||
.map((removal, idxInRequest) => ({ | ||
beatId: removal.beat_id, | ||
tag: removal.tag, | ||
idxInRequest // so we can add the result of this removal to the correct place in the response | ||
})) | ||
.filter((removal, idx) => response.removals[idx].status === null); | ||
|
||
if (validRemovals.length > 0) { | ||
const removalResults = await persistRemovals(callWithRequest, validRemovals); | ||
addRemovalResultsToResponse(response, removalResults); | ||
} | ||
} catch (err) { | ||
return reply(wrapEsError(err)); | ||
} | ||
|
||
reply(response); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.