From 1385203dcecf5053c44adbac498854af32dcdadf Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Wed, 17 Oct 2018 22:06:43 -0400 Subject: [PATCH] Beats Management (#23819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * [Beats Management] Move to Ingest UI arch and initial TS effort (#20039) * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * Ported over base types and arch structure * move management of installIndexTemplate into the framework adapter * ts-lint fix * tslint fixes * more ts tweaks * fix paths * added several working endpoints * add more routes and bug fixes * fix linting * fix type remove CRUFT * remove more cruft * remove more CRUFT * added comments, change plurality * add tsconfig file * add extends path * fixed typo * serveral PR review fixes * fixed lodash type version * “fix” types by applying a lot of any * [Beats Management] Move tokens to use JWT, add more complete test suite (#20317) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * fix broken test, this is beats CM not logstash 😊 * added readme * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * [Beats Management] add more tests, update types, break out ES into it's own adapter (#20566) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * Add initial efforts for backend framework adapter testing * move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES * re-typed * renamed types to match pattern * aditional renames * adapter tests should always just use adapterSetup(); * database now uses InternalRequest * corrected spelling of framework * fix typings * remove CRUFT * RequestOrInternal * Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible * fix tests, add test, removed extra comment * fix auth * updated lock file * [Beats Management] add get beat endpoint (#20603) * [Beats Management] Move tokens to use JWT, add more complete test suite (#20317) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * fix broken test, this is beats CM not logstash 😊 * added readme * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * Add initial efforts for backend framework adapter testing * move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES * re-typed * renamed types to match pattern * aditional renames * adapter tests should always just use adapterSetup(); * database now uses InternalRequest * corrected spelling of framework * fix typings * remove CRUFT * RequestOrInternal * Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible * fix tests, add test, removed extra comment * Moved critical path code from route, to more easeley tested domain * fix auth * remove beat verification, added get beat endpoint to return configs * fix type * update createGetBeatConfigurationRoute URL * rename method * update to match PR #20566 * updated lock file * fix bad merge * update TSLinting * fix bad rebase * [Beats Management] [WIP] Create public resources for management plugin (#20864) * Init plugin public resources. * rename beats to beats_management * rendering react now * Beats/initial ui (#20994) * initial layout and main nav * modal UI and pattern for UI established * fix path * wire up in-memroy adapters * tweak adapters * add getAll method to tags adapter (#21287) * Beats/real adapters (#21481) * add initial real adapters, and nulled data where we need endpoints * UI adapters and needed endpoints added (though not tested) * prep for route tests and some cleanup * move files * [Beats Management] Add BeatsTable/Bulk Action Search Component (#21182) * Add BeatsTable and control bar components. * Clean yarn.lock. * Move raw numbers/strings to constants. Remove obsolete state/props. * Update/add tests. * Change prop name from "items" to "beats". * Rename some variables. * Move search bar filter definitions to table render. * Update table to support assignment options. * Update action control position. * Refactor split render function into custom components. * Beats/basic use cases (#21660) * tweak adapter responses / types. re-add enroll ui * routes enabled, enroll now pings the server * full enrollment path now working * improved pinging for beat enrollment * fix location of history call * reload beats list on beat enrollment completion * [Beats Management] Add Tags List (#21274) * Add BeatsTable and control bar components. * Clean yarn.lock. * Move raw numbers/strings to constants. Remove obsolete state/props. * Update/add tests. * Change prop name from "items" to "beats". * Add TagsTable component and associated search/action bar. * Rename some variables. * Add constant after forgetting to save file. * Fix design mistake in table component. * Disable delete button when no tags selected. * Export tags table from index.ts. * Move search bar filter definitions to table render. * Update table to support assignment options. * Update action control position. * Refactor split render function into custom components. * Add assignment options to Tags List. * Remove obsolete code. * Move tooltips for tag icons to top position. * Beats/update (#21702) * [ML] Fixing issue with historical job audit messages (#21718) * Add proper aria-label for close inspector (#21719) * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * [Beats Management] Move to Ingest UI arch and initial TS effort (#20039) * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * Ported over base types and arch structure * move management of installIndexTemplate into the framework adapter * ts-lint fix * tslint fixes * more ts tweaks * fix paths * added several working endpoints * add more routes and bug fixes * fix linting * fix type remove CRUFT * remove more cruft * remove more CRUFT * added comments, change plurality * add tsconfig file * add extends path * fixed typo * serveral PR review fixes * fixed lodash type version * “fix” types by applying a lot of any * [Beats Management] Move tokens to use JWT, add more complete test suite (#20317) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * fix broken test, this is beats CM not logstash 😊 * added readme * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * [Beats Management] add more tests, update types, break out ES into it's own adapter (#20566) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * Add initial efforts for backend framework adapter testing * move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES * re-typed * renamed types to match pattern * aditional renames * adapter tests should always just use adapterSetup(); * database now uses InternalRequest * corrected spelling of framework * fix typings * remove CRUFT * RequestOrInternal * Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible * fix tests, add test, removed extra comment * fix auth * updated lock file * [Beats Management] add get beat endpoint (#20603) * [Beats Management] Move tokens to use JWT, add more complete test suite (#20317) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * fix broken test, this is beats CM not logstash 😊 * added readme * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * Add initial efforts for backend framework adapter testing * move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES * re-typed * renamed types to match pattern * aditional renames * adapter tests should always just use adapterSetup(); * database now uses InternalRequest * corrected spelling of framework * fix typings * remove CRUFT * RequestOrInternal * Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible * fix tests, add test, removed extra comment * Moved critical path code from route, to more easeley tested domain * fix auth * remove beat verification, added get beat endpoint to return configs * fix type * update createGetBeatConfigurationRoute URL * rename method * update to match PR #20566 * updated lock file * fix bad merge * update TSLinting * fix bad rebase * [Beats Management] [WIP] Create public resources for management plugin (#20864) * Init plugin public resources. * rename beats to beats_management * rendering react now * Beats/initial ui (#20994) * initial layout and main nav * modal UI and pattern for UI established * fix path * wire up in-memroy adapters * tweak adapters * add getAll method to tags adapter (#21287) * Beats/real adapters (#21481) * add initial real adapters, and nulled data where we need endpoints * UI adapters and needed endpoints added (though not tested) * prep for route tests and some cleanup * move files * [Beats Management] Add BeatsTable/Bulk Action Search Component (#21182) * Add BeatsTable and control bar components. * Clean yarn.lock. * Move raw numbers/strings to constants. Remove obsolete state/props. * Update/add tests. * Change prop name from "items" to "beats". * Rename some variables. * Move search bar filter definitions to table render. * Update table to support assignment options. * Update action control position. * Refactor split render function into custom components. * Beats/basic use cases (#21660) * tweak adapter responses / types. re-add enroll ui * routes enabled, enroll now pings the server * full enrollment path now working * improved pinging for beat enrollment * fix location of history call * reload beats list on beat enrollment completion * add update on client side, expand update on server to allow for partial data, and user auth * remove double beat lookup * fix tests * only return active beats * disenroll now working * fig getAll query * re-enrolling a beat will now work * fix types * fix types * update deps * update kibana API for version * [Beats CM] Manage Tags (#21776) * [ML] Fixing issue with historical job audit messages (#21718) * Add proper aria-label for close inspector (#21719) * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * [Beats Management] Move to Ingest UI arch and initial TS effort (#20039) * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * Ported over base types and arch structure * move management of installIndexTemplate into the framework adapter * ts-lint fix * tslint fixes * more ts tweaks * fix paths * added several working endpoints * add more routes and bug fixes * fix linting * fix type remove CRUFT * remove more cruft * remove more CRUFT * added comments, change plurality * add tsconfig file * add extends path * fixed typo * serveral PR review fixes * fixed lodash type version * “fix” types by applying a lot of any * [Beats Management] Move tokens to use JWT, add more complete test suite (#20317) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * fix broken test, this is beats CM not logstash 😊 * added readme * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * [Beats Management] add more tests, update types, break out ES into it's own adapter (#20566) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * Add initial efforts for backend framework adapter testing * move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES * re-typed * renamed types to match pattern * aditional renames * adapter tests should always just use adapterSetup(); * database now uses InternalRequest * corrected spelling of framework * fix typings * remove CRUFT * RequestOrInternal * Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible * fix tests, add test, removed extra comment * fix auth * updated lock file * [Beats Management] add get beat endpoint (#20603) * [Beats Management] Move tokens to use JWT, add more complete test suite (#20317) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * fix broken test, this is beats CM not logstash 😊 * added readme * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * Add initial efforts for backend framework adapter testing * move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES * re-typed * renamed types to match pattern * aditional renames * adapter tests should always just use adapterSetup(); * database now uses InternalRequest * corrected spelling of framework * fix typings * remove CRUFT * RequestOrInternal * Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible * fix tests, add test, removed extra comment * Moved critical path code from route, to more easeley tested domain * fix auth * remove beat verification, added get beat endpoint to return configs * fix type * update createGetBeatConfigurationRoute URL * rename method * update to match PR #20566 * updated lock file * fix bad merge * update TSLinting * fix bad rebase * [Beats Management] [WIP] Create public resources for management plugin (#20864) * Init plugin public resources. * rename beats to beats_management * rendering react now * Beats/initial ui (#20994) * initial layout and main nav * modal UI and pattern for UI established * fix path * wire up in-memroy adapters * tweak adapters * add getAll method to tags adapter (#21287) * Beats/real adapters (#21481) * add initial real adapters, and nulled data where we need endpoints * UI adapters and needed endpoints added (though not tested) * prep for route tests and some cleanup * move files * [Beats Management] Add BeatsTable/Bulk Action Search Component (#21182) * Add BeatsTable and control bar components. * Clean yarn.lock. * Move raw numbers/strings to constants. Remove obsolete state/props. * Update/add tests. * Change prop name from "items" to "beats". * Rename some variables. * Move search bar filter definitions to table render. * Update table to support assignment options. * Update action control position. * Refactor split render function into custom components. * Beats/basic use cases (#21660) * tweak adapter responses / types. re-add enroll ui * routes enabled, enroll now pings the server * full enrollment path now working * improved pinging for beat enrollment * fix location of history call * reload beats list on beat enrollment completion * add update on client side, expand update on server to allow for partial data, and user auth * remove double beat lookup * fix tests * only return active beats * disenroll now working * fig getAll query * re-enrolling a beat will now work * fix types * Add create tags view. * fix types * update deps * update kibana API for version * Added component/config interface for editing/creating tags. Added separate pages for create/edit tags. * Fixup. * Beats/beat tags workflow (#21923) * [Beats Management] Move to Ingest UI arch and initial TS effort (#20039) * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * Ported over base types and arch structure * move management of installIndexTemplate into the framework adapter * ts-lint fix * tslint fixes * more ts tweaks * fix paths * added several working endpoints * add more routes and bug fixes * fix linting * fix type remove CRUFT * remove more cruft * remove more CRUFT * added comments, change plurality * add tsconfig file * add extends path * fixed typo * serveral PR review fixes * fixed lodash type version * “fix” types by applying a lot of any * add details page, re-configure routes * move tag crud to new route stuff * update tag create/edit component api * tags creation now working * bunch of stuff I should have split up better… * fixed perf bug, selected items that are removed are no longer phantom selected * fix rendering of assignments * remove assign to beats, the UX was too poor * Beats/config view (#22177) * [Beats Management] Move to Ingest UI arch and initial TS effort (#20039) * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * Ported over base types and arch structure * move management of installIndexTemplate into the framework adapter * ts-lint fix * tslint fixes * more ts tweaks * fix paths * added several working endpoints * add more routes and bug fixes * fix linting * fix type remove CRUFT * remove more cruft * remove more CRUFT * added comments, change plurality * add tsconfig file * add extends path * fixed typo * serveral PR review fixes * fixed lodash type version * “fix” types by applying a lot of any * add details page, re-configure routes * move tag crud to new route stuff * update tag create/edit component api * tags creation now working * bunch of stuff I should have split up better… * fixed perf bug, selected items that are removed are no longer phantom selected * fix rendering of assignments * remove assign to beats, the UX was too poor * [Beats Management] Move to Ingest UI arch and initial TS effort (#20039) * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * Ported over base types and arch structure * move management of installIndexTemplate into the framework adapter * ts-lint fix * tslint fixes * more ts tweaks * fix paths * added several working endpoints * add more routes and bug fixes * fix linting * fix type remove CRUFT * remove more cruft * remove more CRUFT * added comments, change plurality * add tsconfig file * add extends path * fixed typo * serveral PR review fixes * fixed lodash type version * “fix” types by applying a lot of any * Beats/update (#21702) * [ML] Fixing issue with historical job audit messages (#21718) * Add proper aria-label for close inspector (#21719) * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * [Beats Management] Move to Ingest UI arch and initial TS effort (#20039) * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [Beats Management] APIs: Enroll beat (#19056) * WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [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 * Ported over base types and arch structure * move management of installIndexTemplate into the framework adapter * ts-lint fix * tslint fixes * more ts tweaks * fix paths * added several working endpoints * add more routes and bug fixes * fix linting * fix type remove CRUFT * remove more cruft * remove more CRUFT * added comments, change plurality * add tsconfig file * add extends path * fixed typo * serveral PR review fixes * fixed lodash type version * “fix” types by applying a lot of any * [Beats Management] Move tokens to use JWT, add more complete test suite (#20317) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * fix broken test, this is beats CM not logstash 😊 * added readme * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * [Beats Management] add more tests, update types, break out ES into it's own adapter (#20566) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * Add initial efforts for backend framework adapter testing * move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES * re-typed * renamed types to match pattern * aditional renames * adapter tests should always just use adapterSetup(); * database now uses InternalRequest * corrected spelling of framework * fix typings * remove CRUFT * RequestOrInternal * Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible * fix tests, add test, removed extra comment * fix auth * updated lock file * [Beats Management] add get beat endpoint (#20603) * [Beats Management] Move tokens to use JWT, add more complete test suite (#20317) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * fix broken test, this is beats CM not logstash 😊 * added readme * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * Add initial efforts for backend framework adapter testing * move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES * re-typed * renamed types to match pattern * aditional renames * adapter tests should always just use adapterSetup(); * database now uses InternalRequest * corrected spelling of framework * fix typings * remove CRUFT * RequestOrInternal * Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible * fix tests, add test, removed extra comment * Moved critical path code from route, to more easeley tested domain * fix auth * remove beat verification, added get beat endpoint to return configs * fix type * update createGetBeatConfigurationRoute URL * rename method * update to match PR #20566 * updated lock file * fix bad merge * update TSLinting * fix bad rebase * [Beats Management] [WIP] Create public resources for management plugin (#20864) * Init plugin public resources. * rename beats to beats_management * rendering react now * Beats/initial ui (#20994) * initial layout and main nav * modal UI and pattern for UI established * fix path * wire up in-memroy adapters * tweak adapters * add getAll method to tags adapter (#21287) * Beats/real adapters (#21481) * add initial real adapters, and nulled data where we need endpoints * UI adapters and needed endpoints added (though not tested) * prep for route tests and some cleanup * move files * [Beats Management] Add BeatsTable/Bulk Action Search Component (#21182) * Add BeatsTable and control bar components. * Clean yarn.lock. * Move raw numbers/strings to constants. Remove obsolete state/props. * Update/add tests. * Change prop name from "items" to "beats". * Rename some variables. * Move search bar filter definitions to table render. * Update table to support assignment options. * Update action control position. * Refactor split render function into custom components. * Beats/basic use cases (#21660) * tweak adapter responses / types. re-add enroll ui * routes enabled, enroll now pings the server * full enrollment path now working * improved pinging for beat enrollment * fix location of history call * reload beats list on beat enrollment completion * add update on client side, expand update on server to allow for partial data, and user auth * remove double beat lookup * fix tests * only return active beats * disenroll now working * fig getAll query * re-enrolling a beat will now work * fix types * fix types * update deps * update kibana API for version * progress on config forms * config view inital input types working * ts fixes * fix more ts * code now errors on invalid yaml * remove un-needed include * fix bad rebase * saving config blocks as yaml to db is now working * propperly formatted YAML * loading tags back on edit screen in-progress * fix types * vis name validation for tag * update EUI style * tweak design * fixed tag assignments (still has a ui glitch) * fix form validation on select * fix deps * update deps * attached beats now works in the edit tag screen, edit now disables changing the tag id * better un-parsing of yaml, some elements now rendering to edit config blocks * delete config block now works * fix ability to edit config * fix deps * fix another rebase issue * tweaks and fixes * fix several bugs * Beats/security (#22500) * client side security working * check license on route * forgot a file * [Beats CM] Add beats details pages (#22455) * Move edits from previous details branch. * Add tag view to beat details. * Added notifications for add/remove tag from beat. * Fix dependencies upgrade/downgrade add/removes. * Create new page files for each sub-section of details view. * Move page functionality from dedicated components to pages. * [Beats CM] move to json for configs (#22693) * move to json for configs, fix some tests * tweaks * add fixes (#22711) * add fixes * return only one config * much improved validation for hosts, no longer require not required fields, no longer have JS in the config form config * fix condition argument * [Beats CM] Improve UX for assignment of tag to list of beats (#22687) * Improve UX for assignment of tag to list of beats. * Revert a change. * [Beats CM] Sort beat list tags (#22729) * Add logic to sort tags by ID when rendering in Beats List. * Prefer lodash sortBy over inline sort implementation. * [Beats CM] Remove key warnings from beat list components (#22772) * Add logic to sort tags by ID when rendering in Beats List. * Remove key warnings from beats list and associated components. * Prefer lodash sortBy over inline sort implementation. * [Beats CM] Add check for BeatsPage component to avoid setState when unmounted (#22836) * Add check if component is unmounted when loading Beats List page. * Move call to loadBeats() to componentDidMount lifecycle function. * Update field name in table type config. (#23228) * Utilize TagBadge functional component to standardize display of tag names. (#23253) * [Beats CM] Add beat name to update endpoint (#23291) * Update beat index template and update endpoints to expect name type. Add test file for update. * Update enroll script and endpoint to create a 'name' field for beats. * Add name field to CMBeat domain type. * Update functional tests to include name field. * Fix broken tag assignment functional test. * Edit beats list table config to display most recently-updated tag time. (#23337) * Update beat detail view info. (#23369) * Update beat detail view info. * Add period to end of update field. * [Beat CM] Show only tags associated with selected beat (#23398) * Reenable output config. * Make beat detail tag page only show tags associated with that tag. * Display beat name instead of ID on details screen. (#23410) * [Beat CM] Display config name instead of beat type in beat detail view (#23411) * Update config table in beat detail view to show config type instead of beat type. * Modify Beat Detail view to display human-friendly names for config block types. * [Beats CM] Add password input and re-enable output config schema (#23417) * wip defining controls * Complete adding formsy password field. * Re-enable output config schema definition. * Simplify import/export for formsy components. * Add full list of current Filebeat & Metricbeat module (#23258) This change also renames `Metricbeat input` to `Metricbeat module` * Beats/ui tweaks (#23655) * add initial breadcrumbs * prevent errors * New routing in place for supporting URLState needed for Kuery bar * beats table kuery bar “working” (but not submitting) * pulling activity view from current phase’s spec * setup link now at correct URL * kuery bar * autocomplete bar now working * Add walkthrough (#23785) * add test system, framework, and example tests * adding walkthrough * clicking walkthrough step should take you to that step * try/catch beats loading to prevent unhandeled error when there are no beats * fix walkthrough not redirecting to main beats page * update yarn lock files * fix prettier * fix x-pack package.json formatting * update kbn server creation * remove types from old table implementation * move to Boom.boomify * fix TS errors * fix type * rollback dep version * fix more conflicting dep issues * deps * force correct node type resolution * cleanup and skip tests (kbn test util is broken) * added formsy and basePath * update yarn lock file * add beats management icon * rename beats management main section * Tags now called Configuration Tags in the tabs * tokens must expire at most after 2 weeks * fix bad auto-import * beat details now shows the extra data needed * tweak package.json deps for continuity * update yarn lock for new yarn version * [Beats CM] Re-arch table assignment control definitions (#23663) * Patch changes to latest feature branch. * Update table controls to offer standard search bar if kuery bar props not provided. * Tweaked enrollment UI to inlcude beat type selection * Reduce badge list gutter size, change logic for tag assignment to unassign all if some selected. (#24091) * dont use genaric search bar * removed unused import * fix es-lint error * fix outputs * fix types from infra merge * remove dupe dep * update lock file * push another lock file * Add unfilteredBeats field to beat list state to prevent inappropriate redirect. (#24146) * fix yarn lock because yarn is dumb * Fix broken tests # Conflicts: # yarn.lock --- package.json | 5 +- x-pack/index.js | 2 + x-pack/package.json | 8 +- .../common/constants/configuration_blocks.ts | 15 + .../common/constants/index.ts | 11 + .../common/constants/index_names.ts | 9 + .../common/constants/plugin.ts | 9 + .../common/constants/table.ts | 12 + .../beats_management/common/domain_types.ts | 87 ++++ x-pack/plugins/beats_management/index.ts | 38 ++ .../plugins/beats_management/public/app.d.ts | 14 + .../components/autocomplete_field/index.tsx | 290 +++++++++++ .../autocomplete_field/suggestion_item.tsx | 123 +++++ .../public/components/config_list.tsx | 62 +++ .../public/components/connected_link.tsx | 40 ++ .../public/components/inputs/code_editor.tsx | 113 +++++ .../public/components/inputs/index.ts | 11 + .../public/components/inputs/input.tsx | 111 +++++ .../public/components/inputs/multi_input.tsx | 113 +++++ .../components/inputs/password_input.tsx | 110 +++++ .../public/components/inputs/select.tsx | 124 +++++ .../public/components/layouts/header.tsx | 36 ++ .../public/components/layouts/no_data.tsx | 62 +++ .../public/components/layouts/primary.tsx | 62 +++ .../public/components/layouts/walkthrough.tsx | 61 +++ .../public/components/table/action_button.tsx | 77 +++ .../components/table/assignment_schema.ts | 70 +++ .../public/components/table/controls.tsx | 66 +++ .../public/components/table/index.ts | 21 + .../public/components/table/table.tsx | 122 +++++ .../components/table/table_search_control.tsx | 29 ++ .../components/table/table_type_configs.tsx | 208 ++++++++ .../table_controls/action_control.tsx | 79 +++ .../public/components/table_controls/index.ts | 7 + .../table_controls/option_control.tsx | 54 +++ .../table_controls/popover_control.tsx | 90 ++++ .../table_controls/selection_count.tsx | 17 + .../table_controls/tag_assignment.tsx | 55 +++ .../table_controls/tag_badge_list.tsx | 29 ++ .../tag/config_view/config_form.tsx | 224 +++++++++ .../components/tag/config_view/index.tsx | 144 ++++++ .../public/components/tag/index.ts | 8 + .../public/components/tag/tag_badge.tsx | 39 ++ .../public/components/tag/tag_edit.tsx | 223 +++++++++ .../beats_management/public/config_schemas.ts | 370 ++++++++++++++ .../containers/with_kuery_autocompletion.tsx | 89 ++++ .../public/containers/with_url_state.tsx | 99 ++++ .../plugins/beats_management/public/index.tsx | 30 ++ .../public/lib/__tests__/tags.test.ts | 136 ++++++ .../lib/adapters/beats/adapter_types.ts | 36 ++ .../adapters/beats/memory_beats_adapter.ts | 105 ++++ .../lib/adapters/beats/rest_beats_adapter.ts | 57 +++ .../adapters/elasticsearch/adapter_types.ts | 12 + .../lib/adapters/elasticsearch/memory.ts | 29 ++ .../public/lib/adapters/elasticsearch/rest.ts | 77 +++ .../framework/kibana_framework_adapter.ts | 205 ++++++++ .../lib/adapters/rest_api/adapter_types.ts | 13 + .../rest_api/axios_rest_api_adapter.ts | 78 +++ .../public/lib/adapters/tags/adapter_types.ts | 13 + .../lib/adapters/tags/memory_tags_adapter.ts | 39 ++ .../lib/adapters/tags/rest_tags_adapter.ts | 36 ++ .../lib/adapters/tokens/adapter_types.ts | 9 + .../adapters/tokens/memory_tokens_adapter.ts | 13 + .../adapters/tokens/rest_tokens_adapter.ts | 18 + .../beats_management/public/lib/beats.ts | 70 +++ .../public/lib/compose/kibana.ts | 67 +++ .../public/lib/compose/memory.ts | 64 +++ .../public/lib/elasticsearch.ts | 69 +++ .../beats_management/public/lib/lib.ts | 88 ++++ .../beats_management/public/lib/tags.ts | 98 ++++ .../beats_management/public/pages/404.tsx | 13 + .../public/pages/beat/action_section.tsx | 58 +++ .../public/pages/beat/activity.tsx | 14 + .../public/pages/beat/detail.tsx | 103 ++++ .../public/pages/beat/index.tsx | 157 ++++++ .../public/pages/beat/tags.tsx | 67 +++ .../public/pages/main/activity.tsx | 13 + .../public/pages/main/beats.tsx | 292 +++++++++++ .../public/pages/main/create_tag_fragment.tsx | 108 +++++ .../public/pages/main/enroll_fragment.tsx | 280 +++++++++++ .../public/pages/main/index.tsx | 256 ++++++++++ .../public/pages/main/tags.tsx | 77 +++ .../public/pages/main/walkthrough_review.tsx | 106 ++++ .../public/pages/tag/index.tsx | 121 +++++ .../beats_management/public/router.tsx | 51 ++ .../public/utils/typed_react.ts | 65 +++ x-pack/plugins/beats_management/readme.md | 22 + .../beats_management/scripts/enroll.js | 35 ++ .../beats_management/server/kibana.index.ts | 13 + .../lib/adapters/beats/adapter_types.ts | 45 ++ .../beats/elasticsearch_beats_adapter.ts | 239 +++++++++ .../adapters/beats/memory_beats_adapter.ts | 114 +++++ .../database/__tests__/kibana.test.ts | 39 ++ .../database/__tests__/test_contract.ts | 75 +++ .../lib/adapters/database/adapter_types.ts | 307 ++++++++++++ .../database/kibana_database_adapter.ts | 107 ++++ .../database/memory_database_adapter.ts | 69 +++ .../framework/__tests__/kibana.test.ts | 34 ++ .../framework/__tests__/test_contract.ts | 34 ++ .../lib/adapters/framework/adapter_types.ts | 75 +++ .../framework/hapi_framework_adapter.ts | 88 ++++ .../framework/kibana_framework_adapter.ts | 161 ++++++ .../server/lib/adapters/tags/adapter_types.ts | 14 + .../tags/elasticsearch_tags_adapter.ts | 147 ++++++ .../lib/adapters/tags/memory_tags_adapter.ts | 43 ++ .../lib/adapters/tokens/adapter_types.ts | 17 + .../tokens/elasticsearch_tokens_adapter.ts | 75 +++ .../adapters/tokens/memory_tokens_adapter.ts | 46 ++ .../server/lib/compose/kibana.ts | 47 ++ .../server/lib/compose/testing.ts | 44 ++ .../__tests__/beats/assign_tags.test.ts | 233 +++++++++ .../domains/__tests__/beats/enroll.test.ts | 137 ++++++ .../__tests__/beats/remove_tags.test.ts | 112 +++++ .../domains/__tests__/beats/update.test.ts | 116 +++++ .../lib/domains/__tests__/tokens.test.ts | 76 +++ .../server/lib/domains/beats.ts | 246 ++++++++++ .../server/lib/domains/tags.ts | 96 ++++ .../server/lib/domains/tokens.ts | 137 ++++++ .../beats_management/server/lib/lib.ts | 31 ++ .../server/management_server.ts | 42 ++ .../__tests__/beats_assignments.test.ts | 253 ++++++++++ .../server/rest_api/__tests__/data.json | 158 ++++++ .../server/rest_api/__tests__/test_harnes.ts | 103 ++++ .../server/rest_api/beats/configuration.ts | 69 +++ .../server/rest_api/beats/enroll.ts | 64 +++ .../server/rest_api/beats/get.ts | 43 ++ .../server/rest_api/beats/list.ts | 62 +++ .../server/rest_api/beats/tag_assignment.ts | 42 ++ .../server/rest_api/beats/tag_removal.ts | 40 ++ .../server/rest_api/beats/update.ts | 72 +++ .../server/rest_api/tags/delete.ts | 27 ++ .../server/rest_api/tags/get.ts | 28 ++ .../server/rest_api/tags/list.ts | 39 ++ .../server/rest_api/tags/set.ts | 56 +++ .../server/rest_api/tokens/create.ts | 40 ++ .../beats_management/server/utils/README.md | 1 + .../server/utils/error_wrappers/index.ts | 7 + .../error_wrappers/wrap_es_error.test.ts | 42 ++ .../utils/error_wrappers/wrap_es_error.ts | 31 ++ .../server/utils/find_non_existent_items.ts | 19 + .../utils/index_templates/beats_template.json | 103 ++++ .../server/utils/index_templates/index.ts | 8 + .../server/utils/polyfills.ts | 17 + .../server/utils/wrap_request.ts | 36 ++ x-pack/plugins/beats_management/tsconfig.json | 7 + .../plugins/beats_management/types/eui.d.ts | 18 + .../beats_management/types/formsy.d.ts | 47 ++ .../beats_management/types/kibana.d.ts | 57 +++ x-pack/plugins/beats_management/wallaby.js | 60 +++ .../canvas/public/angular/services/index.js | 0 x-pack/plugins/canvas/tasks/mocks/noop.js | 0 .../grokdebugger/common/constants/index.js | 2 +- .../call_with_request_factory.js | 2 +- .../call_with_request_factory.js | 2 +- .../scripts/generate_types_from_graphql.js | 4 +- x-pack/plugins/logstash/README.md | 0 .../common/constants/es_scroll_settings.js | 0 .../logstash/common/constants/index.js | 0 .../logstash/common/constants/index_names.js | 0 .../logstash/common/constants/monitoring.js | 0 .../logstash/common/constants/pagination.js | 0 .../logstash/common/constants/pipeline.js | 0 .../logstash/common/constants/plugin.js | 0 .../logstash/common/constants/routes.js | 0 .../logstash/common/constants/tooltips.js | 0 .../logstash/common/constants/type_names.js | 0 .../common/lib/__tests__/get_moment.js | 0 .../plugins/logstash/common/lib/get_moment.js | 0 x-pack/plugins/logstash/common/lib/index.js | 0 x-pack/plugins/logstash/index.js | 0 .../lib/get_search_value/get_search_value.js | 0 .../public/lib/get_search_value/index.js | 0 .../public/lib/register_home_feature/index.js | 0 .../register_home_feature.js | 0 .../lib/update_management_sections/index.js | 0 .../update_logstash_sections.js | 0 .../logstash/public/models/cluster/cluster.js | 0 .../logstash/public/models/cluster/index.js | 0 .../logstash/public/models/pipeline/index.js | 0 .../public/models/pipeline/pipeline.js | 0 .../public/models/pipeline_list_item/index.js | 0 .../pipeline_list_item/pipeline_list_item.js | 0 .../components/pipeline_edit/index.js | 0 .../components/pipeline_edit/pipeline_edit.js | 0 .../components/upgrade_failure/index.js | 0 .../upgrade_failure/upgrade_failure.js | 0 .../public/sections/pipeline_edit/index.js | 0 .../pipeline_edit/pipeline_edit_route.html | 0 .../pipeline_edit/pipeline_edit_route.js | 0 .../components/pipeline_list/index.js | 0 .../components/pipeline_list/pipeline_list.js | 0 .../public/sections/pipeline_list/index.js | 0 .../pipeline_list/pipeline_list_route.html | 0 .../pipeline_list/pipeline_list_route.js | 0 .../register_management_section.js | 0 .../cluster/cluster_service.factory.js | 0 .../services/cluster/cluster_service.js | 0 .../logstash/public/services/cluster/index.js | 0 .../logstash/public/services/license/index.js | 0 .../license/license_service.factory.js | 0 .../license/logstash_license_service.js | 0 .../public/services/monitoring/index.js | 0 .../monitoring/monitoring_service.factory.js | 0 .../services/monitoring/monitoring_service.js | 0 .../public/services/pipeline/index.js | 0 .../pipeline/pipeline_service.factory.js | 0 .../services/pipeline/pipeline_service.js | 0 .../public/services/pipelines/index.js | 0 .../pipelines/pipelines_service.factory.js | 0 .../services/pipelines/pipelines_service.js | 0 .../public/services/security/index.js | 0 .../security/logstash_security_service.js | 0 .../security/security_service.factory.js | 0 .../logstash/public/services/upgrade/index.js | 0 .../upgrade/upgrade_service.factory.js | 0 .../services/upgrade/upgrade_service.js | 0 .../call_with_request_factory.js | 0 .../lib/call_with_request_factory/index.js | 0 .../check_license/__tests__/check_license.js | 0 .../server/lib/check_license/check_license.js | 0 .../server/lib/check_license/index.js | 0 .../__tests__/wrap_custom_error.js | 0 .../error_wrappers/__tests__/wrap_es_error.js | 0 .../__tests__/wrap_unknown_error.js | 0 .../server/lib/error_wrappers/index.js | 0 .../lib/error_wrappers/wrap_custom_error.js | 0 .../lib/error_wrappers/wrap_es_error.js | 0 .../lib/error_wrappers/wrap_unknown_error.js | 0 .../__tests__/fetch_all_from_scroll.js | 0 .../fetch_all_from_scroll.js | 0 .../server/lib/fetch_all_from_scroll/index.js | 0 .../__tests__/license_pre_routing_factory.js | 0 .../lib/license_pre_routing_factory/index.js | 0 .../license_pre_routing_factory.js | 0 .../lib/register_license_checker/index.js | 0 .../register_license_checker.js | 0 .../models/cluster/__tests__/cluster.js | 0 .../logstash/server/models/cluster/cluster.js | 0 .../logstash/server/models/cluster/index.js | 0 .../models/pipeline/__tests__/pipeline.js | 0 .../logstash/server/models/pipeline/index.js | 0 .../server/models/pipeline/pipeline.js | 0 .../__tests__/pipeline_list_item.js | 0 .../server/models/pipeline_list_item/index.js | 0 .../pipeline_list_item/pipeline_list_item.js | 0 .../server/routes/api/cluster/index.js | 0 .../api/cluster/register_cluster_routes.js | 0 .../routes/api/cluster/register_load_route.js | 0 .../server/routes/api/pipeline/index.js | 0 .../api/pipeline/register_delete_route.js | 0 .../api/pipeline/register_load_route.js | 0 .../api/pipeline/register_pipeline_routes.js | 0 .../api/pipeline/register_save_route.js | 0 .../server/routes/api/pipelines/index.js | 0 .../api/pipelines/register_delete_route.js | 0 .../api/pipelines/register_list_route.js | 0 .../pipelines/register_pipelines_routes.js | 0 .../server/routes/api/upgrade/index.js | 0 .../api/upgrade/register_execute_route.js | 0 .../api/upgrade/register_upgrade_routes.js | 0 .../simple/components/constants/states.js | 6 +- x-pack/plugins/monitoring/public/index.css | 457 ++++++++++++++++++ .../watcher/common/constants/index_names.js | 2 +- .../apis/beats/assign_tags_to_beats.js | 257 ++++++++++ .../api_integration/apis/beats/constants.js | 9 + .../apis/beats/create_enrollment_tokens.js | 100 ++++ .../api_integration/apis/beats/enroll_beat.js | 187 +++++++ .../api_integration/apis/beats/get_beat.js | 51 ++ .../test/api_integration/apis/beats/index.js | 29 ++ .../api_integration/apis/beats/list_beats.js | 38 ++ .../apis/beats/remove_tags_from_beats.js | 210 ++++++++ .../api_integration/apis/beats/set_tag.js | 273 +++++++++++ .../api_integration/apis/beats/update_beat.js | 120 +++++ x-pack/test/api_integration/apis/index.js | 1 + x-pack/test/api_integration/config.js | 1 + .../es_archives/beats/list/data.json | 158 ++++++ x-pack/yarn.lock | 164 +++++-- yarn.lock | 165 +++++-- 278 files changed, 13611 insertions(+), 83 deletions(-) create mode 100644 x-pack/plugins/beats_management/common/constants/configuration_blocks.ts create mode 100644 x-pack/plugins/beats_management/common/constants/index.ts create mode 100644 x-pack/plugins/beats_management/common/constants/index_names.ts create mode 100644 x-pack/plugins/beats_management/common/constants/plugin.ts create mode 100644 x-pack/plugins/beats_management/common/constants/table.ts create mode 100644 x-pack/plugins/beats_management/common/domain_types.ts create mode 100644 x-pack/plugins/beats_management/index.ts create mode 100644 x-pack/plugins/beats_management/public/app.d.ts create mode 100644 x-pack/plugins/beats_management/public/components/autocomplete_field/index.tsx create mode 100644 x-pack/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx create mode 100644 x-pack/plugins/beats_management/public/components/config_list.tsx create mode 100644 x-pack/plugins/beats_management/public/components/connected_link.tsx create mode 100644 x-pack/plugins/beats_management/public/components/inputs/code_editor.tsx create mode 100644 x-pack/plugins/beats_management/public/components/inputs/index.ts create mode 100644 x-pack/plugins/beats_management/public/components/inputs/input.tsx create mode 100644 x-pack/plugins/beats_management/public/components/inputs/multi_input.tsx create mode 100644 x-pack/plugins/beats_management/public/components/inputs/password_input.tsx create mode 100644 x-pack/plugins/beats_management/public/components/inputs/select.tsx create mode 100644 x-pack/plugins/beats_management/public/components/layouts/header.tsx create mode 100644 x-pack/plugins/beats_management/public/components/layouts/no_data.tsx create mode 100644 x-pack/plugins/beats_management/public/components/layouts/primary.tsx create mode 100644 x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/action_button.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/assignment_schema.ts create mode 100644 x-pack/plugins/beats_management/public/components/table/controls.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/index.ts create mode 100644 x-pack/plugins/beats_management/public/components/table/table.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/table_search_control.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table_controls/action_control.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table_controls/index.ts create mode 100644 x-pack/plugins/beats_management/public/components/table_controls/option_control.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table_controls/popover_control.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table_controls/selection_count.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table_controls/tag_assignment.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table_controls/tag_badge_list.tsx create mode 100644 x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx create mode 100644 x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx create mode 100644 x-pack/plugins/beats_management/public/components/tag/index.ts create mode 100644 x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx create mode 100644 x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx create mode 100644 x-pack/plugins/beats_management/public/config_schemas.ts create mode 100644 x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx create mode 100644 x-pack/plugins/beats_management/public/containers/with_url_state.tsx create mode 100644 x-pack/plugins/beats_management/public/index.tsx create mode 100644 x-pack/plugins/beats_management/public/lib/__tests__/tags.test.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/beats.ts create mode 100644 x-pack/plugins/beats_management/public/lib/compose/kibana.ts create mode 100644 x-pack/plugins/beats_management/public/lib/compose/memory.ts create mode 100644 x-pack/plugins/beats_management/public/lib/elasticsearch.ts create mode 100644 x-pack/plugins/beats_management/public/lib/lib.ts create mode 100644 x-pack/plugins/beats_management/public/lib/tags.ts create mode 100644 x-pack/plugins/beats_management/public/pages/404.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/beat/action_section.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/beat/activity.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/beat/detail.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/beat/index.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/beat/tags.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/main/activity.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/main/beats.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/main/create_tag_fragment.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/main/enroll_fragment.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/main/index.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/main/tags.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/main/walkthrough_review.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/tag/index.tsx create mode 100644 x-pack/plugins/beats_management/public/router.tsx create mode 100644 x-pack/plugins/beats_management/public/utils/typed_react.ts create mode 100644 x-pack/plugins/beats_management/readme.md create mode 100644 x-pack/plugins/beats_management/scripts/enroll.js create mode 100644 x-pack/plugins/beats_management/server/kibana.index.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.test.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/database/memory_database_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.test.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/test_contract.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/compose/kibana.ts create mode 100644 x-pack/plugins/beats_management/server/lib/compose/testing.ts create mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts create mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts create mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts create mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/update.test.ts create mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts create mode 100644 x-pack/plugins/beats_management/server/lib/domains/beats.ts create mode 100644 x-pack/plugins/beats_management/server/lib/domains/tags.ts create mode 100644 x-pack/plugins/beats_management/server/lib/domains/tokens.ts create mode 100644 x-pack/plugins/beats_management/server/lib/lib.ts create mode 100644 x-pack/plugins/beats_management/server/management_server.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/__tests__/data.json create mode 100644 x-pack/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/beats/get.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/beats/list.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/beats/update.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/tags/delete.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/tags/get.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/tags/list.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/tags/set.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/tokens/create.ts create mode 100644 x-pack/plugins/beats_management/server/utils/README.md create mode 100644 x-pack/plugins/beats_management/server/utils/error_wrappers/index.ts create mode 100644 x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.test.ts create mode 100644 x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.ts create mode 100644 x-pack/plugins/beats_management/server/utils/find_non_existent_items.ts create mode 100644 x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json create mode 100644 x-pack/plugins/beats_management/server/utils/index_templates/index.ts create mode 100644 x-pack/plugins/beats_management/server/utils/polyfills.ts create mode 100644 x-pack/plugins/beats_management/server/utils/wrap_request.ts create mode 100644 x-pack/plugins/beats_management/tsconfig.json create mode 100644 x-pack/plugins/beats_management/types/eui.d.ts create mode 100644 x-pack/plugins/beats_management/types/formsy.d.ts create mode 100644 x-pack/plugins/beats_management/types/kibana.d.ts create mode 100644 x-pack/plugins/beats_management/wallaby.js mode change 100644 => 100755 x-pack/plugins/canvas/public/angular/services/index.js mode change 100644 => 100755 x-pack/plugins/canvas/tasks/mocks/noop.js mode change 100644 => 100755 x-pack/plugins/logstash/README.md mode change 100644 => 100755 x-pack/plugins/logstash/common/constants/es_scroll_settings.js mode change 100644 => 100755 x-pack/plugins/logstash/common/constants/index.js mode change 100644 => 100755 x-pack/plugins/logstash/common/constants/index_names.js mode change 100644 => 100755 x-pack/plugins/logstash/common/constants/monitoring.js mode change 100644 => 100755 x-pack/plugins/logstash/common/constants/pagination.js mode change 100644 => 100755 x-pack/plugins/logstash/common/constants/pipeline.js mode change 100644 => 100755 x-pack/plugins/logstash/common/constants/plugin.js mode change 100644 => 100755 x-pack/plugins/logstash/common/constants/routes.js mode change 100644 => 100755 x-pack/plugins/logstash/common/constants/tooltips.js mode change 100644 => 100755 x-pack/plugins/logstash/common/constants/type_names.js mode change 100644 => 100755 x-pack/plugins/logstash/common/lib/__tests__/get_moment.js mode change 100644 => 100755 x-pack/plugins/logstash/common/lib/get_moment.js mode change 100644 => 100755 x-pack/plugins/logstash/common/lib/index.js mode change 100644 => 100755 x-pack/plugins/logstash/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/lib/get_search_value/get_search_value.js mode change 100644 => 100755 x-pack/plugins/logstash/public/lib/get_search_value/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/lib/register_home_feature/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/lib/register_home_feature/register_home_feature.js mode change 100644 => 100755 x-pack/plugins/logstash/public/lib/update_management_sections/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/lib/update_management_sections/update_logstash_sections.js mode change 100644 => 100755 x-pack/plugins/logstash/public/models/cluster/cluster.js mode change 100644 => 100755 x-pack/plugins/logstash/public/models/cluster/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/models/pipeline/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/models/pipeline/pipeline.js mode change 100644 => 100755 x-pack/plugins/logstash/public/models/pipeline_list_item/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/pipeline_edit.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/upgrade_failure.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_edit/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.html mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/pipeline_list.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_list/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.html mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.js mode change 100644 => 100755 x-pack/plugins/logstash/public/sections/pipeline_list/register_management_section.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/cluster/cluster_service.factory.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/cluster/cluster_service.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/cluster/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/license/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/license/license_service.factory.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/license/logstash_license_service.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/monitoring/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/monitoring/monitoring_service.factory.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/pipeline/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/pipeline/pipeline_service.factory.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/pipeline/pipeline_service.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/pipelines/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/pipelines/pipelines_service.factory.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/pipelines/pipelines_service.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/security/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/security/logstash_security_service.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/security/security_service.factory.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/upgrade/index.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/upgrade/upgrade_service.factory.js mode change 100644 => 100755 x-pack/plugins/logstash/public/services/upgrade/upgrade_service.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/call_with_request_factory/call_with_request_factory.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/call_with_request_factory/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/check_license/__tests__/check_license.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/check_license/check_license.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/check_license/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_custom_error.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_es_error.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_unknown_error.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/error_wrappers/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/error_wrappers/wrap_custom_error.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/error_wrappers/wrap_es_error.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/error_wrappers/wrap_unknown_error.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/license_pre_routing_factory/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/license_pre_routing_factory/license_pre_routing_factory.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/register_license_checker/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/lib/register_license_checker/register_license_checker.js mode change 100644 => 100755 x-pack/plugins/logstash/server/models/cluster/__tests__/cluster.js mode change 100644 => 100755 x-pack/plugins/logstash/server/models/cluster/cluster.js mode change 100644 => 100755 x-pack/plugins/logstash/server/models/cluster/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/models/pipeline/__tests__/pipeline.js mode change 100644 => 100755 x-pack/plugins/logstash/server/models/pipeline/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/models/pipeline/pipeline.js mode change 100644 => 100755 x-pack/plugins/logstash/server/models/pipeline_list_item/__tests__/pipeline_list_item.js mode change 100644 => 100755 x-pack/plugins/logstash/server/models/pipeline_list_item/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/cluster/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/cluster/register_cluster_routes.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/cluster/register_load_route.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/pipeline/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/pipeline/register_delete_route.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/pipeline/register_load_route.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/pipeline/register_pipeline_routes.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/pipeline/register_save_route.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/pipelines/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/pipelines/register_delete_route.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/pipelines/register_list_route.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/pipelines/register_pipelines_routes.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/upgrade/index.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/upgrade/register_execute_route.js mode change 100644 => 100755 x-pack/plugins/logstash/server/routes/api/upgrade/register_upgrade_routes.js create mode 100644 x-pack/plugins/monitoring/public/index.css create mode 100644 x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js create mode 100644 x-pack/test/api_integration/apis/beats/constants.js create mode 100644 x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js create mode 100644 x-pack/test/api_integration/apis/beats/enroll_beat.js create mode 100644 x-pack/test/api_integration/apis/beats/get_beat.js create mode 100644 x-pack/test/api_integration/apis/beats/index.js create mode 100644 x-pack/test/api_integration/apis/beats/list_beats.js create mode 100644 x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js create mode 100644 x-pack/test/api_integration/apis/beats/set_tag.js create mode 100644 x-pack/test/api_integration/apis/beats/update_beat.js create mode 100644 x-pack/test/functional/es_archives/beats/list/data.json diff --git a/package.json b/package.json index cc50bbf569322..923b77631f009 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,9 @@ "type": "git", "url": "https://github.com/elastic/kibana.git" }, + "resolutions": { + "**/@types/node": "8.10.21" + }, "dependencies": { "@elastic/eui": "4.4.1", "@elastic/filesaver": "1.1.2", @@ -234,9 +237,9 @@ "@kbn/eslint-plugin-license-header": "link:packages/kbn-eslint-plugin-license-header", "@kbn/plugin-generator": "link:packages/kbn-plugin-generator", "@kbn/test": "link:packages/kbn-test", + "@octokit/rest": "^15.10.0", "@types/angular": "^1.6.50", "@types/angular-mocks": "^1.7.0", - "@octokit/rest": "^15.10.0", "@types/babel-core": "^6.25.5", "@types/bluebird": "^3.1.1", "@types/boom": "^7.2.0", diff --git a/x-pack/index.js b/x-pack/index.js index f047000d8c606..be1ee31f7c2b2 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -16,6 +16,7 @@ import { watcher } from './plugins/watcher'; import { grokdebugger } from './plugins/grokdebugger'; import { dashboardMode } from './plugins/dashboard_mode'; import { logstash } from './plugins/logstash'; +import { beats } from './plugins/beats_management'; import { apm } from './plugins/apm'; import { licenseManagement } from './plugins/license_management'; import { cloud } from './plugins/cloud'; @@ -42,6 +43,7 @@ module.exports = function (kibana) { grokdebugger(kibana), dashboardMode(kibana), logstash(kibana), + beats(kibana), apm(kibana), canvas(kibana), licenseManagement(kibana), diff --git a/x-pack/package.json b/x-pack/package.json index d15f142ed3d67..73c8a384effaa 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -20,6 +20,9 @@ "intermediateBuildDirectory": "build/plugin/kibana/x-pack" } }, + "resolutions": { + "**/@types/node": "8.10.21" + }, "devDependencies": { "@kbn/dev-utils": "link:../packages/kbn-dev-utils", "@kbn/es": "link:../packages/kbn-es", @@ -38,6 +41,7 @@ "@types/history": "^4.6.2", "@types/jest": "^23.3.1", "@types/joi": "^10.4.4", + "@types/jsonwebtoken": "^7.2.7", "@types/lodash": "^3.10.1", "@types/mocha": "^5.2.5", "@types/pngjs": "^3.3.1", @@ -46,7 +50,7 @@ "@types/react-datepicker": "^1.1.5", "@types/react-dom": "^16.0.5", "@types/react-redux": "^6.0.6", - "@types/react-router-dom": "^4.2.6", + "@types/react-router-dom": "4.2.6", "@types/reduce-reducers": "^0.1.3", "@types/sinon": "^5.0.1", "@types/supertest": "^2.0.5", @@ -157,6 +161,7 @@ "extract-zip": "1.5.0", "file-saver": "^1.3.8", "font-awesome": "4.4.0", + "formsy-react": "^1.1.5", "get-port": "2.1.0", "getos": "^3.1.0", "glob": "6.0.4", @@ -173,6 +178,7 @@ "isomorphic-fetch": "2.2.1", "joi": "6.10.1", "jquery": "^3.3.1", + "jsonwebtoken": "^8.3.0", "jstimezonedetect": "1.0.5", "lodash": "npm:@elastic/lodash@3.10.1-kibana1", "lodash.clone": "^4.5.0", diff --git a/x-pack/plugins/beats_management/common/constants/configuration_blocks.ts b/x-pack/plugins/beats_management/common/constants/configuration_blocks.ts new file mode 100644 index 0000000000000..e89e53e25b89d --- /dev/null +++ b/x-pack/plugins/beats_management/common/constants/configuration_blocks.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export enum ConfigurationBlockTypes { + FilebeatInputs = 'filebeat.inputs', + FilebeatModules = 'filebeat.modules', + MetricbeatModules = 'metricbeat.modules', + Output = 'output', + Processors = 'processors', +} + +export const UNIQUENESS_ENFORCING_TYPES = [ConfigurationBlockTypes.Output]; diff --git a/x-pack/plugins/beats_management/common/constants/index.ts b/x-pack/plugins/beats_management/common/constants/index.ts new file mode 100644 index 0000000000000..50851dcef947e --- /dev/null +++ b/x-pack/plugins/beats_management/common/constants/index.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export { PLUGIN } from './plugin'; +export { INDEX_NAMES } from './index_names'; +export { UNIQUENESS_ENFORCING_TYPES, ConfigurationBlockTypes } from './configuration_blocks'; +export const BASE_PATH = '/management/beats_management/'; +export { TABLE_CONFIG } from './table'; diff --git a/x-pack/plugins/beats_management/common/constants/index_names.ts b/x-pack/plugins/beats_management/common/constants/index_names.ts new file mode 100644 index 0000000000000..f8d20fb79c360 --- /dev/null +++ b/x-pack/plugins/beats_management/common/constants/index_names.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export const INDEX_NAMES = { + BEATS: '.management-beats', +}; diff --git a/x-pack/plugins/beats_management/common/constants/plugin.ts b/x-pack/plugins/beats_management/common/constants/plugin.ts new file mode 100644 index 0000000000000..dc7cd85300341 --- /dev/null +++ b/x-pack/plugins/beats_management/common/constants/plugin.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export const PLUGIN = { + ID: 'beats_management', +}; diff --git a/x-pack/plugins/beats_management/common/constants/table.ts b/x-pack/plugins/beats_management/common/constants/table.ts new file mode 100644 index 0000000000000..119b4f3da4596 --- /dev/null +++ b/x-pack/plugins/beats_management/common/constants/table.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export const TABLE_CONFIG = { + INITIAL_ROW_SIZE: 5, + PAGE_SIZE_OPTIONS: [3, 5, 10, 20], + TRUNCATE_TAG_LENGTH: 33, + TRUNCATE_TAG_LENGTH_SMALL: 20, +}; diff --git a/x-pack/plugins/beats_management/common/domain_types.ts b/x-pack/plugins/beats_management/common/domain_types.ts new file mode 100644 index 0000000000000..e65a7192780a7 --- /dev/null +++ b/x-pack/plugins/beats_management/common/domain_types.ts @@ -0,0 +1,87 @@ +/* + * 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 { ConfigurationBlockTypes } from './constants'; + +export enum FilebeatModuleName { + system = 'system', + apache2 = 'apache2', + nginx = 'nginx', + mongodb = 'mongodb', + elasticsearch = 'elasticsearch', +} + +export enum MetricbeatModuleName { + system = 'system', + apache2 = 'apache2', + nginx = 'nginx', + mongodb = 'mongodb', + elasticsearch = 'elasticsearch', +} + +export enum OutputType { + elasticsearch = 'elasticsearch', + logstash = 'logstash', + kafka = 'kafka', + console = 'console', +} + +export interface FilebeatInputsConfig { + paths: string[]; + other: string; +} +export interface FilebeatModuleConfig { + module: FilebeatModuleName; + other: string; +} +export interface MetricbeatModuleConfig { + module: MetricbeatModuleName; + hosts?: string[]; + period: string; + other: string; +} + +export type ConfigContent = FilebeatInputsConfig | FilebeatModuleConfig | MetricbeatModuleConfig; +export interface ConfigurationBlock { + type: ConfigurationBlockTypes; + description: string; + configs: ConfigContent[]; +} + +export interface ReturnedConfigurationBlock + extends Pick> { + config: ConfigContent; +} + +export interface CMBeat { + id: string; + enrollment_token: string; + active: boolean; + access_token: string; + verified_on?: string; + type: string; + version?: string; + host_ip: string; + host_name: string; + ephemeral_id?: string; + last_updated?: string; + event_rate?: string; + local_configuration_yml?: string; + tags?: string[]; + central_configuration_yml?: string; + metadata?: {}; + name?: string; +} + +export interface CMPopulatedBeat extends CMBeat { + full_tags: BeatTag[]; +} + +export interface BeatTag { + id: string; + configuration_blocks: ConfigurationBlock[]; + color?: string; + last_updated: Date; +} diff --git a/x-pack/plugins/beats_management/index.ts b/x-pack/plugins/beats_management/index.ts new file mode 100644 index 0000000000000..00853dc82af98 --- /dev/null +++ b/x-pack/plugins/beats_management/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { resolve } from 'path'; +import { PLUGIN } from './common/constants'; +import { initServerWithKibana } from './server/kibana.index'; + +const DEFAULT_ENROLLMENT_TOKENS_TTL_S = 10 * 60; // 10 minutes + +export const config = Joi.object({ + enabled: Joi.boolean().default(true), + encryptionKey: Joi.string(), + enrollmentTokensTtlInSeconds: Joi.number() + .integer() + .min(1) + .max(10 * 60 * 14) // No more then 2 weeks for security reasons + .default(DEFAULT_ENROLLMENT_TOKENS_TTL_S), +}).default(); +export const configPrefix = 'xpack.beats'; + +export function beats(kibana: any) { + return new kibana.Plugin({ + id: PLUGIN.ID, + require: ['kibana', 'elasticsearch', 'xpack_main'], + publicDir: resolve(__dirname, 'public'), + uiExports: { + managementSections: ['plugins/beats_management'], + }, + config: () => config, + configPrefix, + init(server: any) { + initServerWithKibana(server); + }, + }); +} diff --git a/x-pack/plugins/beats_management/public/app.d.ts b/x-pack/plugins/beats_management/public/app.d.ts new file mode 100644 index 0000000000000..4e806e12d3843 --- /dev/null +++ b/x-pack/plugins/beats_management/public/app.d.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +export type FlatObject = { [Key in keyof T]: string }; + +export interface AppURLState { + beatsKBar?: string; + tagsKBar?: string; + enrollmentToken?: string; + createdTag?: string; +} diff --git a/x-pack/plugins/beats_management/public/components/autocomplete_field/index.tsx b/x-pack/plugins/beats_management/public/components/autocomplete_field/index.tsx new file mode 100644 index 0000000000000..7ff5296ef4d92 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/autocomplete_field/index.tsx @@ -0,0 +1,290 @@ +/* + * 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 { + EuiFieldSearch, + EuiFieldSearchProps, + EuiOutsideClickDetector, + EuiPanel, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +// @ts-ignore +import { AutocompleteSuggestion } from 'ui/autocomplete_providers'; + +import { composeStateUpdaters } from '../../utils/typed_react'; +import { SuggestionItem } from './suggestion_item'; + +interface AutocompleteFieldProps { + isLoadingSuggestions: boolean; + isValid: boolean; + loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; + onSubmit?: (value: string) => void; + onChange?: (value: string) => void; + placeholder?: string; + suggestions: AutocompleteSuggestion[]; + value: string; +} + +interface AutocompleteFieldState { + areSuggestionsVisible: boolean; + selectedIndex: number | null; +} + +export class AutocompleteField extends React.Component< + AutocompleteFieldProps, + AutocompleteFieldState +> { + public readonly state: AutocompleteFieldState = { + areSuggestionsVisible: false, + selectedIndex: null, + }; + + private inputElement: HTMLInputElement | null = null; + + public render() { + const { suggestions, isLoadingSuggestions, isValid, placeholder, value } = this.props; + const { areSuggestionsVisible, selectedIndex } = this.state; + + return ( + + + + {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( + + {suggestions.map((suggestion, suggestionIndex) => ( + + ))} + + ) : null} + + + ); + } + + public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) { + const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; + const hasNewValue = prevProps.value !== this.props.value; + + if (hasNewValue) { + this.updateSuggestions(); + } + + if (hasNewSuggestions) { + this.showSuggestions(); + } + } + + private handleChangeInputRef = (element: HTMLInputElement | null) => { + this.inputElement = element; + }; + + private handleChange = (evt: React.ChangeEvent) => { + this.changeValue(evt.currentTarget.value); + }; + + private handleKeyDown = (evt: React.KeyboardEvent) => { + const { suggestions } = this.props; + + switch (evt.key) { + case 'ArrowUp': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState( + composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) + ); + } + break; + case 'ArrowDown': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); + } else { + this.updateSuggestions(); + } + break; + case 'Enter': + evt.preventDefault(); + if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } else { + this.submit(); + } + break; + case 'Escape': + evt.preventDefault(); + this.setState(withSuggestionsHidden); + break; + } + }; + + private handleKeyUp = (evt: React.KeyboardEvent) => { + switch (evt.key) { + case 'ArrowLeft': + case 'ArrowRight': + case 'Home': + case 'End': + this.updateSuggestions(); + break; + } + }; + + private selectSuggestionAt = (index: number) => () => { + this.setState(withSuggestionAtIndexSelected(index)); + }; + + private applySelectedSuggestion = () => { + if (this.state.selectedIndex !== null) { + this.applySuggestionAt(this.state.selectedIndex)(); + } + }; + + private applySuggestionAt = (index: number) => () => { + const { value, suggestions } = this.props; + const selectedSuggestion = suggestions[index]; + + if (!selectedSuggestion) { + return; + } + + const newValue = + value.substr(0, selectedSuggestion.start) + + selectedSuggestion.text + + value.substr(selectedSuggestion.end); + + this.setState(withSuggestionsHidden); + this.changeValue(newValue); + this.focusInputElement(); + }; + + private changeValue = (value: string) => { + const { onChange } = this.props; + if (onChange) { + onChange(value); + } + }; + + private focusInputElement = () => { + if (this.inputElement) { + this.inputElement.focus(); + } + }; + + private showSuggestions = () => { + this.setState(withSuggestionsVisible); + }; + + private hideSuggestions = () => { + this.setState(withSuggestionsHidden); + }; + + private submit = () => { + const { isValid, onSubmit, value } = this.props; + + if (isValid && onSubmit) { + onSubmit(value); + } + + this.setState(withSuggestionsHidden); + }; + + private updateSuggestions = (value?: string) => { + const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; + this.props.loadSuggestions(value || this.props.value, inputCursorPosition, 10); + }; +} + +const withPreviousSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length + : Math.max(props.suggestions.length - 1, 0), +}); + +const withNextSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + 1) % props.suggestions.length + : 0, +}); + +const withSuggestionAtIndexSelected = (suggestionIndex: number) => ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length + ? suggestionIndex + : 0, +}); + +const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: true, +}); + +const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: false, + selectedIndex: null, +}); + +const FixedEuiFieldSearch: React.SFC< + React.InputHTMLAttributes & + EuiFieldSearchProps & { + inputRef?: (element: HTMLInputElement | null) => void; + onSearch: (value: string) => void; + } +> = EuiFieldSearch as any; + +const AutocompleteContainer = styled.div` + position: relative; +`; + +const SuggestionsPanel = styled(EuiPanel).attrs({ + paddingSize: 'none', + hasShadow: true, +})` + position: absolute; + width: 100%; + margin-top: 2px; + overflow: hidden; + z-index: 1000; +`; diff --git a/x-pack/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx new file mode 100644 index 0000000000000..c9dc832cf37b7 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx @@ -0,0 +1,123 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import { tint } from 'polished'; +import React from 'react'; +import styled from 'styled-components'; + +// @ts-ignore +import { AutocompleteSuggestion } from 'ui/autocomplete_providers'; + +interface SuggestionItemProps { + isSelected?: boolean; + onClick?: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; + suggestion: AutocompleteSuggestion; +} + +export class SuggestionItem extends React.Component { + public static defaultProps: Partial = { + isSelected: false, + }; + + public render() { + const { isSelected, onClick, onMouseEnter, suggestion } = this.props; + + return ( + + + + + {suggestion.text} + + + ); + } +} + +const SuggestionItemContainer = styled.div<{ + isSelected?: boolean; +}>` + display: flex; + flex-direction: row; + font-size: ${props => props.theme.eui.euiFontSizeS}; + height: ${props => props.theme.eui.euiSizeXl}; + white-space: nowrap; + background-color: ${props => + props.isSelected ? props.theme.eui.euiColorLightestShade : 'transparent'}; +`; + +const SuggestionItemField = styled.div` + align-items: center; + cursor: pointer; + display: flex; + flex-direction: row; + height: ${props => props.theme.eui.euiSizeXl}; + padding: ${props => props.theme.eui.euiSizeXs}; +`; + +const SuggestionItemIconField = SuggestionItemField.extend<{ suggestionType: string }>` + background-color: ${props => tint(0.1, getEuiIconColor(props.theme, props.suggestionType))}; + color: ${props => getEuiIconColor(props.theme, props.suggestionType)}; + flex: 0 0 auto; + justify-content: center; + width: ${props => props.theme.eui.euiSizeXl}; +`; + +const SuggestionItemTextField = SuggestionItemField.extend` + flex: 2 0 0; + font-family: ${props => props.theme.eui.euiCodeFontFamily}; +`; + +const SuggestionItemDescriptionField = SuggestionItemField.extend` + flex: 3 0 0; + p { + display: inline; + span { + font-family: ${props => props.theme.eui.euiCodeFontFamily}; + } + } +`; + +const getEuiIconType = (suggestionType: string) => { + switch (suggestionType) { + case 'field': + return 'kqlField'; + case 'value': + return 'kqlValue'; + case 'recentSearch': + return 'search'; + case 'conjunction': + return 'kqlSelector'; + case 'operator': + return 'kqlOperand'; + default: + return 'empty'; + } +}; + +const getEuiIconColor = (theme: any, suggestionType: string): string => { + switch (suggestionType) { + case 'field': + return theme.eui.euiColorVis7; + case 'value': + return theme.eui.euiColorVis0; + case 'operator': + return theme.eui.euiColorVis1; + case 'conjunction': + return theme.eui.euiColorVis2; + case 'recentSearch': + default: + return theme.eui.euiColorMediumShade; + } +}; diff --git a/x-pack/plugins/beats_management/public/components/config_list.tsx b/x-pack/plugins/beats_management/public/components/config_list.tsx new file mode 100644 index 0000000000000..b04163a183264 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/config_list.tsx @@ -0,0 +1,62 @@ +/* + * 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. + */ + +// @ts-ignore +import { EuiBasicTable, EuiLink } from '@elastic/eui'; +import React from 'react'; +import { ConfigurationBlock } from '../../common/domain_types'; +import { supportedConfigs } from '../config_schemas'; + +interface ComponentProps { + configs: ConfigurationBlock[]; + onConfigClick: (action: 'edit' | 'delete', config: ConfigurationBlock) => any; +} + +export const ConfigList: React.SFC = props => ( + { + const type = supportedConfigs.find((sc: any) => sc.value === config.type); + + return ( + props.onConfigClick('edit', config)}> + {type ? type.text : config.type} + + ); + }, + }, + { + field: 'module', + name: 'Module', + truncateText: false, + render: (value: string) => { + return value || 'N/A'; + }, + }, + { + field: 'description', + name: 'Description', + }, + { + name: 'Actions', + actions: [ + { + name: 'Remove', + description: 'Remove this config from tag', + type: 'icon', + icon: 'trash', + onClick: (item: ConfigurationBlock) => props.onConfigClick('delete', item), + }, + ], + }, + ]} + /> +); diff --git a/x-pack/plugins/beats_management/public/components/connected_link.tsx b/x-pack/plugins/beats_management/public/components/connected_link.tsx new file mode 100644 index 0000000000000..b2c0e8ad607af --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/connected_link.tsx @@ -0,0 +1,40 @@ +/* + * 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 React from 'react'; + +import { EuiLink } from '@elastic/eui'; +import { Link, withRouter } from 'react-router-dom'; + +export function ConnectedLinkComponent({ + location, + path, + query, + disabled, + ...props +}: { + location: any; + path: string; + disabled: boolean; + query: any; + [key: string]: any; +}) { + if (disabled) { + return ; + } + + // Shorthand for pathname + const pathname = path || _.get(props.to, 'pathname') || location.pathname; + + return ( + + ); +} + +export const ConnectedLink = withRouter(ConnectedLinkComponent); diff --git a/x-pack/plugins/beats_management/public/components/inputs/code_editor.tsx b/x-pack/plugins/beats_management/public/components/inputs/code_editor.tsx new file mode 100644 index 0000000000000..508649d6ad22d --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/inputs/code_editor.tsx @@ -0,0 +1,113 @@ +/* + * 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. + */ +// @ts-ignore +import { CommonProps, EuiCodeEditor, EuiCodeEditorProps, EuiFormRow } from '@elastic/eui'; +// @ts-ignore +import { FormsyInputProps, withFormsy } from 'formsy-react'; +import React, { Component, InputHTMLAttributes } from 'react'; + +interface ComponentProps extends FormsyInputProps, CommonProps, EuiCodeEditorProps { + instantValidation: boolean; + label: string; + isReadOnly: boolean; + mode: 'javascript' | 'yaml'; + errorText: string; + fullWidth: boolean; + helpText: React.ReactElement; + compressed: boolean; + onChange(value: string): void; + onBlur(): void; +} + +interface ComponentState { + allowError: boolean; +} + +class CodeEditor extends Component< + InputHTMLAttributes & ComponentProps, + ComponentState +> { + public static defaultProps = { + passRequiredToField: true, + }; + + public state = { allowError: false }; + + public componentDidMount() { + const { defaultValue, setValue } = this.props; + setValue(defaultValue || ''); + } + + public componentWillReceiveProps(nextProps: ComponentProps) { + if (nextProps.isFormSubmitted()) { + this.showError(); + } + } + + public handleChange = (value: string) => { + this.props.setValue(value); + if (this.props.onChange) { + this.props.onChange(value); + } + if (this.props.instantValidation) { + this.showError(); + } + }; + + public handleBlur = () => { + this.showError(); + if (this.props.onBlur) { + this.props.onBlur(); + } + }; + + public showError = () => this.setState({ allowError: true }); + + public render() { + const { + id, + label, + isReadOnly, + isValid, + getValue, + isPristine, + getErrorMessage, + mode, + fullWidth, + className, + helpText, + } = this.props; + + const { allowError } = this.state; + const error = !isPristine() && !isValid() && allowError; + + return ( + + + + ); + } +} + +export const FormsyEuiCodeEditor = withFormsy(CodeEditor); diff --git a/x-pack/plugins/beats_management/public/components/inputs/index.ts b/x-pack/plugins/beats_management/public/components/inputs/index.ts new file mode 100644 index 0000000000000..50a5674f6fd7c --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/inputs/index.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export { FormsyEuiCodeEditor } from './code_editor'; +export { FormsyEuiFieldText } from './input'; +export { FormsyEuiPasswordText } from './password_input'; +export { FormsyEuiMultiFieldText } from './multi_input'; +export { FormsyEuiSelect } from './select'; diff --git a/x-pack/plugins/beats_management/public/components/inputs/input.tsx b/x-pack/plugins/beats_management/public/components/inputs/input.tsx new file mode 100644 index 0000000000000..cfcb1de832098 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/inputs/input.tsx @@ -0,0 +1,111 @@ +/* + * 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 { CommonProps, EuiFieldText, EuiFieldTextProps, EuiFormRow } from '@elastic/eui'; +import { FormsyInputProps, withFormsy } from 'formsy-react'; +import React, { Component, InputHTMLAttributes } from 'react'; + +interface ComponentProps extends FormsyInputProps, CommonProps, EuiFieldTextProps { + instantValidation?: boolean; + label: string; + errorText: string; + fullWidth: boolean; + helpText: React.ReactElement; + compressed: boolean; + onChange?(e: React.ChangeEvent, value: any): void; + onBlur?(e: React.ChangeEvent, value: any): void; +} + +interface ComponentState { + allowError: boolean; +} + +class FieldText extends Component< + InputHTMLAttributes & ComponentProps, + ComponentState +> { + public static defaultProps = { + passRequiredToField: true, + }; + + public state = { allowError: false }; + + public componentDidMount() { + const { defaultValue, setValue } = this.props; + if (defaultValue) { + setValue(defaultValue); + } + } + + public componentWillReceiveProps(nextProps: ComponentProps) { + if (nextProps.isFormSubmitted()) { + this.showError(); + } + } + + public handleChange = (e: React.ChangeEvent) => { + const { value } = e.currentTarget; + this.props.setValue(value); + if (this.props.onChange) { + this.props.onChange(e, e.currentTarget.value); + } + if (this.props.instantValidation) { + this.showError(); + } + }; + + public handleBlur = (e: React.ChangeEvent) => { + this.showError(); + if (this.props.onBlur) { + this.props.onBlur(e, e.currentTarget.value); + } + }; + + public showError = () => this.setState({ allowError: true }); + + public render() { + const { + id, + required, + label, + getValue, + isValid, + isPristine, + getErrorMessage, + fullWidth, + className, + disabled, + helpText, + } = this.props; + + const { allowError } = this.state; + const error = !isPristine() && !isValid() && allowError; + + return ( + + + + ); + } +} + +export const FormsyEuiFieldText = withFormsy(FieldText); diff --git a/x-pack/plugins/beats_management/public/components/inputs/multi_input.tsx b/x-pack/plugins/beats_management/public/components/inputs/multi_input.tsx new file mode 100644 index 0000000000000..00111b879fe13 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/inputs/multi_input.tsx @@ -0,0 +1,113 @@ +/* + * 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 { CommonProps, EuiFormRow, EuiTextArea, EuiTextAreaProps } from '@elastic/eui'; +// @ts-ignore +import { FormsyInputProps, withFormsy } from 'formsy-react'; +import React, { Component, InputHTMLAttributes } from 'react'; + +interface ComponentProps extends FormsyInputProps, CommonProps, EuiTextAreaProps { + instantValidation: boolean; + label: string; + errorText: string; + fullWidth: boolean; + helpText: React.ReactElement; + compressed: boolean; + onChange(e: React.ChangeEvent, value: any): void; + onBlur(e: React.ChangeEvent, value: any): void; +} + +interface ComponentState { + allowError: boolean; +} + +class MultiFieldText extends Component< + InputHTMLAttributes & ComponentProps, + ComponentState +> { + public static defaultProps = { + passRequiredToField: true, + }; + + public state = { allowError: false }; + + public componentDidMount() { + const { defaultValue, setValue } = this.props; + + if (defaultValue) { + setValue(defaultValue); + } + } + + public componentWillReceiveProps(nextProps: ComponentProps) { + if (nextProps.isFormSubmitted()) { + this.showError(); + } + } + + public handleChange = (e: React.ChangeEvent) => { + const value = e.currentTarget.value.split('\n'); + this.props.setValue(value); + if (this.props.onChange) { + this.props.onChange(e, value); + } + if (this.props.instantValidation) { + this.showError(); + } + }; + + public handleBlur = (e: React.ChangeEvent) => { + this.showError(); + if (this.props.onBlur) { + this.props.onBlur(e, e.currentTarget.value); + } + }; + + public showError = () => this.setState({ allowError: true }); + + public render() { + const { + id, + required, + label, + getValue, + isValid, + isPristine, + getErrorMessage, + fullWidth, + className, + disabled, + helpText, + } = this.props; + + const { allowError } = this.state; + const error = !isPristine() && !isValid() && allowError; + + return ( + + + + ); + } +} + +export const FormsyEuiMultiFieldText = withFormsy(MultiFieldText); diff --git a/x-pack/plugins/beats_management/public/components/inputs/password_input.tsx b/x-pack/plugins/beats_management/public/components/inputs/password_input.tsx new file mode 100644 index 0000000000000..30318f76bb90f --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/inputs/password_input.tsx @@ -0,0 +1,110 @@ +/* + * 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. + */ + +// @ts-ignore currently no definition for EuiFieldPassword +import { CommonProps, EuiFieldPassword, EuiFieldPasswordProps, EuiFormRow } from '@elastic/eui'; +import { FormsyInputProps, withFormsy } from 'formsy-react'; +import React, { Component, InputHTMLAttributes } from 'react'; + +interface ComponentProps extends FormsyInputProps, CommonProps, EuiFieldPasswordProps { + instantValidation?: boolean; + label: string; + errorText: string; + fullWidth: boolean; + helpText: React.ReactElement; + compressed: boolean; + onChange?(e: React.ChangeEvent, value: any): void; + onBlur?(e: React.ChangeEvent, value: any): void; +} + +interface ComponentState { + allowError: boolean; +} + +class FieldPassword extends Component< + InputHTMLAttributes & ComponentProps, + ComponentState +> { + constructor(props: any) { + super(props); + + this.state = { + allowError: false, + }; + } + + public componentDidMount() { + const { defaultValue, setValue } = this.props; + if (defaultValue) { + setValue(defaultValue); + } + } + + public handleChange = (e: React.ChangeEvent) => { + const { value } = e.currentTarget; + this.props.setValue(value); + if (this.props.onChange) { + this.props.onChange(e, value); + } + if (this.props.instantValidation) { + this.showError(); + } + }; + + public handleBlur = (e: React.ChangeEvent) => { + this.showError(); + if (this.props.onBlur) { + this.props.onBlur(e, e.currentTarget.value); + } + }; + + public showError = () => this.setState({ allowError: true }); + + public render() { + const { + id, + required, + label, + getValue, + isValid, + isPristine, + getErrorMessage, + fullWidth, + className, + disabled, + helpText, + onBlur, + } = this.props; + + const { allowError } = this.state; + const error = !isPristine() && !isValid() && allowError; + + return ( + + + + ); + } +} + +export const FormsyEuiPasswordText = withFormsy(FieldPassword); diff --git a/x-pack/plugins/beats_management/public/components/inputs/select.tsx b/x-pack/plugins/beats_management/public/components/inputs/select.tsx new file mode 100644 index 0000000000000..8fa192090a99a --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/inputs/select.tsx @@ -0,0 +1,124 @@ +/* + * 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 { + CommonProps, + EuiFormRow, + // @ts-ignore + EuiSelect, +} from '@elastic/eui'; +// @ts-ignore +import { FormsyInputProps, withFormsy } from 'formsy-react'; +import React, { Component, InputHTMLAttributes } from 'react'; + +const FixedSelect = EuiSelect as React.SFC; + +interface ComponentProps extends FormsyInputProps, CommonProps { + instantValidation: boolean; + options: Array<{ value: string; text: string }>; + label: string; + errorText: string; + fullWidth: boolean; + helpText: React.ReactElement; + compressed: boolean; + onChange(e: React.ChangeEvent, value: any): void; + onBlur(e: React.ChangeEvent, value: any): void; +} + +interface ComponentState { + allowError: boolean; +} + +class FieldSelect extends Component< + InputHTMLAttributes & ComponentProps, + ComponentState +> { + public static defaultProps = { + passRequiredToField: true, + }; + + public state = { allowError: false }; + + public componentDidMount() { + const { defaultValue, setValue } = this.props; + if (defaultValue) { + setValue(defaultValue); + } + } + + public componentWillReceiveProps(nextProps: ComponentProps) { + if (nextProps.isFormSubmitted()) { + this.showError(); + } + } + + public handleChange = (e: React.ChangeEvent) => { + const { value } = e.currentTarget; + + this.props.setValue(value); + if (this.props.onChange) { + this.props.onChange(e, e.currentTarget.value); + } + if (this.props.instantValidation) { + this.showError(); + } + }; + + public handleBlur = (e: React.ChangeEvent) => { + this.showError(); + if (this.props.onBlur) { + this.props.onBlur(e, e.currentTarget.value); + } + }; + + public showError = () => this.setState({ allowError: true }); + + public render() { + const { + id, + required, + label, + options, + getValue, + isValid, + isPristine, + getErrorMessage, + fullWidth, + className, + disabled, + helpText, + } = this.props; + + const { allowError } = this.state; + const error = !isPristine() && !isValid() && allowError; + + return ( + + + + ); + } +} + +export const FormsyEuiSelect = withFormsy(FieldSelect); diff --git a/x-pack/plugins/beats_management/public/components/layouts/header.tsx b/x-pack/plugins/beats_management/public/components/layouts/header.tsx new file mode 100644 index 0000000000000..4ad567b73fc77 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/layouts/header.tsx @@ -0,0 +1,36 @@ +/* + * 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 { + EuiBreadcrumbDefinition, + EuiHeader, + EuiHeaderBreadcrumbs, + EuiHeaderSection, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +interface HeaderProps { + breadcrumbs?: EuiBreadcrumbDefinition[]; +} + +export class Header extends React.PureComponent { + public render() { + const { breadcrumbs = [] } = this.props; + + return ( + + + + + + ); + } +} + +const HeaderWrapper = styled(EuiHeader)` + height: 29px; +`; diff --git a/x-pack/plugins/beats_management/public/components/layouts/no_data.tsx b/x-pack/plugins/beats_management/public/components/layouts/no_data.tsx new file mode 100644 index 0000000000000..8f31b90ff507e --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/layouts/no_data.tsx @@ -0,0 +1,62 @@ +/* + * 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 React from 'react'; +import { withRouter } from 'react-router-dom'; + +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiOverlayMask, + EuiPage, + EuiPageBody, + EuiPageContent, +} from '@elastic/eui'; + +interface LayoutProps { + title: string; + actionSection?: React.ReactNode; + modalRender?: () => React.ReactNode; + modalClosePath?: string; +} + +export const NoDataLayout: React.SFC = withRouter( + ({ actionSection, title, modalRender, modalClosePath, children, history }) => { + const modalContent = modalRender && modalRender(); + return ( + + + + + + {title}} + body={children} + actions={actionSection} + /> + + + + + {modalContent && ( + + { + history.push(modalClosePath); + }} + style={{ width: '640px' }} + > + {modalContent} + + + )} + + ); + } +) as any; diff --git a/x-pack/plugins/beats_management/public/components/layouts/primary.tsx b/x-pack/plugins/beats_management/public/components/layouts/primary.tsx new file mode 100644 index 0000000000000..516ad648eac8b --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/layouts/primary.tsx @@ -0,0 +1,62 @@ +/* + * 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 React from 'react'; +import { withRouter } from 'react-router-dom'; + +import { + EuiModal, + EuiOverlayMask, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; + +interface PrimaryLayoutProps { + title: string; + actionSection?: React.ReactNode; + modalRender?: () => React.ReactNode; + modalClosePath?: string; +} + +export const PrimaryLayout: React.SFC = withRouter( + ({ actionSection, title, modalRender, modalClosePath, children, history }) => { + const modalContent = modalRender && modalRender(); + return ( + + + + + +

{title}

+
+
+ {actionSection} +
+ + {children} + +
+ {modalContent && ( + + { + history.push(modalClosePath); + }} + style={{ width: '640px' }} + > + {modalContent} + + + )} +
+ ); + } +) as any; diff --git a/x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx b/x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx new file mode 100644 index 0000000000000..32cfd4cb43316 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx @@ -0,0 +1,61 @@ +/* + * 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 React from 'react'; + +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + // @ts-ignore + EuiStepsHorizontal, + EuiTitle, +} from '@elastic/eui'; + +interface LayoutProps { + title: string; + goTo: (path: string) => any; + walkthroughSteps: Array<{ + id: string; + name: string; + disabled: boolean; + }>; + activePath: string; +} + +export const WalkthroughLayout: React.SFC = ({ + walkthroughSteps, + title, + activePath, + goTo, + children, +}) => { + const indexOfCurrent = walkthroughSteps.findIndex(step => activePath === step.id); + return ( + + + + +

{title}

+
+
+
+ ({ + title: step.name, + isComplete: i <= indexOfCurrent, + onClick: () => goTo(step.id), + }))} + /> +
+
+ {children} +
+
+
+ ); +}; diff --git a/x-pack/plugins/beats_management/public/components/table/action_button.tsx b/x-pack/plugins/beats_management/public/components/table/action_button.tsx new file mode 100644 index 0000000000000..54be343bbd5ab --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/action_button.tsx @@ -0,0 +1,77 @@ +/* + * 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 { EuiButton, EuiContextMenu, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; +import React from 'react'; +import { ActionDefinition } from './table_type_configs'; + +interface ActionButtonProps { + actions: ActionDefinition[]; + isPopoverVisible: boolean; + actionHandler(action: string, payload?: any): void; + hidePopover(): void; + showPopover(): void; +} + +const Action = (props: { + action: string; + danger?: boolean; + name: string; + actionHandler(action: string, payload?: any): void; +}) => { + const { action, actionHandler, danger, name } = props; + return ( + actionHandler(action)}> + {name} + + ); +}; + +export function ActionButton(props: ActionButtonProps) { + const { actions, actionHandler, hidePopover, isPopoverVisible, showPopover } = props; + if (actions.length === 0) { + return null; + } else if (actions.length <= 2) { + return ( + + {actions.map(({ action, danger, name }) => ( + + + + ))} + + ); + } + return ( + + Bulk Action + + } + closePopover={hidePopover} + id="contextMenu" + isOpen={isPopoverVisible} + panelPaddingSize="none" + withTitle + > + ({ + ...action, + onClick: () => actionHandler(action.action), + })), + }, + ]} + /> + + ); +} diff --git a/x-pack/plugins/beats_management/public/components/table/assignment_schema.ts b/x-pack/plugins/beats_management/public/components/table/assignment_schema.ts new file mode 100644 index 0000000000000..305a6f3470662 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/assignment_schema.ts @@ -0,0 +1,70 @@ +/* + * 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 { AssignmentActionType } from './table'; + +export enum AssignmentComponentType { + Action, + Popover, + SelectionCount, + TagBadgeList, +} + +export interface AssignmentControlSchema { + name: string; + type: AssignmentComponentType; + danger?: boolean; + action?: AssignmentActionType; + showWarning?: boolean; + warningHeading?: string; + warningMessage?: string; + lazyLoad?: boolean; + children?: AssignmentControlSchema[]; + grow?: boolean; +} + +export const beatsListAssignmentOptions: AssignmentControlSchema[] = [ + { + type: AssignmentComponentType.Action, + grow: false, + name: 'Disenroll selected', + showWarning: true, + warningHeading: 'Disenroll beats', + warningMessage: 'This will disenroll the selected beat(s) from centralized management', + action: AssignmentActionType.Delete, + danger: true, + }, + { + type: AssignmentComponentType.Popover, + name: 'Set tags', + grow: false, + lazyLoad: true, + children: [ + { + name: 'Assign tags', + type: AssignmentComponentType.TagBadgeList, + }, + ], + }, + { + type: AssignmentComponentType.SelectionCount, + grow: true, + name: 'selectionCount', + }, +]; + +export const tagConfigAssignmentOptions: AssignmentControlSchema[] = [ + { + type: AssignmentComponentType.Action, + danger: true, + grow: false, + name: 'Detach beat(s)', + showWarning: true, + warningHeading: 'Detatch beats', + warningMessage: 'This will detatch the selected beat(s) from this tag.', + action: AssignmentActionType.Delete, + }, +]; diff --git a/x-pack/plugins/beats_management/public/components/table/controls.tsx b/x-pack/plugins/beats_management/public/components/table/controls.tsx new file mode 100644 index 0000000000000..302c96d7c05a4 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/controls.tsx @@ -0,0 +1,66 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { AutocompleteField } from '../autocomplete_field/index'; +import { OptionControl } from '../table_controls'; +import { AssignmentOptions as AssignmentOptionsType, KueryBarProps } from './table'; + +interface ControlBarProps { + assignmentOptions: AssignmentOptionsType; + kueryBarProps?: KueryBarProps; + selectionCount: number; +} + +export function ControlBar(props: ControlBarProps) { + const { + assignmentOptions: { actionHandler, items, schema, type }, + kueryBarProps, + selectionCount, + } = props; + + if (type === 'none') { + return null; + } + + const showSearch = type !== 'assignment' || selectionCount === 0; + const showAssignmentOptions = type === 'assignment' && selectionCount > 0; + const showPrimaryOptions = type === 'primary' && selectionCount > 0; + + return ( + + {showPrimaryOptions && + schema.map(def => ( + + + + ))} + {showSearch && + kueryBarProps && ( + + + + )} + {showAssignmentOptions && + schema.map(def => ( + + + + ))} + + ); +} diff --git a/x-pack/plugins/beats_management/public/components/table/index.ts b/x-pack/plugins/beats_management/public/components/table/index.ts new file mode 100644 index 0000000000000..7025a5f334241 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AssignmentActionType, AssignmentOptions, KueryBarProps, Table } from './table'; +export { + AssignmentComponentType, + AssignmentControlSchema, + beatsListAssignmentOptions, + tagConfigAssignmentOptions, +} from './assignment_schema'; +export { ControlBar } from './controls'; +export { + ActionDefinition, + BeatDetailTagsTable, + BeatsTableType, + FilterDefinition, + TagsTableType, +} from './table_type_configs'; diff --git a/x-pack/plugins/beats_management/public/components/table/table.tsx b/x-pack/plugins/beats_management/public/components/table/table.tsx new file mode 100644 index 0000000000000..8b29ad34203f1 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/table.tsx @@ -0,0 +1,122 @@ +/* + * 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 { + // @ts-ignore no typings for EuiInMemoryTable in EUI + EuiInMemoryTable, + EuiSpacer, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { AutocompleteSuggestion } from 'ui/autocomplete_providers'; +import { TABLE_CONFIG } from '../../../common/constants'; +import { AssignmentControlSchema } from './assignment_schema'; +import { ControlBar } from './controls'; +import { TableType } from './table_type_configs'; + +export enum AssignmentActionType { + Add, + Assign, + Delete, + Edit, + Reload, + Search, +} + +export interface AssignmentOptions { + schema: AssignmentControlSchema[]; + items: any[]; + type?: 'none' | 'primary' | 'assignment'; + actionHandler(action: AssignmentActionType, payload?: any): void; +} + +export interface KueryBarProps { + filterQueryDraft: string; + isLoadingSuggestions: boolean; + isValid: boolean; + loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; + onChange?: (value: string) => void; + onSubmit?: (value: string) => void; + suggestions: AutocompleteSuggestion[]; + value: string; +} + +interface TableProps { + assignmentOptions?: AssignmentOptions; + hideTableControls?: boolean; + kueryBarProps?: KueryBarProps; + items: any[]; + type: TableType; +} + +interface TableState { + selection: any[]; +} + +const TableContainer = styled.div` + padding: 16px; +`; + +export class Table extends React.Component { + constructor(props: any) { + super(props); + + this.state = { + selection: [], + }; + } + + public resetSelection = () => { + this.setSelection([]); + }; + + public setSelection = (selection: any[]) => { + this.setState({ + selection, + }); + }; + + public render() { + const { assignmentOptions, hideTableControls, items, kueryBarProps, type } = this.props; + + const pagination = { + initialPageSize: TABLE_CONFIG.INITIAL_ROW_SIZE, + pageSizeOptions: TABLE_CONFIG.PAGE_SIZE_OPTIONS, + }; + + const selectionOptions = hideTableControls + ? null + : { + onSelectionChange: this.setSelection, + selectable: () => true, + selectableMessage: () => 'Select this beat', + selection: this.state.selection, + }; + + return ( + + {!hideTableControls && + assignmentOptions && ( + + )} + + + + ); + } +} diff --git a/x-pack/plugins/beats_management/public/components/table/table_search_control.tsx b/x-pack/plugins/beats_management/public/components/table/table_search_control.tsx new file mode 100644 index 0000000000000..c3d79f5d1891b --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/table_search_control.tsx @@ -0,0 +1,29 @@ +/* + * 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 { + // @ts-ignore typings for EuiSearchar not included in EUI + EuiSearchBar, +} from '@elastic/eui'; +import React from 'react'; +import { FilterDefinition } from '../table'; +import { AssignmentActionType } from './table'; + +interface TableSearchControlProps { + filters?: FilterDefinition[]; + actionHandler(action: AssignmentActionType, payload?: any): void; +} + +export const TableSearchControl = (props: TableSearchControlProps) => { + const { actionHandler, filters } = props; + return ( + actionHandler(AssignmentActionType.Search, query)} + /> + ); +}; diff --git a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx new file mode 100644 index 0000000000000..f5a3e9c441844 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx @@ -0,0 +1,208 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { first, sortBy, sortByOrder, uniq } from 'lodash'; +import moment from 'moment'; +import React from 'react'; +import { BeatTag, CMPopulatedBeat, ConfigurationBlock } from '../../../common/domain_types'; +import { ConnectedLink } from '../connected_link'; +import { TagBadge } from '../tag'; + +export interface ColumnDefinition { + align?: string; + field: string; + name: string; + sortable?: boolean; + width?: string; + render?(value: any, object?: any): any; +} + +export interface ActionDefinition { + action: string; + danger?: boolean; + icon?: any; + name: string; +} + +interface FilterOption { + value: string; +} + +export interface FilterDefinition { + field: string; + name: string; + options?: FilterOption[]; + type: string; +} + +export interface ControlDefinitions { + actions: ActionDefinition[]; + filters: FilterDefinition[]; + primaryActions?: ActionDefinition[]; +} + +export interface TableType { + columnDefinitions: ColumnDefinition[]; + controlDefinitions(items: any[]): ControlDefinitions; +} + +export const BeatsTableType: TableType = { + columnDefinitions: [ + { + field: 'name', + name: 'Beat name', + render: (name: string, beat: CMPopulatedBeat) => ( + {name} + ), + sortable: true, + }, + { + field: 'type', + name: 'Type', + sortable: true, + }, + { + field: 'full_tags', + name: 'Tags', + render: (value: string, beat: CMPopulatedBeat) => ( + + {(sortBy(beat.full_tags, 'id') || []).map(tag => ( + + + + + + ))} + + ), + sortable: false, + }, + { + // TODO: update to use actual metadata field + field: 'event_rate', + name: 'Event rate', + sortable: true, + }, + { + // TODO: update to use actual metadata field + field: 'full_tags', + name: 'Last config update', + render: (tags: BeatTag[]) => + tags.length ? ( + + {moment(first(sortByOrder(tags, ['last_updated'], ['desc'])).last_updated).fromNow()} + + ) : null, + sortable: true, + }, + ], + controlDefinitions: (data: any[]) => ({ + actions: [ + { + name: 'Disenroll Selected', + action: 'delete', + danger: true, + }, + ], + filters: [ + { + type: 'field_value_selection', + field: 'type', + name: 'Type', + options: uniq(data.map(({ type }: { type: any }) => ({ value: type })), 'value'), + }, + ], + }), +}; + +export const TagsTableType: TableType = { + columnDefinitions: [ + { + field: 'id', + name: 'Tag name', + render: (id: string, tag: BeatTag) => ( + + + + ), + sortable: true, + width: '45%', + }, + { + align: 'right', + field: 'configuration_blocks', + name: 'Configurations', + render: (configurationBlocks: ConfigurationBlock[]) => ( +
{configurationBlocks.length}
+ ), + sortable: false, + }, + { + align: 'right', + field: 'last_updated', + name: 'Last update', + render: (lastUpdate: Date) =>
{moment(lastUpdate).fromNow()}
, + sortable: true, + }, + ], + controlDefinitions: (data: any) => ({ + actions: [ + { + name: 'Remove Selected', + action: 'delete', + danger: true, + }, + ], + filters: [], + }), +}; + +export const BeatDetailTagsTable: TableType = { + columnDefinitions: [ + { + field: 'id', + name: 'Tag name', + render: (id: string, tag: BeatTag) => ( + + + + ), + sortable: true, + width: '55%', + }, + { + align: 'right', + field: 'configuration_blocks', + name: 'Configurations', + render: (configurations: ConfigurationBlock[]) => {configurations.length}, + sortable: true, + }, + { + align: 'right', + field: 'last_updated', + name: 'Last update', + render: (lastUpdate: string) => {moment(lastUpdate).fromNow()}, + sortable: true, + }, + ], + controlDefinitions: (data: any) => ({ + actions: [], + filters: [], + primaryActions: [ + { + name: 'Add Tag', + action: 'add', + danger: false, + }, + { + name: 'Remove Selected', + action: 'remove', + danger: true, + }, + ], + }), +}; diff --git a/x-pack/plugins/beats_management/public/components/table_controls/action_control.tsx b/x-pack/plugins/beats_management/public/components/table_controls/action_control.tsx new file mode 100644 index 0000000000000..74c9a0f404c5c --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table_controls/action_control.tsx @@ -0,0 +1,79 @@ +/* + * 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 { + EuiButton, + // @ts-ignore EuiConfirmModal typings not included in current EUI + EuiConfirmModal, + EuiOverlayMask, +} from '@elastic/eui'; +import React from 'react'; +import { AssignmentActionType } from '../table'; + +interface ActionControlProps { + action: AssignmentActionType; + danger?: boolean; + name: string; + showWarning?: boolean; + warningHeading?: string; + warningMessage?: string; + actionHandler(action: AssignmentActionType, payload?: any): void; +} + +interface ActionControlState { + showModal: boolean; +} + +export class ActionControl extends React.PureComponent { + constructor(props: ActionControlProps) { + super(props); + + this.state = { + showModal: false, + }; + } + + public render() { + const { + action, + actionHandler, + danger, + name, + showWarning, + warningHeading, + warningMessage, + } = this.props; + return ( +
+ this.setState({ showModal: true }) : () => actionHandler(action) + } + > + {name} + + {this.state.showModal && ( + + { + actionHandler(action); + this.setState({ showModal: false }); + }} + onCancel={() => this.setState({ showModal: false })} + title={warningHeading ? warningHeading : 'Confirm'} + > + {warningMessage} + + + )} +
+ ); + } +} diff --git a/x-pack/plugins/beats_management/public/components/table_controls/index.ts b/x-pack/plugins/beats_management/public/components/table_controls/index.ts new file mode 100644 index 0000000000000..f97cd9e727d2a --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table_controls/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { OptionControl } from './option_control'; diff --git a/x-pack/plugins/beats_management/public/components/table_controls/option_control.tsx b/x-pack/plugins/beats_management/public/components/table_controls/option_control.tsx new file mode 100644 index 0000000000000..7640d3a8965e2 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table_controls/option_control.tsx @@ -0,0 +1,54 @@ +/* + * 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 React from 'react'; +import { AssignmentComponentType, AssignmentControlSchema } from '../table'; +import { AssignmentActionType } from '../table'; +import { ActionControl } from './action_control'; +import { PopoverControl } from './popover_control'; +import { SelectionCount } from './selection_count'; +import { TagBadgeList } from './tag_badge_list'; + +interface OptionControlProps { + items: any[]; + schema: AssignmentControlSchema; + selectionCount: number; + actionHandler(action: AssignmentActionType, payload?: any): void; +} + +export const OptionControl = (props: OptionControlProps) => { + const { + actionHandler, + items, + schema, + schema: { action, danger, name, showWarning, warningHeading, warningMessage }, + selectionCount, + } = props; + switch (schema.type) { + case AssignmentComponentType.Action: + if (!action) { + throw Error('Action cannot be undefined'); + } + return ( + + ); + case AssignmentComponentType.Popover: + return ; + case AssignmentComponentType.SelectionCount: + return ; + case AssignmentComponentType.TagBadgeList: + return ; + } + return
{schema.type}
; +}; diff --git a/x-pack/plugins/beats_management/public/components/table_controls/popover_control.tsx b/x-pack/plugins/beats_management/public/components/table_controls/popover_control.tsx new file mode 100644 index 0000000000000..bb66ab50aaef8 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table_controls/popover_control.tsx @@ -0,0 +1,90 @@ +/* + * 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 { EuiButton, EuiPopover } from '@elastic/eui'; +import React from 'react'; +import { AssignmentActionType } from '../table'; +import { AssignmentControlSchema } from '../table/assignment_schema'; +import { OptionControl } from './option_control'; + +interface PopoverControlProps { + items: any[]; + schema: AssignmentControlSchema; + selectionCount: number; + actionHandler(action: AssignmentActionType, payload?: any): void; +} + +interface PopoverControlState { + showPopover: boolean; +} + +export class PopoverControl extends React.PureComponent { + constructor(props: PopoverControlProps) { + super(props); + + this.state = { + showPopover: false, + }; + } + + public componentDidMount() { + const { + schema: { lazyLoad }, + } = this.props; + if (!lazyLoad) { + this.props.actionHandler(AssignmentActionType.Reload); + } + } + + public render() { + const { + actionHandler, + items, + schema: { children, lazyLoad, name }, + selectionCount, + } = this.props; + return ( + { + if (lazyLoad) { + actionHandler(AssignmentActionType.Reload); + } + this.setState({ + showPopover: true, + }); + }} + > + {name} + + } + closePopover={() => { + this.setState({ showPopover: false }); + }} + id="assignmentList" + isOpen={this.state.showPopover} + panelPaddingSize="s" + withTitle + > + {children + ? children.map(def => ( + + )) + : null} + + ); + } +} diff --git a/x-pack/plugins/beats_management/public/components/table_controls/selection_count.tsx b/x-pack/plugins/beats_management/public/components/table_controls/selection_count.tsx new file mode 100644 index 0000000000000..8a4a525a2ddbf --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table_controls/selection_count.tsx @@ -0,0 +1,17 @@ +/* + * 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 React from 'react'; + +interface SelectionCountProps { + selectionCount: number; +} + +export const SelectionCount = (props: SelectionCountProps) => ( +
+ {props.selectionCount} {`item${props.selectionCount === 1 ? '' : 's'}`} selected +
+); diff --git a/x-pack/plugins/beats_management/public/components/table_controls/tag_assignment.tsx b/x-pack/plugins/beats_management/public/components/table_controls/tag_assignment.tsx new file mode 100644 index 0000000000000..952636d9b9804 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table_controls/tag_assignment.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React from 'react'; +import { TABLE_CONFIG } from '../../../common/constants'; +import { TagBadge } from '../tag/tag_badge'; + +interface TagAssignmentProps { + tag: any; + assignTag(id: string): void; +} + +interface TagAssignmentState { + isFetchingTags: boolean; +} + +export class TagAssignment extends React.PureComponent { + constructor(props: TagAssignmentProps) { + super(props); + + this.state = { + isFetchingTags: false, + }; + } + + public render() { + const { + assignTag, + tag, + tag: { id }, + } = this.props; + + return ( + + {this.state.isFetchingTags && ( + + + + )} + + assignTag(id)} + onClickAriaLabel={id} + tag={tag} + /> + + + ); + } +} diff --git a/x-pack/plugins/beats_management/public/components/table_controls/tag_badge_list.tsx b/x-pack/plugins/beats_management/public/components/table_controls/tag_badge_list.tsx new file mode 100644 index 0000000000000..0801dc211e1ea --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table_controls/tag_badge_list.tsx @@ -0,0 +1,29 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { AssignmentActionType } from '../table/table'; +import { TagAssignment } from './tag_assignment'; + +interface TagBadgeListProps { + items: any[]; + actionHandler(action: AssignmentActionType, payload?: any): void; +} + +export const TagBadgeList = (props: TagBadgeListProps) => ( + // @ts-ignore direction prop type "column" not defined in current EUI version + + {props.items.map((item: any) => ( + + props.actionHandler(AssignmentActionType.Assign, id)} + /> + + ))} + +); diff --git a/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx b/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx new file mode 100644 index 0000000000000..70273740b2fde --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx @@ -0,0 +1,224 @@ +/* + * 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. + */ +// @ts-ignore +import Formsy, { addValidationRule, FieldValue, FormData } from 'formsy-react'; +import yaml from 'js-yaml'; +import { get } from 'lodash'; +import React from 'react'; +import { ConfigurationBlock } from '../../../../common/domain_types'; +import { YamlConfigSchema } from '../../../lib/lib'; +import { + FormsyEuiCodeEditor, + FormsyEuiFieldText, + FormsyEuiMultiFieldText, + FormsyEuiPasswordText, + FormsyEuiSelect, +} from '../../inputs'; + +addValidationRule('isHosts', (form: FormData, values: FieldValue | string[]) => { + if (values && values.length > 0 && values instanceof Array) { + return values.reduce((pass: boolean, value: string) => { + if ( + pass && + value.match( + new RegExp( + '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$' + ) + ) !== null + ) { + return true; + } + return false; + }, true); + } else { + return true; + } +}); + +addValidationRule('isString', (values: FormData, value: FieldValue) => { + return true; +}); + +addValidationRule('isPeriod', (values: FormData, value: FieldValue) => { + // TODO add more validation + return true; +}); + +addValidationRule('isPath', (values: FormData, value: FieldValue) => { + // TODO add more validation + return value && value.length > 0; +}); + +addValidationRule('isPaths', (values: FormData, value: FieldValue) => { + // TODO add more validation + + return true; +}); + +addValidationRule('isYaml', (values: FormData, value: FieldValue) => { + try { + const stuff = yaml.safeLoad(value || ''); + if (typeof stuff === 'string') { + return false; + } + return true; + } catch (e) { + return false; + } +}); + +interface ComponentProps { + values: ConfigurationBlock; + schema: YamlConfigSchema[]; + id: string; + canSubmit(canIt: boolean): any; + onSubmit(modal: any): any; +} + +export class ConfigForm extends React.Component { + private form = React.createRef(); + constructor(props: ComponentProps) { + super(props); + + this.state = { + canSubmit: false, + }; + } + + public enableButton = () => { + this.setState({ + canSubmit: true, + }); + this.props.canSubmit(true); + }; + public disableButton = () => { + this.setState({ + canSubmit: false, + }); + this.props.canSubmit(false); + }; + public submit = () => { + if (this.form.current) { + this.form.current.click(); + } + }; + public onValidSubmit = (model: ModelType) => { + const processed = JSON.parse(JSON.stringify(model), (key, value) => { + return _.isObject(value) && !_.isArray(value) + ? _.mapKeys(value, (v, k: string) => { + return k.replace(/{{[^{}]+}}/g, (token: string) => { + return model[token.replace(/[{}]+/g, '')] || 'error'; + }); + }) + : value; + }); + + this.props.schema.forEach(s => { + if (s.ui.transform && s.ui.transform === 'removed') { + delete processed[s.id]; + } + }); + this.props.onSubmit(processed); + }; + public render() { + return ( +
+
+ + {this.props.schema.map(schema => { + switch (schema.ui.type) { + case 'input': + return ( + + ); + case 'password': + return ( + + ); + case 'multi-input': + return ( + + ); + case 'select': + return ( + + ); + case 'code': + return ( + + ); + } + })} +
+ ); + } +} diff --git a/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx b/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx new file mode 100644 index 0000000000000..ad1e46a9a7a75 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx @@ -0,0 +1,144 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + // @ts-ignore + EuiCodeEditor, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFormRow, + // @ts-ignore + EuiHorizontalRule, + // @ts-ignore + EuiSearchBar, + // @ts-ignore + EuiSelect, + // @ts-ignore + EuiTabbedContent, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import { ConfigurationBlock } from '../../../../common/domain_types'; +import { supportedConfigs } from '../../../config_schemas'; +import { ConfigForm } from './config_form'; + +interface ComponentProps { + configBlock?: ConfigurationBlock; + onClose(): any; + onSave(config: ConfigurationBlock): any; +} + +export class ConfigView extends React.Component { + private form = React.createRef(); + private editMode: boolean; + constructor(props: any) { + super(props); + this.editMode = props.configBlock !== undefined; + + this.state = { + valid: false, + configBlock: props.configBlock || { + type: supportedConfigs[0].value, + }, + }; + } + public onValueChange = (field: string) => (e: any) => { + const value = e.currentTarget ? e.currentTarget.value : e; + this.setState((state: any) => ({ + configBlock: { + ...state.configBlock, + [field]: value, + }, + })); + }; + public render() { + return ( + + + +

{this.editMode ? 'Edit Configuration' : 'Add Configuration'}

+
+
+ + + + + + + +

+ Config for  + { + (supportedConfigs.find(config => this.state.configBlock.type === config.value) as any) + .text + } +

+ + + { + this.props.onSave({ + ...this.state.configBlock, + configs: [data], + }); + this.props.onClose(); + }} + canSubmit={canIt => this.setState({ valid: canIt })} + ref={this.form} + values={this.state.configBlock} + id={ + (supportedConfigs.find(config => this.state.configBlock.type === config.value) as any) + .value + } + schema={ + (supportedConfigs.find(config => this.state.configBlock.type === config.value) as any) + .config + } + /> +
+ + + + + Close + + + + { + if (this.form.current) { + this.form.current.submit(); + } + }} + > + Save + + + + +
+ ); + } +} diff --git a/x-pack/plugins/beats_management/public/components/tag/index.ts b/x-pack/plugins/beats_management/public/components/tag/index.ts new file mode 100644 index 0000000000000..24a1c3f6f8b1e --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/tag/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { TagBadge } from './tag_badge'; +export { TagEdit } from './tag_edit'; diff --git a/x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx b/x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx new file mode 100644 index 0000000000000..f0cca09b0181c --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EuiBadge } from '@elastic/eui'; +import React from 'react'; +import { TABLE_CONFIG } from '../../../common/constants'; + +interface TagBadgeProps { + iconType?: any; + onClick?: () => void; + onClickAriaLabel?: string; + maxIdRenderSize?: number; + tag: { color?: string; id: string }; +} + +export const TagBadge = (props: TagBadgeProps) => { + const { + iconType, + onClick, + onClickAriaLabel, + tag: { color, id }, + } = props; + + const maxIdRenderSize = props.maxIdRenderSize || TABLE_CONFIG.TRUNCATE_TAG_LENGTH; + const idToRender = id.length > maxIdRenderSize ? `${id.substring(0, maxIdRenderSize)}...` : id; + return ( + + {idToRender} + + ); +}; diff --git a/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx b/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx new file mode 100644 index 0000000000000..7d80b180e2f87 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx @@ -0,0 +1,223 @@ +/* + * 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 { + EuiButton, + // @ts-ignore + EuiColorPicker, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + // @ts-ignore + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import 'brace/mode/yaml'; +import 'brace/theme/github'; +import { isEqual } from 'lodash'; +import React from 'react'; +import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types'; +import { ConfigList } from '../config_list'; +import { AssignmentActionType, Table } from '../table'; +import { BeatsTableType } from '../table'; +import { tagConfigAssignmentOptions } from '../table'; +import { ConfigView } from './config_view'; +import { TagBadge } from './tag_badge'; + +interface TagEditProps { + mode: 'edit' | 'create'; + tag: Pick>; + onDetachBeat: (beatIds: string[]) => void; + onTagChange: (field: keyof BeatTag, value: string) => any; + attachedBeats: CMBeat[] | null; +} + +interface TagEditState { + showFlyout: boolean; + tableRef: any; + selectedConfigIndex?: number; +} + +export class TagEdit extends React.PureComponent { + constructor(props: TagEditProps) { + super(props); + + this.state = { + showFlyout: false, + tableRef: React.createRef(), + }; + } + + public render() { + const { tag, attachedBeats } = this.props; + return ( +
+ + + +

Tag details

+
+ +

+ Tags will apply the configurations below to all beats assigned this tag. +
+ The tag type defines the options available. +

+
+
+ +
+
+ + + + + + {this.props.mode === 'create' && ( + + + + )} + + +
+ + + + + + +

Tag Configurations

+
+ +

+ Tags can contain multiple configurations. These configurations can repeat or mix + types as necessary. For example, you may utilize three metricbeat configurations + alongside one input and filebeat configuration. +

+
+
+ +
+ { + const selectedIndex = tag.configuration_blocks.findIndex(c => { + return isEqual(config, c); + }); + if (action === 'delete') { + const configs = [...tag.configuration_blocks]; + configs.splice(selectedIndex, 1); + this.updateTag('configuration_blocks', configs); + } else { + this.setState({ + showFlyout: true, + selectedConfigIndex: selectedIndex, + }); + } + }} + /> +
+ { + this.setState({ showFlyout: true }); + }} + > + Add configuration + +
+
+
+ + {attachedBeats && ( +
+ + + +

Attached Beats

+
+ + + )} + + {this.state.showFlyout && ( + this.setState({ showFlyout: false, selectedConfigIndex: undefined })} + onSave={(config: any) => { + this.setState({ showFlyout: false, selectedConfigIndex: undefined }); + if (this.state.selectedConfigIndex !== undefined) { + const configs = [...tag.configuration_blocks]; + configs[this.state.selectedConfigIndex] = config; + this.updateTag('configuration_blocks', configs); + } else { + this.updateTag('configuration_blocks', [ + ...(tag.configuration_blocks || []), + config, + ]); + } + }} + /> + )} + + ); + } + + private getNameError = (name: string) => { + if (name && name !== '' && name.search(/^[a-zA-Z0-9-]+$/) === -1) { + return 'Tag name must consist of letters, numbers, and dashes only'; + } else { + return false; + } + }; + + private handleAssignmentActions = (action: AssignmentActionType) => { + switch (action) { + case AssignmentActionType.Delete: + const { selection } = this.state.tableRef.current.state; + this.props.onDetachBeat(selection.map((beat: any) => beat.id)); + } + }; + + // TODO this should disable save button on bad validations + private updateTag = (key: keyof BeatTag, value?: any) => + value !== undefined + ? this.props.onTagChange(key, value) + : (e: any) => this.props.onTagChange(key, e.target ? e.target.value : e); +} diff --git a/x-pack/plugins/beats_management/public/config_schemas.ts b/x-pack/plugins/beats_management/public/config_schemas.ts new file mode 100644 index 0000000000000..0c6d1ded95cb3 --- /dev/null +++ b/x-pack/plugins/beats_management/public/config_schemas.ts @@ -0,0 +1,370 @@ +/* + * 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 { YamlConfigSchema } from './lib/lib'; + +const filebeatInputConfig: YamlConfigSchema[] = [ + { + id: 'paths', + ui: { + label: 'Paths', + type: 'multi-input', + }, + validations: 'isPaths', + error: 'One file path per line', + required: true, + }, + { + id: 'other', + ui: { + label: 'Other Config', + type: 'code', + }, + validations: 'isYaml', + error: 'Config entered must be in valid YAML format', + }, +]; + +const filebeatModuleConfig: YamlConfigSchema[] = [ + { + id: 'module', + ui: { + label: 'Module', + type: 'select', + }, + options: [ + { + value: 'apache2', + text: 'apache2', + }, + { + value: 'auditd', + text: 'auditd', + }, + { + value: 'elasticsearch', + text: 'elasticsearch', + }, + { + value: 'haproxy', + text: 'haproxy', + }, + { + value: 'icinga', + text: 'icinga', + }, + { + value: 'iis', + text: 'iis', + }, + { + value: 'kafka', + text: 'kafka', + }, + { + value: 'kibana', + text: 'kibana', + }, + { + value: 'logstash', + text: 'logstash', + }, + { + value: 'mongodb', + text: 'mongodb', + }, + { + value: 'mysql', + text: 'mysql', + }, + { + value: 'nginx', + text: 'nginx', + }, + { + value: 'osquery', + text: 'osquery', + }, + { + value: 'postgresql', + text: 'postgresql', + }, + { + value: 'redis', + text: 'redis', + }, + { + value: 'system', + text: 'system', + }, + { + value: 'traefik', + text: 'traefik', + }, + ], + error: 'Please select a module', + required: true, + }, + { + id: 'other', + ui: { + label: 'Other Config', + type: 'code', + }, + validations: 'isYaml', + error: 'Config entered must be in valid YAML format', + }, +]; + +const metricbeatModuleConfig: YamlConfigSchema[] = [ + { + id: 'module', + ui: { + label: 'Module', + type: 'select', + }, + options: [ + { + value: 'aerospike', + text: 'aerospike', + }, + { + value: 'apache', + text: 'apache', + }, + { + value: 'ceph', + text: 'ceph', + }, + { + value: 'couchbase', + text: 'couchbase', + }, + { + value: 'docker', + text: 'docker', + }, + { + value: 'dropwizard', + text: 'dropwizard', + }, + { + value: 'elasticsearch', + text: 'elasticsearch', + }, + { + value: 'envoyproxy', + text: 'envoyproxy', + }, + { + value: 'etcd', + text: 'etcd', + }, + { + value: 'golang', + text: 'golang', + }, + { + value: 'graphite', + text: 'graphite', + }, + { + value: 'haproxy', + text: 'haproxy', + }, + { + value: 'http', + text: 'http', + }, + { + value: 'jolokia', + text: 'jolokia', + }, + { + value: 'kafka', + text: 'kafka', + }, + { + value: 'kibana', + text: 'kibana', + }, + { + value: 'kubernetes', + text: 'kubernetes', + }, + { + value: 'kvm', + text: 'kvm', + }, + { + value: 'logstash', + text: 'logstash', + }, + { + value: 'memcached', + text: 'memcached', + }, + { + value: 'mongodb', + text: 'mongodb', + }, + { + value: 'munin', + text: 'munin', + }, + { + value: 'mysql', + text: 'mysql', + }, + { + value: 'nginx', + text: 'nginx', + }, + { + value: 'php_fpm', + text: 'php_fpm', + }, + { + value: 'postgresql', + text: 'postgresql', + }, + { + value: 'prometheus', + text: 'prometheus', + }, + { + value: 'rabbitmq', + text: 'rabbitmq', + }, + { + value: 'redis', + text: 'redis', + }, + { + value: 'system', + text: 'system', + }, + { + value: 'traefik', + text: 'traefik', + }, + { + value: 'uwsgi', + text: 'uwsgi', + }, + { + value: 'vsphere', + text: 'vsphere', + }, + { + value: 'windows', + text: 'windows', + }, + { + value: 'zookeeper', + text: 'zookeeper', + }, + ], + error: 'Please select a module', + required: true, + }, + { + id: 'hosts', + ui: { + label: 'Hosts', + type: 'multi-input', + }, + validations: 'isHosts', + error: 'One file host per line', + required: false, + }, + { + id: 'period', + ui: { + label: 'Period', + type: 'input', + }, + defaultValue: '10s', + validations: 'isPeriod', + error: 'Invalid Period, must be formatted as `10s` for 10 seconds', + required: true, + }, + { + id: 'other', + ui: { + label: 'Other Config', + type: 'code', + }, + validations: 'isYaml', + error: 'Config entered must be in valid YAML format', + }, +]; + +const outputConfig: YamlConfigSchema[] = [ + { + id: 'output', + ui: { + label: 'Output Type', + type: 'select', + transform: 'removed', + }, + options: [ + { + value: 'elasticsearch', + text: 'Elasticsearch', + }, + { + value: 'logstash', + text: 'Logstash', + }, + { + value: 'kafka', + text: 'Kafka', + }, + { + value: 'console', + text: 'Console', + }, + ], + error: 'Please select an output type', + required: true, + }, + { + id: '{{output}}.hosts', + ui: { + label: 'Hosts', + type: 'multi-input', + }, + validations: 'isHosts', + error: 'One file host per line', + parseValidResult: v => v.split('\n'), + }, + { + id: '{{output}}.username', + ui: { + label: 'Username', + type: 'input', + }, + validations: 'isString', + error: 'Unprocessable username', + }, + { + id: '{{output}}.password', + ui: { + label: 'Password', + type: 'password', + }, + validations: 'isString', + error: 'Unprocessable password', + }, +]; + +export const supportedConfigs = [ + { text: 'Filebeat Input', value: 'filebeat.inputs', config: filebeatInputConfig }, + { text: 'Filebeat Module', value: 'filebeat.modules', config: filebeatModuleConfig }, + { text: 'Metricbeat Module', value: 'metricbeat.modules', config: metricbeatModuleConfig }, + { text: 'Output', value: 'output', config: outputConfig }, +]; diff --git a/x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx new file mode 100644 index 0000000000000..a0a2afb7e63f2 --- /dev/null +++ b/x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx @@ -0,0 +1,89 @@ +/* + * 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 React from 'react'; + +import { AutocompleteSuggestion } from 'ui/autocomplete_providers'; + +import { FrontendLibs } from '../lib/lib'; +import { RendererFunction } from '../utils/typed_react'; + +interface WithKueryAutocompletionLifecycleProps { + libs: FrontendLibs; + fieldPrefix?: string; + children: RendererFunction<{ + isLoadingSuggestions: boolean; + loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; + suggestions: AutocompleteSuggestion[]; + }>; +} + +interface WithKueryAutocompletionLifecycleState { + // lacking cancellation support in the autocompletion api, + // this is used to keep older, slower requests from clobbering newer ones + currentRequest: { + expression: string; + cursorPosition: number; + } | null; + suggestions: AutocompleteSuggestion[]; +} + +export class WithKueryAutocompletion extends React.Component< + WithKueryAutocompletionLifecycleProps, + WithKueryAutocompletionLifecycleState +> { + public readonly state: WithKueryAutocompletionLifecycleState = { + currentRequest: null, + suggestions: [], + }; + + public render() { + const { currentRequest, suggestions } = this.state; + + return this.props.children({ + isLoadingSuggestions: currentRequest !== null, + loadSuggestions: this.loadSuggestions, + suggestions, + }); + } + + private loadSuggestions = async ( + expression: string, + cursorPosition: number, + maxSuggestions?: number + ) => { + this.setState({ + currentRequest: { + expression, + cursorPosition, + }, + suggestions: [], + }); + let suggestions: any[] = []; + try { + suggestions = await this.props.libs.elasticsearch.getSuggestions( + expression, + cursorPosition, + this.props.fieldPrefix + ); + } catch (e) { + suggestions = []; + } + + this.setState( + state => + state.currentRequest && + state.currentRequest.expression !== expression && + state.currentRequest.cursorPosition !== cursorPosition + ? state // ignore this result, since a newer request is in flight + : { + ...state, + currentRequest: null, + suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions, + } + ); + }; +} diff --git a/x-pack/plugins/beats_management/public/containers/with_url_state.tsx b/x-pack/plugins/beats_management/public/containers/with_url_state.tsx new file mode 100644 index 0000000000000..1630c2c20cda8 --- /dev/null +++ b/x-pack/plugins/beats_management/public/containers/with_url_state.tsx @@ -0,0 +1,99 @@ +/* + * 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 { parse, stringify } from 'querystring'; +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import { FlatObject } from '../app'; +import { RendererFunction } from '../utils/typed_react'; + +type StateCallback = (previousState: T) => T; + +export interface URLStateProps { + goTo: (path: string) => void; + setUrlState: ( + newState: + | Partial> + | StateCallback + | Promise> + ) => void; + urlState: URLState; +} +interface ComponentProps { + history: any; + match: any; + children: RendererFunction>; +} + +export class WithURLStateComponent extends React.Component< + ComponentProps +> { + private get URLState(): URLState { + // slice because parse does not account for the initial ? in the search string + return parse(decodeURIComponent(this.props.history.location.search).substring(1)) as URLState; + } + + private historyListener: (() => void) | null = null; + + public componentWillUnmount() { + if (this.historyListener) { + this.historyListener(); + } + } + public render() { + return this.props.children({ + goTo: this.goTo, + setUrlState: this.setURLState, + urlState: this.URLState || {}, + }); + } + + private setURLState = async ( + state: + | Partial> + | StateCallback + | Promise> + ) => { + let newState; + const pastState = this.URLState; + if (typeof state === 'function') { + newState = await state(pastState); + } else { + newState = state; + } + + const search: string = stringify({ + ...(pastState as any), + ...(newState as any), + }); + + const newLocation = { + ...this.props.history.location, + search, + }; + + this.props.history.replace(newLocation); + this.forceUpdate(); + }; + + private goTo = (path: string) => { + this.props.history.push({ + pathname: path, + search: this.props.history.location.search, + }); + }; +} +export const WithURLState = withRouter(WithURLStateComponent); + +export function withUrlState(UnwrappedComponent: React.ComponentType): React.SFC { + return (origProps: OP) => { + return ( + + {(URLProps: URLStateProps) => } + + ); + }; +} diff --git a/x-pack/plugins/beats_management/public/index.tsx b/x-pack/plugins/beats_management/public/index.tsx new file mode 100644 index 0000000000000..ac869eb83c6ac --- /dev/null +++ b/x-pack/plugins/beats_management/public/index.tsx @@ -0,0 +1,30 @@ +/* + * 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 React from 'react'; +import { BASE_PATH } from '../common/constants'; +import { compose } from './lib/compose/kibana'; +import { FrontendLibs } from './lib/lib'; + +// import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json'; +// import { ThemeProvider } from 'styled-components'; +import { PageRouter } from './router'; + +// TODO use theme provided from parentApp when kibana supports it +import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json'; +import '@elastic/eui/dist/eui_theme_light.css'; +import { ThemeProvider } from 'styled-components'; + +function startApp(libs: FrontendLibs) { + libs.framework.registerManagementSection('beats', 'Beats Management', BASE_PATH); + libs.framework.render( + + + + ); +} + +startApp(compose()); diff --git a/x-pack/plugins/beats_management/public/lib/__tests__/tags.test.ts b/x-pack/plugins/beats_management/public/lib/__tests__/tags.test.ts new file mode 100644 index 0000000000000..0c950be147f15 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/__tests__/tags.test.ts @@ -0,0 +1,136 @@ +/* + * 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 { BeatTag } from '../../../common/domain_types'; +import { supportedConfigs } from '../../config_schemas'; +import { CMTagsAdapter } from '../adapters/tags/adapter_types'; +import { TagsLib } from '../tags'; + +describe('Tags Client Domain Lib', () => { + let tagsLib: TagsLib; + + beforeEach(async () => { + tagsLib = new TagsLib({} as CMTagsAdapter, supportedConfigs); + }); + + it('should use helper function to convert users yaml in tag to config object', async () => { + const convertedTag = tagsLib.userConfigsToJson([ + { + id: 'foo', + configuration_blocks: [ + { + type: 'filebeat.inputs', + description: 'string', + configs: [{ paths: ['adad/adasd'], other: "something: 'here'" }], + }, + ], + color: 'red', + last_updated: new Date(), + } as BeatTag, + ]); + + expect(convertedTag.length).toBe(1); + expect(convertedTag[0].configuration_blocks.length).toBe(1); + expect(convertedTag[0].configuration_blocks[0].configs.length).toBe(1); + expect(convertedTag[0].configuration_blocks[0].configs[0]).not.toHaveProperty('other'); + expect(convertedTag[0].configuration_blocks[0].configs[0]).toHaveProperty('something'); + expect((convertedTag[0].configuration_blocks[0].configs[0] as any).something).toBe('here'); + }); + + it('should use helper function to convert user config to json with undefined `other`', async () => { + const convertedTag = tagsLib.userConfigsToJson([ + { + id: 'fsdfsdfs', + color: '#DD0A73', + configuration_blocks: [ + { + type: 'filebeat.inputs', + description: 'sdfsdf', + configs: [{ paths: ['sdfsfsdf'], other: undefined }], + }, + ], + last_updated: '2018-09-04T15:52:08.983Z', + } as any, + ]); + + expect(convertedTag.length).toBe(1); + expect(convertedTag[0].configuration_blocks.length).toBe(1); + expect(convertedTag[0].configuration_blocks[0].configs.length).toBe(1); + expect(convertedTag[0].configuration_blocks[0].configs[0]).not.toHaveProperty('other'); + }); + + it('should use helper function to convert users yaml in tag to config object, where empty other leads to no other fields saved', async () => { + const convertedTag = tagsLib.userConfigsToJson([ + { + id: 'foo', + configuration_blocks: [ + { + type: 'filebeat.inputs', + description: 'string', + configs: [{ paths: ['adad/adasd'], other: '' }], + }, + ], + color: 'red', + last_updated: new Date(), + } as BeatTag, + ]); + + expect(convertedTag.length).toBe(1); + expect(convertedTag[0].configuration_blocks.length).toBe(1); + expect(convertedTag[0].configuration_blocks[0].configs.length).toBe(1); + expect(convertedTag[0].configuration_blocks[0].configs[0]).not.toHaveProperty('other'); + }); + + it('should use helper function to convert config object to users yaml', async () => { + const convertedTag = tagsLib.jsonConfigToUserYaml([ + { + id: 'fsdfsdfs', + color: '#DD0A73', + configuration_blocks: [ + { + type: 'filebeat.inputs', + description: 'sdfsdf', + configs: [{ paths: ['sdfsfsdf'], something: 'here' }], + }, + ], + last_updated: '2018-09-04T15:52:08.983Z', + } as any, + ]); + + expect(convertedTag.length).toBe(1); + expect(convertedTag[0].configuration_blocks.length).toBe(1); + expect(convertedTag[0].configuration_blocks[0].configs.length).toBe(1); + expect(convertedTag[0].configuration_blocks[0].configs[0]).not.toHaveProperty('something'); + expect(convertedTag[0].configuration_blocks[0].configs[0]).toHaveProperty('other'); + + expect(convertedTag[0].configuration_blocks[0].configs[0].other).toBe('something: here\n'); + }); + + it('should use helper function to convert config object to users yaml with empty `other`', async () => { + const convertedTag = tagsLib.jsonConfigToUserYaml([ + { + id: 'fsdfsdfs', + color: '#DD0A73', + configuration_blocks: [ + { + type: 'filebeat.inputs', + description: undefined, + configs: [{ paths: ['sdfsfsdf'] }], + }, + ], + last_updated: '2018-09-04T15:52:08.983Z', + } as any, + ]); + + expect(convertedTag.length).toBe(1); + expect(convertedTag[0].configuration_blocks.length).toBe(1); + expect(convertedTag[0].configuration_blocks[0].configs.length).toBe(1); + expect(convertedTag[0].configuration_blocks[0].configs[0]).not.toHaveProperty('something'); + expect(convertedTag[0].configuration_blocks[0].configs[0]).toHaveProperty('other'); + + expect(convertedTag[0].configuration_blocks[0].configs[0].other).toBe(''); + }); +}); diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts new file mode 100644 index 0000000000000..3808ec1d57422 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts @@ -0,0 +1,36 @@ +/* + * 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 { CMBeat } from '../../../../common/domain_types'; + +export interface CMBeatsAdapter { + get(id: string): Promise; + update(id: string, beatData: Partial): Promise; + getBeatsWithTag(tagId: string): Promise; + getAll(ESQuery?: any): Promise; + removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise; + assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise; + getBeatWithToken(enrollmentToken: string): Promise; +} + +export interface BeatsTagAssignment { + beatId: string; + tag: string; + idxInRequest?: number; +} + +interface BeatsReturnedTagAssignment { + status: number | null; + result?: string; +} + +export interface CMAssignmentReturn { + assignments: BeatsReturnedTagAssignment[]; +} + +export interface BeatsRemovalReturn { + removals: BeatsReturnedTagAssignment[]; +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts new file mode 100644 index 0000000000000..e66e7e45b5c4f --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts @@ -0,0 +1,105 @@ +/* + * 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 { omit } from 'lodash'; + +import { CMBeat } from '../../../../common/domain_types'; +import { + BeatsRemovalReturn, + BeatsTagAssignment, + CMAssignmentReturn, + CMBeatsAdapter, +} from './adapter_types'; + +export class MemoryBeatsAdapter implements CMBeatsAdapter { + private beatsDB: CMBeat[]; + + constructor(beatsDB: CMBeat[]) { + this.beatsDB = beatsDB; + } + + public async get(id: string) { + return this.beatsDB.find(beat => beat.id === id) || null; + } + + public async update(id: string, beatData: Partial): Promise { + const index = this.beatsDB.findIndex(beat => beat.id === id); + + if (index === -1) { + return false; + } + + this.beatsDB[index] = { ...this.beatsDB[index], ...beatData }; + return true; + } + + public async getAll() { + return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); + } + public async getBeatsWithTag(tagId: string): Promise { + return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); + } + + public async getBeatWithToken(enrollmentToken: string): Promise { + return this.beatsDB.map((beat: any) => omit(beat, ['access_token']))[0]; + } + public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { + const beatIds = removals.map(r => r.beatId); + + const response = this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { + const tagData = removals.find(r => r.beatId === beat.id); + if (tagData) { + if (beat.tags) { + beat.tags = beat.tags.filter(tag => tag !== tagData.tag); + } + } + const removalsForBeat = removals.filter(r => r.beatId === beat.id); + if (removalsForBeat.length) { + removalsForBeat.forEach((assignment: BeatsTagAssignment) => { + if (beat.tags) { + beat.tags = beat.tags.filter(tag => tag !== assignment.tag); + } + }); + } + return beat; + }); + + return response.map((item: CMBeat, resultIdx: number) => ({ + idxInRequest: removals[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); + } + + public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise { + const beatIds = assignments.map(r => r.beatId); + + this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { + // get tags that need to be assigned to this beat + const tags = assignments + .filter(a => a.beatId === beat.id) + .map((t: BeatsTagAssignment) => t.tag); + + if (tags.length > 0) { + if (!beat.tags) { + beat.tags = []; + } + const nonExistingTags = tags.filter((t: string) => beat.tags && !beat.tags.includes(t)); + + if (nonExistingTags.length > 0) { + beat.tags = beat.tags.concat(nonExistingTags); + } + } + return beat; + }); + + return assignments.map((item: BeatsTagAssignment, resultIdx: number) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts new file mode 100644 index 0000000000000..8649bf9c37e0e --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts @@ -0,0 +1,57 @@ +/* + * 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 { CMBeat } from '../../../../common/domain_types'; +import { RestAPIAdapter } from '../rest_api/adapter_types'; +import { + BeatsRemovalReturn, + BeatsTagAssignment, + CMAssignmentReturn, + CMBeatsAdapter, +} from './adapter_types'; +export class RestBeatsAdapter implements CMBeatsAdapter { + constructor(private readonly REST: RestAPIAdapter) {} + + public async get(id: string): Promise { + return await this.REST.get(`/api/beats/agent/${id}`); + } + + public async getBeatWithToken(enrollmentToken: string): Promise { + const beat = await this.REST.get(`/api/beats/agent/unknown/${enrollmentToken}`); + return beat; + } + + public async getAll(ESQuery?: any): Promise { + return (await this.REST.get<{ beats: CMBeat[] }>('/api/beats/agents/all', { ESQuery })).beats; + } + + public async getBeatsWithTag(tagId: string): Promise { + return (await this.REST.get<{ beats: CMBeat[] }>(`/api/beats/agents/tag/${tagId}`)).beats; + } + + public async update(id: string, beatData: Partial): Promise { + await this.REST.put<{ success: true }>(`/api/beats/agent/${id}`, beatData); + return true; + } + + public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { + return (await this.REST.post<{ removals: BeatsRemovalReturn[] }>( + `/api/beats/agents_tags/removals`, + { + removals, + } + )).removals; + } + + public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise { + return (await this.REST.post<{ assignments: CMAssignmentReturn[] }>( + `/api/beats/agents_tags/assignments`, + { + assignments, + } + )).assignments; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts new file mode 100644 index 0000000000000..4940857493275 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts @@ -0,0 +1,12 @@ +/* + * 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 { AutocompleteSuggestion } from 'ui/autocomplete_providers'; + +export interface ElasticsearchAdapter { + convertKueryToEsQuery: (kuery: string) => Promise; + getSuggestions: (kuery: string, selectionStart: any) => Promise; + isKueryValid(kuery: string): boolean; +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts new file mode 100644 index 0000000000000..1b918fb72c809 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts @@ -0,0 +1,29 @@ +/* + * 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 { AutocompleteSuggestion } from 'ui/autocomplete_providers'; +import { ElasticsearchAdapter } from './adapter_types'; + +export class MemoryElasticsearchAdapter implements ElasticsearchAdapter { + constructor( + private readonly mockIsKueryValid: (kuery: string) => boolean, + private readonly mockKueryToEsQuery: (kuery: string) => string, + private readonly suggestions: AutocompleteSuggestion[] + ) {} + + public isKueryValid(kuery: string): boolean { + return this.mockIsKueryValid(kuery); + } + public async convertKueryToEsQuery(kuery: string): Promise { + return this.mockKueryToEsQuery(kuery); + } + public async getSuggestions( + kuery: string, + selectionStart: any + ): Promise { + return this.suggestions; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts new file mode 100644 index 0000000000000..d987a18137116 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts @@ -0,0 +1,77 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { AutocompleteSuggestion, getAutocompleteProvider } from 'ui/autocomplete_providers'; +// @ts-ignore TODO type this +import { fromKueryExpression, toElasticsearchQuery } from 'ui/kuery'; +import { RestAPIAdapter } from '../rest_api/adapter_types'; +import { ElasticsearchAdapter } from './adapter_types'; + +export class RestElasticsearchAdapter implements ElasticsearchAdapter { + private cachedIndexPattern: any = null; + constructor(private readonly api: RestAPIAdapter, private readonly indexPatternName: string) {} + + public isKueryValid(kuery: string): boolean { + try { + fromKueryExpression(kuery); + } catch (err) { + return false; + } + + return true; + } + public async convertKueryToEsQuery(kuery: string): Promise { + if (!this.isKueryValid(kuery)) { + return ''; + } + const ast = fromKueryExpression(kuery); + const indexPattern = await this.getIndexPattern(); + return JSON.stringify(toElasticsearchQuery(ast, indexPattern)); + } + public async getSuggestions( + kuery: string, + selectionStart: any + ): Promise { + const autocompleteProvider = getAutocompleteProvider('kuery'); + if (!autocompleteProvider) { + return []; + } + const config = { + get: () => true, + }; + const indexPattern = await this.getIndexPattern(); + + const getAutocompleteSuggestions = autocompleteProvider({ + config, + indexPatterns: [indexPattern], + boolFilter: null, + }); + const results = getAutocompleteSuggestions({ + query: kuery || '', + selectionStart, + selectionEnd: selectionStart, + }); + return results; + } + + private async getIndexPattern() { + if (this.cachedIndexPattern) { + return this.cachedIndexPattern; + } + const res = await this.api.get( + `/api/index_patterns/_fields_for_wildcard?pattern=${this.indexPatternName}` + ); + if (isEmpty(res.fields)) { + return; + } + this.cachedIndexPattern = { + fields: res.fields, + title: `${this.indexPatternName}`, + }; + return this.cachedIndexPattern; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts new file mode 100644 index 0000000000000..a0c24557267a2 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -0,0 +1,205 @@ +/* + * 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 { IModule, IScope } from 'angular'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { + BufferedKibanaServiceCall, + FrameworkAdapter, + KibanaAdapterServiceRefs, + KibanaUIConfig, +} from '../../lib'; + +export class KibanaFrameworkAdapter implements FrameworkAdapter { + public appState: object; + + private management: any; + private adapterService: KibanaAdapterServiceProvider; + private rootComponent: React.ReactElement | null = null; + private uiModule: IModule; + private routes: any; + private XPackInfoProvider: any; + private xpackInfo: null | any; + private notifier: any; + private kbnUrlService: any; + private chrome: any; + + constructor( + uiModule: IModule, + management: any, + routes: any, + chrome: any, + XPackInfoProvider: any, + Notifier: any + ) { + this.adapterService = new KibanaAdapterServiceProvider(); + this.management = management; + this.uiModule = uiModule; + this.routes = routes; + this.chrome = chrome; + this.XPackInfoProvider = XPackInfoProvider; + this.appState = {}; + this.notifier = new Notifier({ location: 'Beats' }); + } + + public get baseURLPath(): string { + return this.chrome.getBasePath(); + } + + public setUISettings = (key: string, value: any) => { + this.adapterService.callOrBuffer(({ config }) => { + config.set(key, value); + }); + }; + + public render = (component: React.ReactElement) => { + this.rootComponent = component; + }; + + public hadValidLicense() { + if (!this.xpackInfo) { + return false; + } + return this.xpackInfo.get('features.beats_management.licenseValid', false); + } + + public securityEnabled() { + if (!this.xpackInfo) { + return false; + } + + return this.xpackInfo.get('features.beats_management.securityEnabled', false); + } + + public registerManagementSection(pluginId: string, displayName: string, basePath: string) { + this.register(this.uiModule); + + this.hookAngular(() => { + if (this.hadValidLicense() && this.securityEnabled()) { + const registerSection = () => + this.management.register(pluginId, { + display: 'Beats', // TODO these need to be config options not hard coded in the adapter + icon: 'logoBeats', + order: 30, + }); + const getSection = () => this.management.getSection(pluginId); + + const section = this.management.hasItem(pluginId) ? getSection() : registerSection(); + + section.register(pluginId, { + visible: true, + display: displayName, + order: 30, + url: `#${basePath}`, + }); + } + + if (!this.securityEnabled()) { + this.notifier.error(this.xpackInfo.get(`features.beats_management.message`)); + this.kbnUrlService.redirect('/management'); + } + }); + } + + private manageAngularLifecycle($scope: any, $route: any, elem: any) { + const lastRoute = $route.current; + const deregister = $scope.$on('$locationChangeSuccess', () => { + const currentRoute = $route.current; + // if templates are the same we are on the same route + if (lastRoute.$$route.template === currentRoute.$$route.template) { + // this prevents angular from destroying scope + $route.current = lastRoute; + } + }); + $scope.$on('$destroy', () => { + if (deregister) { + deregister(); + } + // manually unmount component when scope is destroyed + if (elem) { + ReactDOM.unmountComponentAtNode(elem); + } + }); + } + + private hookAngular(done: () => any) { + this.chrome.dangerouslyGetActiveInjector().then(($injector: any) => { + const Private = $injector.get('Private'); + const xpackInfo = Private(this.XPackInfoProvider); + const kbnUrlService = $injector.get('kbnUrl'); + + this.xpackInfo = xpackInfo; + this.kbnUrlService = kbnUrlService; + done(); + }); + } + + private register = (adapterModule: IModule) => { + const adapter = this; + this.routes.when(`/management/beats_management/:view?/:id?/:other?/:other2?`, { + template: + '
', + controllerAs: 'beatsManagement', + // tslint:disable-next-line: max-classes-per-file + controller: class BeatsManagementController { + constructor($scope: any, $route: any) { + $scope.$$postDigest(() => { + const elem = document.getElementById('beatsReactRoot'); + ReactDOM.render(adapter.rootComponent as React.ReactElement, elem); + adapter.manageAngularLifecycle($scope, $route, elem); + }); + $scope.$onInit = () => { + $scope.topNavMenu = []; + }; + } + }, + }); + }; +} + +// tslint:disable-next-line: max-classes-per-file +class KibanaAdapterServiceProvider { + public serviceRefs: KibanaAdapterServiceRefs | null = null; + public bufferedCalls: Array> = []; + + public $get($rootScope: IScope, config: KibanaUIConfig) { + this.serviceRefs = { + config, + rootScope: $rootScope, + }; + + this.applyBufferedCalls(this.bufferedCalls); + + return this; + } + + public callOrBuffer(serviceCall: (serviceRefs: KibanaAdapterServiceRefs) => void) { + if (this.serviceRefs !== null) { + this.applyBufferedCalls([serviceCall]); + } else { + this.bufferedCalls.push(serviceCall); + } + } + + public applyBufferedCalls( + bufferedCalls: Array> + ) { + if (!this.serviceRefs) { + return; + } + + this.serviceRefs.rootScope.$apply(() => { + bufferedCalls.forEach(serviceCall => { + if (!this.serviceRefs) { + return; + } + return serviceCall(this.serviceRefs); + }); + }); + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts new file mode 100644 index 0000000000000..e9d9bf551f739 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts @@ -0,0 +1,13 @@ +/* + * 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 { FlatObject } from '../../../app'; + +export interface RestAPIAdapter { + get(url: string, query?: FlatObject): Promise; + post(url: string, body?: { [key: string]: any }): Promise; + delete(url: string): Promise; + put(url: string, body?: any): Promise; +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts new file mode 100644 index 0000000000000..690843bbb1cf8 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts @@ -0,0 +1,78 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import { FlatObject } from '../../../app'; +import { RestAPIAdapter } from './adapter_types'; +let globalAPI: AxiosInstance; + +export class AxiosRestAPIAdapter implements RestAPIAdapter { + constructor(private readonly xsrfToken: string, private readonly basePath: string) {} + + public async get(url: string, query?: FlatObject): Promise { + return await this.REST.get(url, query ? { params: query } : {}).then(resp => resp.data); + } + + public async post( + url: string, + body?: { [key: string]: any } + ): Promise { + return await this.REST.post(url, body).then(resp => resp.data); + } + + public async delete(url: string): Promise { + return await this.REST.delete(url).then(resp => resp.data); + } + + public async put(url: string, body?: any): Promise { + return await this.REST.put(url, body).then(resp => resp.data); + } + + private get REST() { + if (globalAPI) { + return globalAPI; + } + + globalAPI = axios.create({ + baseURL: this.basePath, + withCredentials: true, + responseType: 'json', + timeout: 30000, + headers: { + Accept: 'application/json', + credentials: 'same-origin', + 'Content-Type': 'application/json', + 'kbn-version': this.xsrfToken, + 'kbn-xsrf': this.xsrfToken, + }, + }); + // Add a request interceptor + globalAPI.interceptors.request.use( + config => { + // Do something before request is sent + return config; + }, + error => { + // Do something with request error + return Promise.reject(error); + } + ); + + // Add a response interceptor + globalAPI.interceptors.response.use( + response => { + // Do something with response data + return response; + }, + error => { + // Do something with response error + return Promise.reject(error); + } + ); + + return globalAPI; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts new file mode 100644 index 0000000000000..395c01f259dc3 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts @@ -0,0 +1,13 @@ +/* + * 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 { BeatTag } from '../../../../common/domain_types'; + +export interface CMTagsAdapter { + getTagsWithIds(tagIds: string[]): Promise; + delete(tagIds: string[]): Promise; + getAll(): Promise; + upsertTag(tag: BeatTag): Promise; +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts new file mode 100644 index 0000000000000..86daefb47c653 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts @@ -0,0 +1,39 @@ +/* + * 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 { BeatTag } from '../../../../common/domain_types'; +import { CMTagsAdapter } from './adapter_types'; + +export class MemoryTagsAdapter implements CMTagsAdapter { + private tagsDB: BeatTag[] = []; + + constructor(tagsDB: BeatTag[]) { + this.tagsDB = tagsDB; + } + + public async getTagsWithIds(tagIds: string[]) { + return this.tagsDB.filter(tag => tagIds.includes(tag.id)); + } + + public async delete(tagIds: string[]) { + this.tagsDB = this.tagsDB.filter(tag => !tagIds.includes(tag.id)); + return true; + } + + public async getAll() { + return this.tagsDB; + } + + public async upsertTag(tag: BeatTag) { + const existingTagIndex = this.tagsDB.findIndex(t => t.id === tag.id); + if (existingTagIndex !== -1) { + this.tagsDB[existingTagIndex] = tag; + } else { + this.tagsDB.push(tag); + } + return tag; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts new file mode 100644 index 0000000000000..e49d4a9109984 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts @@ -0,0 +1,36 @@ +/* + * 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 { BeatTag } from '../../../../common/domain_types'; +import { RestAPIAdapter } from '../rest_api/adapter_types'; +import { CMTagsAdapter } from './adapter_types'; + +export class RestTagsAdapter implements CMTagsAdapter { + constructor(private readonly REST: RestAPIAdapter) {} + + public async getTagsWithIds(tagIds: string[]): Promise { + const tags = await this.REST.get(`/api/beats/tags/${tagIds.join(',')}`); + return tags; + } + + public async getAll(): Promise { + return await this.REST.get(`/api/beats/tags`); + } + + public async delete(tagIds: string[]): Promise { + return (await this.REST.delete<{ success: boolean }>(`/api/beats/tags/${tagIds.join(',')}`)) + .success; + } + + public async upsertTag(tag: BeatTag): Promise { + const response = await this.REST.put<{ success: boolean }>(`/api/beats/tag/${tag.id}`, { + color: tag.color, + configuration_blocks: tag.configuration_blocks, + }); + + return response.success ? tag : null; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts new file mode 100644 index 0000000000000..55b7e6f94fe04 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export interface CMTokensAdapter { + createEnrollmentToken(): Promise; +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts new file mode 100644 index 0000000000000..f329e491c9ad0 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts @@ -0,0 +1,13 @@ +/* + * 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 { CMTokensAdapter } from './adapter_types'; + +export class MemoryTokensAdapter implements CMTokensAdapter { + public async createEnrollmentToken(): Promise { + return '2jnwkrhkwuehriauhweair'; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts new file mode 100644 index 0000000000000..778bcbf5d8d5c --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts @@ -0,0 +1,18 @@ +/* + * 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 { RestAPIAdapter } from '../rest_api/adapter_types'; +import { CMTokensAdapter } from './adapter_types'; + +export class RestTokensAdapter implements CMTokensAdapter { + constructor(private readonly REST: RestAPIAdapter) {} + + public async createEnrollmentToken(): Promise { + const tokens = (await this.REST.post<{ tokens: string[] }>('/api/beats/enrollment_tokens')) + .tokens; + return tokens[0]; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/beats.ts b/x-pack/plugins/beats_management/public/lib/beats.ts new file mode 100644 index 0000000000000..f676f4611be63 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/beats.ts @@ -0,0 +1,70 @@ +/* + * 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 { flatten } from 'lodash'; +import { CMBeat, CMPopulatedBeat } from './../../common/domain_types'; +import { + BeatsRemovalReturn, + BeatsTagAssignment, + CMAssignmentReturn, + CMBeatsAdapter, +} from './adapters/beats/adapter_types'; +import { FrontendDomainLibs } from './lib'; + +export class BeatsLib { + constructor( + private readonly adapter: CMBeatsAdapter, + private readonly libs: { tags: FrontendDomainLibs['tags'] } + ) {} + + public async get(id: string): Promise { + const beat = await this.adapter.get(id); + return beat ? (await this.mergeInTags([beat]))[0] : null; + } + + public async getBeatWithToken(enrollmentToken: string): Promise { + const beat = await this.adapter.getBeatWithToken(enrollmentToken); + return beat; + } + + public async getBeatsWithTag(tagId: string): Promise { + const beats = await this.adapter.getBeatsWithTag(tagId); + return await this.mergeInTags(beats); + } + + public async getAll(ESQuery?: any): Promise { + const beats = await this.adapter.getAll(ESQuery); + return await this.mergeInTags(beats); + } + + public async update(id: string, beatData: Partial): Promise { + return await this.adapter.update(id, beatData); + } + + public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { + return await this.adapter.removeTagsFromBeats(removals); + } + + public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise { + return await this.adapter.assignTagsToBeats(assignments); + } + + private async mergeInTags(beats: CMBeat[]): Promise { + const tagIds = flatten(beats.map(b => b.tags || [])); + const tags = await this.libs.tags.getTagsWithIds(tagIds); + + // TODO the filter should not be needed, if the data gets into a bad state, we should error + // and inform the user they need to delte the tag, or else we should auto delete it + const mergedBeats: CMPopulatedBeat[] = beats.map( + b => + ({ + ...b, + full_tags: (b.tags || []).map(tagId => tags.find(t => t.id === tagId)).filter(t => t), + } as CMPopulatedBeat) + ); + return mergedBeats; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts new file mode 100644 index 0000000000000..9ff3bbe613d83 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts @@ -0,0 +1,67 @@ +/* + * 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. + */ + +// @ts-ignore +import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; +// @ts-ignore +import 'ui/autoload/all'; +// @ts-ignore: path dynamic for kibana +import chrome from 'ui/chrome'; +// @ts-ignore: path dynamic for kibana +import { management } from 'ui/management'; +// @ts-ignore: path dynamic for kibana +import { uiModules } from 'ui/modules'; +// @ts-ignore +import { Notifier } from 'ui/notify'; +// @ts-ignore: path dynamic for kibana +import routes from 'ui/routes'; + +import { INDEX_NAMES } from '../../../common/constants/index_names'; +import { supportedConfigs } from '../../config_schemas'; +import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter'; +import { RestElasticsearchAdapter } from '../adapters/elasticsearch/rest'; +import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { AxiosRestAPIAdapter } from '../adapters/rest_api/axios_rest_api_adapter'; +import { RestTagsAdapter } from '../adapters/tags/rest_tags_adapter'; +import { RestTokensAdapter } from '../adapters/tokens/rest_tokens_adapter'; +import { BeatsLib } from '../beats'; +import { ElasticsearchLib } from '../elasticsearch'; +import { FrontendDomainLibs, FrontendLibs } from '../lib'; +import { TagsLib } from '../tags'; + +export function compose(): FrontendLibs { + const api = new AxiosRestAPIAdapter(chrome.getXsrfToken(), chrome.getBasePath()); + const esAdapter = new RestElasticsearchAdapter(api, INDEX_NAMES.BEATS); + + const tags = new TagsLib(new RestTagsAdapter(api), supportedConfigs); + const tokens = new RestTokensAdapter(api); + const beats = new BeatsLib(new RestBeatsAdapter(api), { + tags, + }); + + const domainLibs: FrontendDomainLibs = { + tags, + tokens, + beats, + }; + const pluginUIModule = uiModules.get('app/beats_management'); + + const framework = new KibanaFrameworkAdapter( + pluginUIModule, + management, + routes, + chrome, + XPackInfoProvider, + Notifier + ); + + const libs: FrontendLibs = { + framework, + elasticsearch: new ElasticsearchLib(esAdapter), + ...domainLibs, + }; + return libs; +} diff --git a/x-pack/plugins/beats_management/public/lib/compose/memory.ts b/x-pack/plugins/beats_management/public/lib/compose/memory.ts new file mode 100644 index 0000000000000..adfba95df01d8 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/compose/memory.ts @@ -0,0 +1,64 @@ +/* + * 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 'ui/autoload/all'; +// @ts-ignore: path dynamic for kibana +import { management } from 'ui/management'; +// @ts-ignore: path dynamic for kibana +import { uiModules } from 'ui/modules'; +// @ts-ignore: path dynamic for kibana +import routes from 'ui/routes'; +// @ts-ignore: path dynamic for kibana +import { MemoryBeatsAdapter } from '../adapters/beats/memory_beats_adapter'; +import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { MemoryTagsAdapter } from '../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter'; + +import { BeatsLib } from '../beats'; +import { FrontendDomainLibs, FrontendLibs } from '../lib'; + +import { AutocompleteSuggestion } from 'ui/autocomplete_providers'; +import { supportedConfigs } from '../../config_schemas'; +import { TagsLib } from '../tags'; +import { MemoryElasticsearchAdapter } from './../adapters/elasticsearch/memory'; +import { ElasticsearchLib } from './../elasticsearch'; + +export function compose( + mockIsKueryValid: (kuery: string) => boolean, + mockKueryToEsQuery: (kuery: string) => string, + suggestions: AutocompleteSuggestion[] +): FrontendLibs { + const esAdapter = new MemoryElasticsearchAdapter( + mockIsKueryValid, + mockKueryToEsQuery, + suggestions + ); + const tags = new TagsLib(new MemoryTagsAdapter([]), supportedConfigs); + const tokens = new MemoryTokensAdapter(); + const beats = new BeatsLib(new MemoryBeatsAdapter([]), { tags }); + + const domainLibs: FrontendDomainLibs = { + tags, + tokens, + beats, + }; + const pluginUIModule = uiModules.get('app/beats_management'); + + const framework = new KibanaFrameworkAdapter( + pluginUIModule, + management, + routes, + null, + null, + null + ); + const libs: FrontendLibs = { + ...domainLibs, + elasticsearch: new ElasticsearchLib(esAdapter), + framework, + }; + return libs; +} diff --git a/x-pack/plugins/beats_management/public/lib/elasticsearch.ts b/x-pack/plugins/beats_management/public/lib/elasticsearch.ts new file mode 100644 index 0000000000000..7ea32a2eb9467 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/elasticsearch.ts @@ -0,0 +1,69 @@ +/* + * 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 { AutocompleteSuggestion } from 'ui/autocomplete_providers'; +import { ElasticsearchAdapter } from './adapters/elasticsearch/adapter_types'; + +interface HiddenFields { + op: 'is' | 'startsWith' | 'withoutPrefix'; + value: string; +} + +export class ElasticsearchLib { + private readonly hiddenFields: HiddenFields[] = [ + { op: 'startsWith', value: 'enrollment_token' }, + { op: 'is', value: 'beat.active' }, + { op: 'is', value: 'beat.enrollment_token' }, + { op: 'is', value: 'beat.access_token' }, + { op: 'is', value: 'beat.ephemeral_id' }, + { op: 'is', value: 'beat.verified_on' }, + ]; + + constructor(private readonly adapter: ElasticsearchAdapter) {} + + public isKueryValid(kuery: string): boolean { + return this.adapter.isKueryValid(kuery); + } + public async convertKueryToEsQuery(kuery: string): Promise { + return await this.adapter.convertKueryToEsQuery(kuery); + } + + public async getSuggestions( + kuery: string, + selectionStart: any, + fieldPrefix?: string + ): Promise { + const suggestions = await this.adapter.getSuggestions(kuery, selectionStart); + + const filteredSuggestions = suggestions.filter(suggestion => { + const hiddenFieldsCheck = this.hiddenFields; + + if (fieldPrefix) { + hiddenFieldsCheck.push({ + op: 'withoutPrefix', + value: `${fieldPrefix}.`, + }); + } + + return hiddenFieldsCheck.reduce((isvalid, field) => { + if (!isvalid) { + return false; + } + + switch (field.op) { + case 'startsWith': + return !suggestion.text.startsWith(field.value); + case 'is': + return suggestion.text.trim() !== field.value; + case 'withoutPrefix': + return suggestion.text.startsWith(field.value); + } + }, true); + }); + + return filteredSuggestions; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/lib.ts b/x-pack/plugins/beats_management/public/lib/lib.ts new file mode 100644 index 0000000000000..51d3f1fd657d9 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/lib.ts @@ -0,0 +1,88 @@ +/* + * 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 { IModule, IScope } from 'angular'; +import { AxiosRequestConfig } from 'axios'; +import React from 'react'; +import { CMTokensAdapter } from './adapters/tokens/adapter_types'; +import { BeatsLib } from './beats'; +import { ElasticsearchLib } from './elasticsearch'; +import { TagsLib } from './tags'; + +export interface FrontendDomainLibs { + beats: BeatsLib; + tags: TagsLib; + tokens: CMTokensAdapter; +} + +export interface FrontendLibs extends FrontendDomainLibs { + elasticsearch: ElasticsearchLib; + framework: FrameworkAdapter; +} + +export interface YamlConfigSchema { + id: string; + ui: { + label: string; + type: 'input' | 'multi-input' | 'select' | 'code' | 'password'; + helpText?: string; + transform?: 'removed'; + }; + options?: Array<{ value: string; text: string }>; + validations?: 'isHosts' | 'isString' | 'isPeriod' | 'isPath' | 'isPaths' | 'isYaml'; + error: string; + defaultValue?: string; + required?: boolean; + parseValidResult?: (value: any) => any; +} + +export interface FrameworkAdapter { + // Instance vars + appState?: object; + kbnVersion?: string; + baseURLPath: string; + registerManagementSection(pluginId: string, displayName: string, basePath: string): void; + // Methods + setUISettings(key: string, value: any): void; + render(component: React.ReactElement): void; +} + +export interface FramworkAdapterConstructable { + new (uiModule: IModule): FrameworkAdapter; +} + +// TODO: replace AxiosRequestConfig with something more defined +export type RequestConfig = AxiosRequestConfig; + +export interface ApiAdapter { + kbnVersion: string; + + get(url: string, config?: RequestConfig | undefined): Promise; + post(url: string, data?: any, config?: AxiosRequestConfig | undefined): Promise; + delete(url: string, config?: RequestConfig | undefined): Promise; + put(url: string, data?: any, config?: RequestConfig | undefined): Promise; +} + +export interface UiKibanaAdapterScope extends IScope { + breadcrumbs: any[]; + topNavMenu: any[]; +} + +export interface KibanaUIConfig { + get(key: string): any; + set(key: string, value: any): Promise; +} + +export interface KibanaAdapterServiceRefs { + config: KibanaUIConfig; + rootScope: IScope; +} + +export type BufferedKibanaServiceCall = (serviceRefs: ServiceRefs) => void; + +export interface Chrome { + setRootTemplate(template: string): void; +} diff --git a/x-pack/plugins/beats_management/public/lib/tags.ts b/x-pack/plugins/beats_management/public/lib/tags.ts new file mode 100644 index 0000000000000..e204585e85698 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/tags.ts @@ -0,0 +1,98 @@ +/* + * 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 yaml from 'js-yaml'; +import { omit, pick } from 'lodash'; +import { BeatTag, ConfigurationBlock } from '../../common/domain_types'; +import { ConfigContent } from '../../common/domain_types'; +import { CMTagsAdapter } from './adapters/tags/adapter_types'; + +export class TagsLib { + constructor(private readonly adapter: CMTagsAdapter, private readonly tagConfigs: any) {} + + public async getTagsWithIds(tagIds: string[]): Promise { + return this.jsonConfigToUserYaml(await this.adapter.getTagsWithIds(tagIds)); + } + public async delete(tagIds: string[]): Promise { + return await this.adapter.delete(tagIds); + } + public async getAll(): Promise { + return this.jsonConfigToUserYaml(await this.adapter.getAll()); + } + public async upsertTag(tag: BeatTag): Promise { + tag.id = tag.id.replace(' ', '-'); + + return await this.adapter.upsertTag(this.userConfigsToJson([tag])[0]); + } + + public jsonConfigToUserYaml(tags: BeatTag[]): BeatTag[] { + return tags.map(tag => { + const transformedTag: BeatTag = tag as any; + // configuration_blocks yaml, JS cant read YAML so we parse it into JS, + // because beats flattens all fields, and we need more structure. + // we take tagConfigs, grab the config that applies here, render what we can into + // an object, and the rest we assume to be the yaml string that goes + // into the yaml editor... + // NOTE: The perk of this, is that as we support more features via controls + // vs yaml editing, it should "just work", and things that were in YAML + // will now be in the UI forms... + transformedTag.configuration_blocks = (tag.configuration_blocks || []).map(block => { + const { type, description, configs } = block; + const activeConfig = configs[0]; + const thisConfig = this.tagConfigs.find((conf: any) => conf.value === type).config; + const knownConfigIds = thisConfig.map((config: any) => config.id); + + const convertedConfig = knownConfigIds.reduce((blockObj: any, id: keyof ConfigContent) => { + blockObj[id] = + id === 'other' ? yaml.dump(omit(activeConfig, knownConfigIds)) : activeConfig[id]; + + return blockObj; + }, {}); + + // Workaround to empty object passed into dump resulting in this odd output + if (convertedConfig.other && convertedConfig.other === '{}\n') { + convertedConfig.other = ''; + } + + return { + type, + description, + configs: [convertedConfig], + } as ConfigurationBlock; + }); + return transformedTag; + }); + } + + public userConfigsToJson(tags: BeatTag[]): BeatTag[] { + return tags.map(tag => { + const transformedTag: BeatTag = tag as any; + // configurations is the JS representation of the config yaml, + // so here we take that JS and convert it into a YAML string. + // we do so while also flattening "other" into the flat yaml beats expect + transformedTag.configuration_blocks = (tag.configuration_blocks || []).map(block => { + const { type, description, configs } = block; + const activeConfig = configs[0]; + const thisConfig = this.tagConfigs.find((conf: any) => conf.value === type).config; + const knownConfigIds = thisConfig + .map((config: any) => config.id) + .filter((id: string) => id !== 'other'); + + const convertedConfig = { + ...yaml.safeLoad(activeConfig.other), + ...pick(activeConfig, knownConfigIds), + }; + + return { + type, + description, + configs: [convertedConfig], + } as ConfigurationBlock; + }); + + return transformedTag; + }); + } +} diff --git a/x-pack/plugins/beats_management/public/pages/404.tsx b/x-pack/plugins/beats_management/public/pages/404.tsx new file mode 100644 index 0000000000000..956bf90e84927 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/404.tsx @@ -0,0 +1,13 @@ +/* + * 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 React from 'react'; + +export class NotFoundPage extends React.PureComponent { + public render() { + return
No content found
; + } +} diff --git a/x-pack/plugins/beats_management/public/pages/beat/action_section.tsx b/x-pack/plugins/beats_management/public/pages/beat/action_section.tsx new file mode 100644 index 0000000000000..0ab8181794628 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/beat/action_section.tsx @@ -0,0 +1,58 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { first, sortByOrder } from 'lodash'; +import moment from 'moment'; +import React from 'react'; +import { CMPopulatedBeat } from '../../../common/domain_types'; + +interface BeatDetailsActionSectionProps { + beat: CMPopulatedBeat | undefined; +} + +export const BeatDetailsActionSection = ({ beat }: BeatDetailsActionSectionProps) => ( +
+ {beat ? ( + + + + Type:  + {beat.type}. + + + + + Version:  + {beat.version}. + + + {/* TODO: We need a populated field before we can run this code + + + Uptime: 12min. + + */} + {beat.full_tags && + beat.full_tags.length > 0 && ( + + + Last Config Update:{' '} + + {moment( + first(sortByOrder(beat.full_tags, 'last_updated')).last_updated + ).fromNow()} + + . + + + )} + + ) : ( +
Beat not found
+ )} +
+); diff --git a/x-pack/plugins/beats_management/public/pages/beat/activity.tsx b/x-pack/plugins/beats_management/public/pages/beat/activity.tsx new file mode 100644 index 0000000000000..b2d40ad00427f --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/beat/activity.tsx @@ -0,0 +1,14 @@ +/* + * 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 React from 'react'; +import { FrontendLibs } from '../../lib/lib'; + +interface BeatActivityPageProps { + libs: FrontendLibs; +} + +export const BeatActivityPage = (props: BeatActivityPageProps) =>
Beat Activity View
; diff --git a/x-pack/plugins/beats_management/public/pages/beat/detail.tsx b/x-pack/plugins/beats_management/public/pages/beat/detail.tsx new file mode 100644 index 0000000000000..915cbb9d1f91a --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/beat/detail.tsx @@ -0,0 +1,103 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + // @ts-ignore EuiInMemoryTable typings not yet available + EuiInMemoryTable, + EuiLink, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { flatten, get } from 'lodash'; +import React from 'react'; +import { TABLE_CONFIG } from '../../../common/constants'; +import { BeatTag, CMPopulatedBeat } from '../../../common/domain_types'; +import { ConnectedLink } from '../../components/connected_link'; +import { TagBadge } from '../../components/tag'; +import { supportedConfigs } from '../../config_schemas'; + +interface BeatDetailPageProps { + beat: CMPopulatedBeat | undefined; +} + +export const BeatDetailPage = (props: BeatDetailPageProps) => { + const { beat } = props; + if (!beat) { + return
Beat not found
; + } + const configurationBlocks = flatten( + beat.full_tags.map((tag: BeatTag) => { + return tag.configuration_blocks.map(configuration => ({ + // @ts-ignore one of the types on ConfigurationBlock doesn't define a "module" property + module: configuration.configs[0].module || null, + tagId: tag.id, + tagColor: tag.color, + ...beat, + ...configuration, + displayValue: get( + supportedConfigs.find(config => config.value === configuration.type), + 'text', + null + ), + })); + }) + ); + + const columns = [ + { + field: 'displayValue', + name: 'Type', + sortable: true, + render: (value: string | null, configuration: any) => ( + {value || configuration.type} + ), + }, + { + field: 'module', + name: 'Module', + sortable: true, + }, + { + field: 'description', + name: 'Description', + sortable: true, + }, + { + field: 'tagId', + name: 'Tag', + render: (id: string, block: any) => ( + + + + ), + sortable: true, + }, + ]; + return ( + + + +

Configurations

+
+ +

+ You can have multiple configurations applied to an individual tag. These configurations + can repeat or mix types as necessary. For example, you may utilize three metricbeat + configurations alongside one input and filebeat configuration. +

+
+
+ + + +
+ ); +}; diff --git a/x-pack/plugins/beats_management/public/pages/beat/index.tsx b/x-pack/plugins/beats_management/public/pages/beat/index.tsx new file mode 100644 index 0000000000000..0b61d8da2fcce --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/beat/index.tsx @@ -0,0 +1,157 @@ +/* + * 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 { + EuiSpacer, + // @ts-ignore types for EuiTab not currently available + EuiTab, + // @ts-ignore types for EuiTabs not currently available + EuiTabs, +} from '@elastic/eui'; +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { CMPopulatedBeat } from '../../../common/domain_types'; +import { AppURLState } from '../../app'; +import { PrimaryLayout } from '../../components/layouts/primary'; +import { URLStateProps, withUrlState } from '../../containers/with_url_state'; +import { FrontendLibs } from '../../lib/lib'; +import { BeatDetailsActionSection } from './action_section'; +import { BeatActivityPage } from './activity'; +import { BeatDetailPage } from './detail'; +import { BeatTagsPage } from './tags'; + +interface Match { + params: any; +} + +interface BeatDetailsPageProps extends URLStateProps { + location: any; + history: any; + libs: FrontendLibs; + match: Match; +} + +interface BeatDetailsPageState { + beat: CMPopulatedBeat | undefined; + beatId: string; + isLoading: boolean; +} + +class BeatDetailsPageComponent extends React.PureComponent< + BeatDetailsPageProps, + BeatDetailsPageState +> { + constructor(props: BeatDetailsPageProps) { + super(props); + + this.state = { + beat: undefined, + beatId: this.props.match.params.beatId, + isLoading: true, + }; + this.loadBeat(); + } + + public onSelectedTabChanged = (id: string) => { + this.props.history.push({ + pathname: id, + search: this.props.location.search, + }); + }; + + public render() { + const { beat } = this.state; + let id; + let name; + + if (beat) { + id = beat.id; + name = beat.name; + } + const title = this.state.isLoading + ? 'Loading' + : `Beat: ${name || 'No name receved from beat'} (id: ${id})`; + + const tabs = [ + { + id: `/beat/${id}`, + name: 'Config', + disabled: false, + }, + // { + // id: `/beat/${id}/activity`, + // name: 'Beat Activity', + // disabled: false, + // }, + { + id: `/beat/${id}/tags`, + name: 'Configuration Tags', + disabled: false, + }, + ]; + + return ( + }> + + {tabs.map((tab, index) => ( + { + this.props.history.push({ + pathname: tab.id, + search: this.props.location.search, + }); + }} + > + {tab.name} + + ))} + + + + } + /> + ( + this.loadBeat()} + {...props} + /> + )} + /> + ( + + )} + /> + + + ); + } + + private async loadBeat() { + const { beatId } = this.props.match.params; + let beat; + try { + beat = await this.props.libs.beats.get(beatId); + if (!beat) { + throw new Error('beat not found'); + } + } catch (e) { + throw new Error(e); + } + this.setState({ beat, isLoading: false }); + } +} +export const BeatDetailsPage = withUrlState(BeatDetailsPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/beat/tags.tsx b/x-pack/plugins/beats_management/public/pages/beat/tags.tsx new file mode 100644 index 0000000000000..9d0a693e8efa7 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/beat/tags.tsx @@ -0,0 +1,67 @@ +/* + * 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 { EuiGlobalToastList } from '@elastic/eui'; +import React from 'react'; +import { CMPopulatedBeat } from '../../../common/domain_types'; +import { BeatDetailTagsTable, Table } from '../../components/table'; +import { FrontendLibs } from '../../lib/lib'; + +interface BeatTagsPageProps { + beatId: string; + libs: FrontendLibs; + refreshBeat(): void; +} + +interface BeatTagsPageState { + beat: CMPopulatedBeat | null; + notifications: any[]; +} + +export class BeatTagsPage extends React.PureComponent { + private tableRef = React.createRef
(); + constructor(props: BeatTagsPageProps) { + super(props); + + this.state = { + beat: null, + notifications: [], + }; + } + + public async componentWillMount() { + await this.getBeat(); + } + + public render() { + const { beat } = this.state; + return ( +
+
+ + this.setState({ notifications: [] })} + toastLifeTimeMs={5000} + /> + + ); + } + + private getBeat = async () => { + try { + const beat = await this.props.libs.beats.get(this.props.beatId); + this.setState({ beat }); + } catch (e) { + throw new Error(e); + } + }; +} diff --git a/x-pack/plugins/beats_management/public/pages/main/activity.tsx b/x-pack/plugins/beats_management/public/pages/main/activity.tsx new file mode 100644 index 0000000000000..5aa523e3a32f8 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/activity.tsx @@ -0,0 +1,13 @@ +/* + * 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 React from 'react'; + +export class ActivityPage extends React.PureComponent { + public render() { + return
activity logs view
; + } +} diff --git a/x-pack/plugins/beats_management/public/pages/main/beats.tsx b/x-pack/plugins/beats_management/public/pages/main/beats.tsx new file mode 100644 index 0000000000000..d161378134d23 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/beats.tsx @@ -0,0 +1,292 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiGlobalToastList, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, +} from '@elastic/eui'; +import { sortBy } from 'lodash'; +import moment from 'moment'; +import React from 'react'; +import { RouteComponentProps } from 'react-router'; +import { CMPopulatedBeat } from '../../../common/domain_types'; +import { BeatsTagAssignment } from '../../../server/lib/adapters/beats/adapter_types'; +import { AppURLState } from '../../app'; +import { BeatsTableType, Table } from '../../components/table'; +import { beatsListAssignmentOptions } from '../../components/table/assignment_schema'; +import { AssignmentActionType } from '../../components/table/table'; +import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; +import { URLStateProps } from '../../containers/with_url_state'; +import { FrontendLibs } from '../../lib/lib'; +import { EnrollBeatPage } from './enroll_fragment'; + +interface BeatsPageProps extends URLStateProps { + libs: FrontendLibs; + location: any; + beats: CMPopulatedBeat[]; + loadBeats: () => any; +} + +interface BeatsPageState { + notifications: any[]; + tableRef: any; + tags: any[] | null; +} + +interface ActionAreaProps extends URLStateProps, RouteComponentProps { + libs: FrontendLibs; +} + +export class BeatsPage extends React.PureComponent { + public static ActionArea = (props: ActionAreaProps) => ( + + { + // random, but specific number ensures new tab does not overwrite another _newtab in chrome + // and at the same time not truly random so that many clicks of the link open many tabs at this same URL + window.open( + 'https://www.elastic.co/guide/en/beats/libbeat/current/getting-started.html', + '_newtab35628937456' + ); + }} + > + Learn how to install beats + + { + props.goTo(`/overview/beats/enroll`); + }} + > + Enroll Beats + + + {props.location.pathname === '/overview/beats/enroll' && ( + + { + props.goTo(`/overview/beats`); + }} + style={{ width: '640px' }} + > + + Enroll a new Beat + + + + + + + )} + + ); + constructor(props: BeatsPageProps) { + super(props); + + this.state = { + notifications: [], + tableRef: React.createRef(), + tags: null, + }; + } + + public componentDidUpdate(prevProps: any) { + if (this.props.location !== prevProps.location) { + this.props.loadBeats(); + } + } + public render() { + return ( +
+ + {autocompleteProps => ( +
this.props.setUrlState({ beatsKBar: value }), // todo + onSubmit: () => null, // todo + value: this.props.urlState.beatsKBar || '', + }} + assignmentOptions={{ + items: this.state.tags || [], + schema: beatsListAssignmentOptions, + type: 'assignment', + actionHandler: this.handleBeatsActions, + }} + items={sortBy(this.props.beats, 'id') || []} + ref={this.state.tableRef} + type={BeatsTableType} + /> + )} + + this.setState({ notifications: [] })} + toastLifeTimeMs={5000} + /> + + ); + } + + private handleBeatsActions = (action: AssignmentActionType, payload: any) => { + switch (action) { + case AssignmentActionType.Assign: + this.handleBeatTagAssignment(payload); + break; + case AssignmentActionType.Edit: + // TODO: navigate to edit page + break; + case AssignmentActionType.Delete: + this.deleteSelected(); + break; + case AssignmentActionType.Search: + this.handleSearchQuery(payload); + break; + case AssignmentActionType.Reload: + this.loadTags(); + break; + } + + this.props.loadBeats(); + }; + + private handleBeatTagAssignment = async (tagId: string) => { + const selected = this.getSelectedBeats(); + if (selected.some(beat => beat.full_tags.some(({ id }) => id === tagId))) { + await this.removeTagsFromBeats(selected, tagId); + } else { + await this.assignTagsToBeats(selected, tagId); + } + }; + + private deleteSelected = async () => { + const selected = this.getSelectedBeats(); + for (const beat of selected) { + await this.props.libs.beats.update(beat.id, { active: false }); + } + + this.notifyBeatDisenrolled(selected); + + // because the compile code above has a very minor race condition, we wait, + // the max race condition time is really 10ms but doing 100 to be safe + setTimeout(async () => { + await this.props.loadBeats(); + }, 100); + }; + + // todo: add reference to ES filter endpoint + private handleSearchQuery = (query: any) => { + // await this.props.libs.beats.searach(query); + }; + + private loadTags = async () => { + const tags = await this.props.libs.tags.getAll(); + this.setState({ + tags, + }); + }; + + private createBeatTagAssignments = ( + beats: CMPopulatedBeat[], + tagId: string + ): BeatsTagAssignment[] => beats.map(({ id }) => ({ beatId: id, tag: tagId })); + + private removeTagsFromBeats = async (beats: CMPopulatedBeat[], tagId: string) => { + if (beats.length) { + const assignments = this.createBeatTagAssignments(beats, tagId); + await this.props.libs.beats.removeTagsFromBeats(assignments); + await this.refreshData(); + this.notifyUpdatedTagAssociation('remove', assignments, tagId); + } + }; + + private assignTagsToBeats = async (beats: CMPopulatedBeat[], tagId: string) => { + if (beats.length) { + const assignments = this.createBeatTagAssignments(beats, tagId); + await this.props.libs.beats.assignTagsToBeats(assignments); + await this.refreshData(); + this.notifyUpdatedTagAssociation('add', assignments, tagId); + } + }; + + private notifyBeatDisenrolled = async (beats: CMPopulatedBeat[]) => { + let title; + let text; + if (beats.length === 1) { + title = `"${beats[0].name || beats[0].id}" disenrolled`; + text = `Beat with ID "${beats[0].id}" was disenrolled.`; + } else { + title = `${beats.length} beats disenrolled`; + } + + this.setState({ + notifications: this.state.notifications.concat({ + color: 'warning', + id: `disenroll_${new Date()}`, + title, + text, + }), + }); + }; + + private notifyUpdatedTagAssociation = ( + action: 'add' | 'remove', + assignments: BeatsTagAssignment[], + tag: string + ) => { + const actionName = action === 'remove' ? 'Removed' : 'Added'; + const preposition = action === 'remove' ? 'from' : 'to'; + const beatMessage = + assignments.length && assignments.length === 1 + ? `beat "${this.getNameForBeatId(assignments[0].beatId)}"` + : `${assignments.length} beats`; + this.setState({ + notifications: this.state.notifications.concat({ + color: 'success', + id: `tag-${moment.now()}`, + text:

{`${actionName} tag "${tag}" ${preposition} ${beatMessage}.`}

, + title: `Tag ${actionName}`, + }), + }); + }; + + private getNameForBeatId = (beatId: string) => { + const beat = this.props.beats.find(b => b.id === beatId); + if (beat) { + return beat.name; + } + return null; + }; + + private refreshData = async () => { + await this.loadTags(); + await this.props.loadBeats(); + this.state.tableRef.current.setSelection(this.getSelectedBeats()); + }; + + private getSelectedBeats = (): CMPopulatedBeat[] => { + const selectedIds = this.state.tableRef.current.state.selection.map((beat: any) => beat.id); + const beats: CMPopulatedBeat[] = []; + selectedIds.forEach((id: any) => { + const beat: CMPopulatedBeat | undefined = this.props.beats.find(b => b.id === id); + if (beat) { + beats.push(beat); + } + }); + return beats; + }; +} diff --git a/x-pack/plugins/beats_management/public/pages/main/create_tag_fragment.tsx b/x-pack/plugins/beats_management/public/pages/main/create_tag_fragment.tsx new file mode 100644 index 0000000000000..e0f561d10e6f4 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/create_tag_fragment.tsx @@ -0,0 +1,108 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import 'brace/mode/yaml'; + +import 'brace/theme/github'; +import React from 'react'; +import { BeatTag } from '../../../common/domain_types'; +import { AppURLState } from '../../app'; +import { TagEdit } from '../../components/tag'; +import { URLStateProps, withUrlState } from '../../containers/with_url_state'; +import { FrontendLibs } from '../../lib/lib'; + +interface TagPageProps extends URLStateProps { + libs: FrontendLibs; + match: any; +} + +interface TagPageState { + showFlyout: boolean; + tag: BeatTag; +} + +export class CreateTagFragment extends React.PureComponent { + private mode: 'edit' | 'create' = 'create'; + constructor(props: TagPageProps) { + super(props); + this.state = { + showFlyout: false, + tag: { + id: props.urlState.createdTag ? props.urlState.createdTag : '', + color: '#DD0A73', + configuration_blocks: [], + last_updated: new Date(), + }, + }; + + if (props.urlState.createdTag) { + this.mode = 'edit'; + this.loadTag(); + } + } + + public render() { + return ( + + { + this.props.libs.beats.removeTagsFromBeats( + beatIds.map(id => { + return { beatId: id, tag: this.state.tag.id }; + }) + ); + }} + onTagChange={(field: string, value: string | number) => + this.setState(oldState => ({ + tag: { ...oldState.tag, [field]: value }, + })) + } + attachedBeats={null} + /> + + + + + + + Save & Continue + + + + + ); + } + + private loadTag = async () => { + const tags = await this.props.libs.tags.getTagsWithIds([this.state.tag.id]); + if (tags.length > 0) { + this.setState({ + tag: tags[0], + }); + } + }; + + private saveTag = async () => { + const newTag = await this.props.libs.tags.upsertTag(this.state.tag as BeatTag); + if (!newTag) { + return alert('error saving tag'); + } + this.props.setUrlState({ + createdTag: newTag.id, + }); + this.props.goTo(`/overview/initial/review`); + }; +} +export const CreateTagPageFragment = withUrlState(CreateTagFragment); diff --git a/x-pack/plugins/beats_management/public/pages/main/enroll_fragment.tsx b/x-pack/plugins/beats_management/public/pages/main/enroll_fragment.tsx new file mode 100644 index 0000000000000..2cc249127e5b1 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/enroll_fragment.tsx @@ -0,0 +1,280 @@ +/* + * 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 { + // @ts-ignore typings for EuiBasicTable not present in current version + EuiBasicTable, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiModalBody, + // @ts-ignore + EuiSelect, + EuiTitle, +} from '@elastic/eui'; +import { capitalize } from 'lodash'; +import React from 'react'; +import { RouteComponentProps } from 'react-router'; +import { CMBeat } from '../../../common/domain_types'; +import { AppURLState } from '../../app'; +import { URLStateProps, withUrlState } from '../../containers/with_url_state'; +import { FrontendLibs } from '../../lib/lib'; + +interface BeatsProps extends URLStateProps, RouteComponentProps { + match: any; + libs: FrontendLibs; +} +export class EnrollBeat extends React.Component { + private pinging = false; + constructor(props: BeatsProps) { + super(props); + + this.state = { + enrolledBeat: null, + command: 'sudo filebeat', + beatType: 'filebeat', + }; + } + public pingForBeatWithToken = async ( + libs: FrontendLibs, + token: string + ): Promise => { + try { + const beats = await libs.beats.getBeatWithToken(token); + if (!beats) { + throw new Error('no beats'); + } + return beats; + } catch (err) { + if (this.pinging) { + const timeout = (ms: number) => new Promise(res => setTimeout(res, ms)); + await timeout(5000); + return await this.pingForBeatWithToken(libs, token); + } + } + }; + public async componentDidMount() { + if (!this.props.urlState.enrollmentToken) { + const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken(); + this.props.setUrlState({ + enrollmentToken, + }); + } + } + public waitForToken = async (token: string) => { + if (this.pinging) { + return; + } + this.pinging = true; + const enrolledBeat = (await this.pingForBeatWithToken(this.props.libs, token)) as CMBeat; + + this.setState({ + enrolledBeat, + }); + this.pinging = false; + }; + public render() { + if (!this.props.urlState.enrollmentToken) { + return null; + } + if (this.props.urlState.enrollmentToken && !this.state.enrolledBeat) { + this.waitForToken(this.props.urlState.enrollmentToken); + } + const { goTo } = this.props; + + const actions = []; + + switch (this.props.location.pathname) { + case '/overview/initial/beats': + actions.push({ + goTo: '/overview/initial/tag', + name: 'Continue', + }); + break; + case '/overview/beats/enroll': + actions.push({ + goTo: '/overview/beats/enroll', + name: 'Enroll another Beat', + newToken: true, + }); + actions.push({ + goTo: '/overview/beats', + name: 'Done', + clearToken: true, + }); + break; + } + + return ( + + {!this.state.enrolledBeat && ( + + + + + + +

Select your beat type:

+
+
+
+ this.setState({ beatType: e.target.value })} + fullWidth={true} + /> +
+
+ +
+
+ + + + + +

Select your operating system:

+
+
+
+ ${ + this.state.beatType + }.exe`, + label: 'Windows', + }, + { + value: `./${this.state.beatType}`, + label: 'MacOS', + }, + { + value: `sudo ${this.state.beatType}`, + label: 'RPM', + }, + ]} + onChange={(e: any) => this.setState({ command: e.target.value })} + fullWidth={true} + /> +
+
+
+
+ {this.state.command && ( + + + + + +

Run the following command to enroll your beat

+
+
+
+
+
+ $ {this.state.command} enroll {window.location.protocol} + {`//`} + {window.location.host} + {this.props.libs.framework.baseURLPath + ? this.props.libs.framework.baseURLPath + : ''}{' '} + {this.props.urlState.enrollmentToken} +
+
+
+
+ + + + + +

Waiting for enroll command to be run...

+
+
+
+
+
+
+ +
+
+ )} +
+ )} + {this.state.enrolledBeat && ( + + A Beat was enrolled with the following data: +
+
+
+ +
+
+ {actions.map(action => ( + { + if (action.clearToken) { + this.props.setUrlState({ enrollmentToken: '' }); + } + + if (action.newToken) { + const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken(); + + this.props.setUrlState({ enrollmentToken }); + return this.setState({ + enrolledBeat: null, + }); + } + goTo(action.goTo); + }} + > + {action.name} + + ))} +
+ )} +
+ ); + } +} + +export const EnrollBeatPage = withUrlState(EnrollBeat); diff --git a/x-pack/plugins/beats_management/public/pages/main/index.tsx b/x-pack/plugins/beats_management/public/pages/main/index.tsx new file mode 100644 index 0000000000000..ae016f8f4b73a --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/index.tsx @@ -0,0 +1,256 @@ +/* + * 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 { + EuiCode, + // @ts-ignore + EuiTab, + // @ts-ignore + EuiTabs, +} from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; +import React from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import { CMPopulatedBeat } from '../../../common/domain_types'; +import { AppURLState } from '../../app'; +import { ConnectedLink } from '../../components/connected_link'; +import { NoDataLayout } from '../../components/layouts/no_data'; +import { PrimaryLayout } from '../../components/layouts/primary'; +import { WalkthroughLayout } from '../../components/layouts/walkthrough'; +import { URLStateProps, withUrlState } from '../../containers/with_url_state'; +import { FrontendLibs } from '../../lib/lib'; +import { ActivityPage } from './activity'; +import { BeatsPage } from './beats'; +import { CreateTagPageFragment } from './create_tag_fragment'; +import { EnrollBeatPage } from './enroll_fragment'; +import { TagsPage } from './tags'; +import { ReviewWalkthroughPage } from './walkthrough_review'; + +interface MainPagesProps extends URLStateProps { + libs: FrontendLibs; + location: any; +} + +interface MainPagesState { + enrollBeat?: { + enrollmentToken: string; + } | null; + beats: CMPopulatedBeat[]; + unfilteredBeats: CMPopulatedBeat[]; + loadedBeatsAtLeastOnce: boolean; +} + +class MainPagesComponent extends React.PureComponent { + private mounted: boolean = false; + + constructor(props: MainPagesProps) { + super(props); + this.state = { + loadedBeatsAtLeastOnce: false, + beats: [], + unfilteredBeats: [], + }; + } + public onSelectedTabChanged = (id: string) => { + this.props.goTo(id); + }; + + public componentDidMount() { + this.mounted = true; + this.loadBeats(); + } + + public componentWillUnmount() { + this.mounted = false; + } + + public render() { + if ( + this.state.loadedBeatsAtLeastOnce && + this.state.unfilteredBeats.length === 0 && + !this.props.location.pathname.includes('/overview/initial') + ) { + return ; + } + const tabs = [ + { + id: '/overview/beats', + name: 'Beats List', + disabled: false, + }, + // { + // id: '/overview/activity', + // name: 'Beats Activity', + // disabled: false, + // }, + { + id: '/overview/tags', + name: 'Configuration Tags', + disabled: false, + }, + ]; + + const walkthroughSteps = [ + { + id: '/overview/initial/beats', + name: 'Enroll Beat', + disabled: false, + page: EnrollBeatPage, + }, + { + id: '/overview/initial/tag', + name: 'Create Configuration Tag', + disabled: false, + page: CreateTagPageFragment, + }, + { + id: '/overview/initial/review', + name: 'Review', + disabled: false, + page: ReviewWalkthroughPage, + }, + ]; + + if (this.props.location.pathname === '/overview/initial/help') { + return ( + + + Enroll Beat + + + } + > +

+ You don’t have any Beat configured to use Central Management, click on{' '} + Enroll Beat to add one now. +

+
+ ); + } + + if (this.props.location.pathname.includes('/overview/initial')) { + return ( + + + {walkthroughSteps.map(step => ( + ( + + )} + /> + ))} + + + ); + } + + const renderedTabs = tabs.map((tab, index) => ( + this.onSelectedTabChanged(tab.id)} + isSelected={tab.id === this.props.location.pathname} + disabled={tab.disabled} + key={index} + > + {tab.name} + + )); + + return ( + + ( + + )} + /> + ( + + )} + /> + + } + > + {renderedTabs} + + + ( + + )} + /> + ( + + )} + /> + } + /> + + + ); + } + + private loadBeats = async () => { + let query; + if (this.props.urlState.beatsKBar) { + query = await this.props.libs.elasticsearch.convertKueryToEsQuery( + this.props.urlState.beatsKBar + ); + } + + let beats: CMPopulatedBeat[]; + let unfilteredBeats: CMPopulatedBeat[]; + try { + [beats, unfilteredBeats] = await Promise.all([ + this.props.libs.beats.getAll(query), + this.props.libs.beats.getAll(), + ]); + } catch (e) { + beats = []; + unfilteredBeats = []; + } + if (this.mounted) { + this.setState({ + loadedBeatsAtLeastOnce: true, + beats, + unfilteredBeats, + }); + } + }; +} + +export const MainPages = withUrlState(MainPagesComponent); diff --git a/x-pack/plugins/beats_management/public/pages/main/tags.tsx b/x-pack/plugins/beats_management/public/pages/main/tags.tsx new file mode 100644 index 0000000000000..ad5ebdaca615e --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/tags.tsx @@ -0,0 +1,77 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import React from 'react'; +import { BeatTag } from '../../../common/domain_types'; +import { AppURLState } from '../../app'; +import { Table, TagsTableType } from '../../components/table'; +import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; +import { URLStateProps } from '../../containers/with_url_state'; +import { FrontendLibs } from '../../lib/lib'; + +interface TagsPageProps extends URLStateProps { + libs: FrontendLibs; +} + +interface TagsPageState { + tags: BeatTag[]; +} + +export class TagsPage extends React.PureComponent { + public static ActionArea = ({ goTo }: TagsPageProps) => ( + { + goTo('/tag/create'); + }} + > + Add Tag + + ); + + constructor(props: TagsPageProps) { + super(props); + + this.state = { + tags: [], + }; + + this.loadTags(); + } + + public render() { + return ( + + {autocompleteProps => ( +
this.props.setUrlState({ tagsKBar: value }), + onSubmit: () => null, // todo + value: this.props.urlState.tagsKBar || '', + }} + hideTableControls={true} + items={this.state.tags} + type={TagsTableType} + /> + )} + + ); + } + + private async loadTags() { + const tags = await this.props.libs.tags.getAll(); + this.setState({ + tags, + }); + } +} diff --git a/x-pack/plugins/beats_management/public/pages/main/walkthrough_review.tsx b/x-pack/plugins/beats_management/public/pages/main/walkthrough_review.tsx new file mode 100644 index 0000000000000..83d39d54389b1 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/walkthrough_review.tsx @@ -0,0 +1,106 @@ +/* + * 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 { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui'; +import React from 'react'; +import { RouteComponentProps } from 'react-router'; +import { BeatTag, CMBeat } from '../../../common/domain_types'; +import { BeatsTagAssignment } from '../../../server/lib/adapters/beats/adapter_types'; +import { AppURLState } from '../../app'; +import { URLStateProps, withUrlState } from '../../containers/with_url_state'; +import { FrontendLibs } from '../../lib/lib'; +interface PageProps extends URLStateProps, RouteComponentProps { + loadBeats: any; + libs: FrontendLibs; +} +export class ReviewWalkthrough extends React.Component { + constructor(props: PageProps) { + super(props); + + this.state = { + assigned: false, + }; + } + + public componentDidMount() { + setTimeout(async () => { + await this.props.loadBeats(); + + const done = await this.assignTagToBeat(); + + if (done) { + this.setState({ + assigned: true, + }); + } + }, 300); + } + + public render() { + const { goTo } = this.props; + + return ( + + + + Congratulations!} + body={ + +

+ You have enrolled your first beat, and we have assigned your new tag with its + configurations to it +

+

Next Steps

+

All that is left to do is to start the beat you just enrolled.

+
+ } + actions={ + { + goTo('/overview/beats'); + }} + > + Done + + } + /> +
+
+
+ ); + } + + private createBeatTagAssignments = (beats: CMBeat[], tag: BeatTag): BeatsTagAssignment[] => + beats.map(({ id }) => ({ beatId: id, tag: tag.id })); + + private assignTagToBeat = async () => { + if (!this.props.urlState.enrollmentToken) { + return alert('Invalid URL, no enrollmentToken found'); + } + if (!this.props.urlState.createdTag) { + return alert('Invalid URL, no createdTag found'); + } + + const beat = await this.props.libs.beats.getBeatWithToken(this.props.urlState.enrollmentToken); + if (!beat) { + return alert('Error: Beat not enrolled properly'); + } + const tags = await this.props.libs.tags.getTagsWithIds([this.props.urlState.createdTag]); + + const assignments = this.createBeatTagAssignments([beat], tags[0]); + await this.props.libs.beats.assignTagsToBeats(assignments); + this.props.setUrlState({ + createdTag: '', + enrollmentToken: '', + }); + return true; + }; +} + +export const ReviewWalkthroughPage = withUrlState(ReviewWalkthrough); diff --git a/x-pack/plugins/beats_management/public/pages/tag/index.tsx b/x-pack/plugins/beats_management/public/pages/tag/index.tsx new file mode 100644 index 0000000000000..90b20b60fe2da --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/tag/index.tsx @@ -0,0 +1,121 @@ +/* + * 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 { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import 'brace/mode/yaml'; + +import 'brace/theme/github'; +import React from 'react'; +import { BeatTag, CMPopulatedBeat } from '../../../common/domain_types'; +import { AppURLState } from '../../app'; +import { PrimaryLayout } from '../../components/layouts/primary'; +import { TagEdit } from '../../components/tag'; +import { URLStateProps, withUrlState } from '../../containers/with_url_state'; +import { FrontendLibs } from '../../lib/lib'; + +interface TagPageProps extends URLStateProps { + libs: FrontendLibs; + match: any; +} + +interface TagPageState { + showFlyout: boolean; + attachedBeats: CMPopulatedBeat[] | null; + tag: BeatTag; +} + +export class TagPageComponent extends React.PureComponent { + private mode: 'edit' | 'create' = 'create'; + constructor(props: TagPageProps) { + super(props); + this.state = { + showFlyout: false, + attachedBeats: null, + tag: { + id: props.match.params.action === 'create' ? '' : props.match.params.tagid, + color: '#DD0A73', + configuration_blocks: [], + last_updated: new Date(), + }, + }; + + if (props.match.params.action !== 'create') { + this.mode = 'edit'; + this.loadTag(); + this.loadAttachedBeats(); + } + } + + public render() { + return ( + +
+ { + await this.props.libs.beats.removeTagsFromBeats( + beatIds.map(id => { + return { beatId: id, tag: this.state.tag.id }; + }) + ); + await this.loadAttachedBeats(); + }} + onTagChange={(field: string, value: string | number) => + this.setState(oldState => ({ + tag: { ...oldState.tag, [field]: value }, + })) + } + attachedBeats={this.state.attachedBeats} + /> + + + + + Save + + + + this.props.goTo('/overview/tags')}> + Cancel + + + +
+
+ ); + } + private loadTag = async () => { + const tags = await this.props.libs.tags.getTagsWithIds([this.props.match.params.tagid]); + if (tags.length === 0) { + // TODO do something to error + } + this.setState({ + tag: tags[0], + }); + }; + + private loadAttachedBeats = async () => { + const beats = await this.props.libs.beats.getBeatsWithTag(this.props.match.params.tagid); + + this.setState({ + attachedBeats: beats, + }); + }; + private saveTag = async () => { + await this.props.libs.tags.upsertTag(this.state.tag as BeatTag); + this.props.goTo(`/overview/tags`); + }; +} +export const TagPage = withUrlState(TagPageComponent); diff --git a/x-pack/plugins/beats_management/public/router.tsx b/x-pack/plugins/beats_management/public/router.tsx new file mode 100644 index 0000000000000..ef4b03c2e439e --- /dev/null +++ b/x-pack/plugins/beats_management/public/router.tsx @@ -0,0 +1,51 @@ +/* + * 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 React from 'react'; +import { HashRouter, Redirect, Route, Switch } from 'react-router-dom'; + +import { Header } from './components/layouts/header'; +import { FrontendLibs } from './lib/lib'; +import { BeatDetailsPage } from './pages/beat'; +import { MainPages } from './pages/main'; +import { TagPage } from './pages/tag'; + +export const PageRouter: React.SFC<{ libs: FrontendLibs }> = ({ libs }) => { + return ( + +
+
+ + } + /> + } /> + } + /> + } + /> + +
+
+ ); +}; diff --git a/x-pack/plugins/beats_management/public/utils/typed_react.ts b/x-pack/plugins/beats_management/public/utils/typed_react.ts new file mode 100644 index 0000000000000..5557befa9d7e5 --- /dev/null +++ b/x-pack/plugins/beats_management/public/utils/typed_react.ts @@ -0,0 +1,65 @@ +/* + * 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 { omit } from 'lodash'; +import React from 'react'; +import { InferableComponentEnhancerWithProps } from 'react-redux'; + +export type RendererResult = React.ReactElement | null; +export type RendererFunction = (args: RenderArgs) => Result; + +export type ChildFunctionRendererProps = { + children: RendererFunction; + initializeOnMount?: boolean; + resetOnUnmount?: boolean; +} & RenderArgs; + +interface ChildFunctionRendererOptions { + onInitialize?: (props: RenderArgs) => void; + onCleanup?: (props: RenderArgs) => void; +} + +export const asChildFunctionRenderer = ( + hoc: InferableComponentEnhancerWithProps, + { onInitialize, onCleanup }: ChildFunctionRendererOptions = {} +) => + hoc( + class ChildFunctionRenderer extends React.Component> { + public displayName = 'ChildFunctionRenderer'; + + public componentDidMount() { + if (this.props.initializeOnMount && onInitialize) { + onInitialize(this.getRendererArgs()); + } + } + + public componentWillUnmount() { + if (this.props.resetOnUnmount && onCleanup) { + onCleanup(this.getRendererArgs()); + } + } + + public render() { + return this.props.children(this.getRendererArgs()); + } + + private getRendererArgs = () => + omit(['children', 'initializeOnMount', 'resetOnUnmount'], this.props) as Pick< + ChildFunctionRendererProps, + keyof InjectedProps + >; + } + ); + +export type StateUpdater = ( + prevState: Readonly, + prevProps: Readonly +) => State | null; + +export function composeStateUpdaters(...updaters: Array>) { + return (state: State, props: Props) => + updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); +} diff --git a/x-pack/plugins/beats_management/readme.md b/x-pack/plugins/beats_management/readme.md new file mode 100644 index 0000000000000..3a9717a58f9a5 --- /dev/null +++ b/x-pack/plugins/beats_management/readme.md @@ -0,0 +1,22 @@ +# Documentation for Beats CM in x-pack kibana + +Notes: +Falure to have auth enabled in Kibana will make for a broken UI. UI based errors not yet in place + +### Run tests + +``` +node scripts/jest.js plugins/beats --watch +``` + +and for functional... (from x-pack root) + +``` + node scripts/functional_tests --config test/api_integration/config +``` + +### Run command to fake an enrolling beat (from beats_management dir) + +``` +node scripts/enroll.js +``` diff --git a/x-pack/plugins/beats_management/scripts/enroll.js b/x-pack/plugins/beats_management/scripts/enroll.js new file mode 100644 index 0000000000000..e1639be281da4 --- /dev/null +++ b/x-pack/plugins/beats_management/scripts/enroll.js @@ -0,0 +1,35 @@ +/* + * 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. + */ +const request = require('request'); +const Chance = require('chance'); // eslint-disable-line +const args = process.argv.slice(2); +const chance = new Chance(); + +const enroll = async token => { + const beatId = chance.word(); + + await request( + { + url: `http://localhost:5601/api/beats/agent/${beatId}`, + method: 'POST', + headers: { + 'kbn-xsrf': 'xxx', + 'kbn-beats-enrollment-token': token, + }, + body: JSON.stringify({ + type: 'filebeat', + host_name: `${chance.word()}.bar.com`, + name: chance.word(), + version: '6.3.0', + }), + }, + (error, response, body) => { + console.log(error, body); + } + ); +}; + +enroll(args[0]); diff --git a/x-pack/plugins/beats_management/server/kibana.index.ts b/x-pack/plugins/beats_management/server/kibana.index.ts new file mode 100644 index 0000000000000..dd7bc443bc603 --- /dev/null +++ b/x-pack/plugins/beats_management/server/kibana.index.ts @@ -0,0 +1,13 @@ +/* + * 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 { compose } from './lib/compose/kibana'; +import { initManagementServer } from './management_server'; + +export const initServerWithKibana = (hapiServer: any) => { + const libs = compose(hapiServer); + initManagementServer(libs); +}; diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts new file mode 100644 index 0000000000000..74c778fbe5672 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts @@ -0,0 +1,45 @@ +/* + * 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 { CMBeat } from '../../../../common/domain_types'; +import { FrameworkUser } from '../framework/adapter_types'; + +export interface CMBeatsAdapter { + insert(user: FrameworkUser, beat: CMBeat): Promise; + update(user: FrameworkUser, beat: CMBeat): Promise; + get(user: FrameworkUser, id: string): Promise; + getAll(user: FrameworkUser, ESQuery?: any): Promise; + getWithIds(user: FrameworkUser, beatIds: string[]): Promise; + getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise; + getBeatWithToken(user: FrameworkUser, enrollmentToken: string): Promise; + removeTagsFromBeats( + user: FrameworkUser, + removals: BeatsTagAssignment[] + ): Promise; + assignTagsToBeats( + user: FrameworkUser, + assignments: BeatsTagAssignment[] + ): Promise; +} + +export interface BeatsTagAssignment { + beatId: string; + tag: string; + idxInRequest?: number; +} + +interface BeatsReturnedTagAssignment { + status: number | null; + result?: string; +} + +export interface CMAssignmentReturn { + assignments: BeatsReturnedTagAssignment[]; +} + +export interface BeatsRemovalReturn { + removals: BeatsReturnedTagAssignment[]; +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts new file mode 100644 index 0000000000000..66fd00a300fa9 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -0,0 +1,239 @@ +/* + * 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 { flatten, get as _get, omit } from 'lodash'; +import { INDEX_NAMES } from '../../../../common/constants'; +import { CMBeat } from '../../../../common/domain_types'; +import { DatabaseAdapter } from '../database/adapter_types'; + +import { FrameworkUser } from '../framework/adapter_types'; +import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; + +export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { + private database: DatabaseAdapter; + + constructor(database: DatabaseAdapter) { + this.database = database; + } + + public async get(user: FrameworkUser, id: string) { + const params = { + id: `beat:${id}`, + ignore: [404], + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + + const response = await this.database.get(user, params); + if (!response.found) { + return null; + } + + return _get(response, '_source.beat'); + } + + public async insert(user: FrameworkUser, beat: CMBeat) { + const body = { + beat, + type: 'beat', + }; + + await this.database.index(user, { + body, + id: `beat:${beat.id}`, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }); + } + + public async update(user: FrameworkUser, beat: CMBeat) { + const body = { + beat, + type: 'beat', + }; + + const params = { + body, + id: `beat:${beat.id}`, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }; + await this.database.index(user, params); + } + + public async getWithIds(user: FrameworkUser, beatIds: string[]) { + const ids = beatIds.map(beatId => `beat:${beatId}`); + + const params = { + _sourceInclude: ['beat.id', 'beat.verified_on'], + body: { + ids, + }, + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + const response = await this.database.mget(user, params); + + return _get(response, 'docs', []) + .filter((b: any) => b.found) + .map((b: any) => b._source.beat); + } + + public async getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise { + const params = { + ignore: [404], + index: INDEX_NAMES.BEATS, + type: '_doc', + body: { + query: { + terms: { 'beat.tags': tagIds }, + }, + }, + }; + + const response = await this.database.search(user, params); + + const beats = _get(response, 'hits.hits', []); + + if (beats.length === 0) { + return []; + } + return beats.map((beat: any) => omit(beat._source.beat, ['access_token'])); + } + + public async getBeatWithToken( + user: FrameworkUser, + enrollmentToken: string + ): Promise { + const params = { + ignore: [404], + index: INDEX_NAMES.BEATS, + type: '_doc', + body: { + query: { + match: { 'beat.enrollment_token': enrollmentToken }, + }, + }, + }; + + const response = await this.database.search(user, params); + + const beats = _get(response, 'hits.hits', []); + + if (beats.length === 0) { + return null; + } + return omit(_get(beats[0], '_source.beat'), ['access_token']); + } + + public async getAll(user: FrameworkUser, ESQuery?: any) { + const params = { + index: INDEX_NAMES.BEATS, + size: 10000, + type: '_doc', + body: { + query: { + bool: { + must: { + term: { + type: 'beat', + }, + }, + }, + }, + }, + }; + + if (ESQuery) { + params.body.query = { + ...params.body.query, + ...ESQuery, + }; + } + + let response; + try { + response = await this.database.search(user, params); + } catch (e) { + // TODO something + } + if (!response) { + return []; + } + const beats = _get(response, 'hits.hits', []); + + return beats.map((beat: any) => omit(beat._source.beat, ['access_token'])); + } + + public async removeTagsFromBeats( + user: FrameworkUser, + removals: BeatsTagAssignment[] + ): Promise { + 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.replace(' ', ''), params: { tag } } }, + ]; + }) + ); + + const response = await this.database.bulk(user, { + body, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }); + return _get(response, 'items', []).map((item: any, resultIdx: number) => ({ + idxInRequest: removals[resultIdx].idxInRequest, + result: item.update.result, + status: item.update.status, + })); + } + + public async assignTagsToBeats( + user: FrameworkUser, + assignments: BeatsTagAssignment[] + ): Promise { + const body = flatten( + assignments.map(({ beatId, tag }) => { + const script = ` + def beat = ctx._source.beat; + if (beat.tags == null) { + beat.tags = []; + } + if (!beat.tags.contains(params.tag)) { + beat.tags.add(params.tag); + }`; + + return [ + { update: { _id: `beat:${beatId}` } }, + { script: { source: script.replace(' ', ''), params: { tag } } }, + ]; + }) + ); + + const response = await this.database.bulk(user, { + body, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }); + return _get(response, 'items', []).map((item: any, resultIdx: any) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: item.update.result, + status: item.update.status, + })); + } +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts new file mode 100644 index 0000000000000..6ae1c018a5490 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts @@ -0,0 +1,114 @@ +/* + * 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 { intersection, omit } from 'lodash'; + +import { CMBeat } from '../../../../common/domain_types'; +import { FrameworkUser } from '../framework/adapter_types'; +import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; + +export class MemoryBeatsAdapter implements CMBeatsAdapter { + private beatsDB: CMBeat[]; + + constructor(beatsDB: CMBeat[]) { + this.beatsDB = beatsDB; + } + + public async get(user: FrameworkUser, id: string) { + return this.beatsDB.find(beat => beat.id === id) || null; + } + + public async insert(user: FrameworkUser, beat: CMBeat) { + this.beatsDB.push(beat); + } + + public async update(user: FrameworkUser, beat: CMBeat) { + const beatIndex = this.beatsDB.findIndex(b => b.id === beat.id); + + this.beatsDB[beatIndex] = { + ...this.beatsDB[beatIndex], + ...beat, + }; + } + + public async getWithIds(user: FrameworkUser, beatIds: string[]) { + return this.beatsDB.filter(beat => beatIds.includes(beat.id)); + } + + public async getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise { + return this.beatsDB.filter(beat => intersection(tagIds, beat.tags || []).length !== 0); + } + + public async getBeatWithToken( + user: FrameworkUser, + enrollmentToken: string + ): Promise { + return this.beatsDB.find(beat => enrollmentToken === beat.enrollment_token) || null; + } + + public async getAll(user: FrameworkUser) { + return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); + } + + public async removeTagsFromBeats( + user: FrameworkUser, + removals: BeatsTagAssignment[] + ): Promise { + const beatIds = removals.map(r => r.beatId); + + const response = this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { + const tagData = removals.find(r => r.beatId === beat.id); + if (tagData) { + if (beat.tags) { + beat.tags = beat.tags.filter(tag => tag !== tagData.tag); + } + } + return beat; + }); + + return response.map((item: CMBeat, resultIdx: number) => ({ + idxInRequest: removals[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); + } + + public async assignTagsToBeats( + user: FrameworkUser, + assignments: BeatsTagAssignment[] + ): Promise { + const beatIds = assignments.map(r => r.beatId); + + this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { + // get tags that need to be assigned to this beat + const tags = assignments + .filter(a => a.beatId === beat.id) + .map((t: BeatsTagAssignment) => t.tag); + + if (tags.length > 0) { + if (!beat.tags) { + beat.tags = []; + } + const nonExistingTags = tags.filter((t: string) => beat.tags && !beat.tags.includes(t)); + + if (nonExistingTags.length > 0) { + beat.tags = beat.tags.concat(nonExistingTags); + } + } + return beat; + }); + + return assignments.map((item: BeatsTagAssignment, resultIdx: number) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); + } + + public setDB(beatsDB: CMBeat[]) { + this.beatsDB = beatsDB; + } +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.test.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.test.ts new file mode 100644 index 0000000000000..bcbb1e9a29b49 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.test.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ +// file.skip + +// @ts-ignore +import { createEsTestCluster } from '@kbn/test'; +import { Root } from 'src/core/server/root'; +// @ts-ignore +import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server'; +import { DatabaseKbnESPlugin } from '../adapter_types'; +import { KibanaDatabaseAdapter } from '../kibana_database_adapter'; +import { contractTests } from './test_contract'; +const es = createEsTestCluster({}); + +let legacyServer: any; +let rootServer: Root; +contractTests('Kibana Database Adapter', { + before: async () => { + await es.start(); + + rootServer = kbnTestServer.createRootWithCorePlugins({ + server: { maxPayloadBytes: 100 }, + }); + + await rootServer.start(); + legacyServer = kbnTestServer.getKbnServer(rootServer); + return await legacyServer.plugins.elasticsearch.waitUntilReady(); + }, + after: async () => { + await rootServer.shutdown(); + return await es.cleanup(); + }, + adapterSetup: () => { + return new KibanaDatabaseAdapter(legacyServer.plugins.elasticsearch as DatabaseKbnESPlugin); + }, +}); diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts new file mode 100644 index 0000000000000..a54fc9537fcaa --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts @@ -0,0 +1,75 @@ +/* + * 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 { beatsIndexTemplate } from '../../../../utils/index_templates'; +import { DatabaseAdapter } from '../adapter_types'; + +interface ContractConfig { + before?(): Promise; + after?(): Promise; + adapterSetup(): DatabaseAdapter; +} + +export const contractTests = (testName: string, config: ContractConfig) => { + describe.skip(testName, () => { + let database: DatabaseAdapter; + beforeAll(async () => { + jest.setTimeout(100000); // 1 second + + if (config.before) { + await config.before(); + } + }); + afterAll(async () => config.after && (await config.after())); + beforeEach(async () => { + database = config.adapterSetup(); + }); + + it('Should inject template into ES', async () => { + try { + await database.putTemplate( + { kind: 'internal' }, + { + name: 'beats-template', + body: beatsIndexTemplate, + } + ); + } catch (e) { + expect(e).toEqual(null); + } + }); + + it('Unauthorized users cant query', async () => { + const params = { + id: `beat:foo`, + ignore: [404], + index: '.management-beats', + type: '_doc', + }; + let ranWithoutError = false; + try { + await database.get({ kind: 'unauthenticated' }, params); + ranWithoutError = true; + } catch (e) { + expect(e).not.toEqual(null); + } + expect(ranWithoutError).toEqual(false); + }); + + it('Should query ES', async () => { + const params = { + id: `beat:foo`, + ignore: [404], + index: '.management-beats', + type: '_doc', + }; + const response = await database.get({ kind: 'internal' }, params); + + expect(response).not.toEqual(undefined); + // @ts-ignore + expect(response.found).toEqual(undefined); + }); + }); +}; diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts new file mode 100644 index 0000000000000..22ee7a34066b3 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts @@ -0,0 +1,307 @@ +/* + * 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 { FrameworkRequest, FrameworkUser } from '../framework/adapter_types'; +export interface DatabaseAdapter { + putTemplate(user: FrameworkUser, params: DatabasePutTemplateParams): Promise; + get( + user: FrameworkUser, + params: DatabaseGetParams + ): Promise>; + create( + user: FrameworkUser, + params: DatabaseCreateDocumentParams + ): Promise; + index( + user: FrameworkUser, + params: DatabaseIndexDocumentParams + ): Promise; + delete( + user: FrameworkUser, + params: DatabaseDeleteDocumentParams + ): Promise; + mget(user: FrameworkUser, params: DatabaseMGetParams): Promise>; + bulk( + user: FrameworkUser, + params: DatabaseBulkIndexDocumentsParams + ): Promise; + search(user: FrameworkUser, params: DatabaseSearchParams): Promise>; +} + +export interface DatabaseKbnESCluster { + callWithInternalUser(esMethod: string, options: {}): Promise; + callWithRequest(req: FrameworkRequest, esMethod: string, options: {}): Promise; +} + +export interface DatabaseKbnESPlugin { + getCluster(clusterName: string): DatabaseKbnESCluster; +} + +export interface DatabaseSearchParams extends DatabaseGenericParams { + analyzer?: string; + analyzeWildcard?: boolean; + defaultOperator?: DefaultOperator; + df?: string; + explain?: boolean; + storedFields?: DatabaseNameList; + docvalueFields?: DatabaseNameList; + fielddataFields?: DatabaseNameList; + from?: number; + ignoreUnavailable?: boolean; + allowNoIndices?: boolean; + expandWildcards?: ExpandWildcards; + lenient?: boolean; + lowercaseExpandedTerms?: boolean; + preference?: string; + q?: string; + routing?: DatabaseNameList; + scroll?: string; + searchType?: 'query_then_fetch' | 'dfs_query_then_fetch'; + size?: number; + sort?: DatabaseNameList; + _source?: DatabaseNameList; + _sourceExclude?: DatabaseNameList; + _sourceInclude?: DatabaseNameList; + terminateAfter?: number; + stats?: DatabaseNameList; + suggestField?: string; + suggestMode?: 'missing' | 'popular' | 'always'; + suggestSize?: number; + suggestText?: string; + timeout?: string; + trackScores?: boolean; + version?: boolean; + requestCache?: boolean; + index?: DatabaseNameList; + type?: DatabaseNameList; +} + +export interface DatabaseSearchResponse { + took: number; + timed_out: boolean; + _scroll_id?: string; + _shards: DatabaseShardsResponse; + hits: { + total: number; + max_score: number; + hits: Array<{ + _index: string; + _type: string; + _id: string; + _score: number; + _source: T; + _version?: number; + _explanation?: DatabaseExplanation; + fields?: any; + highlight?: any; + inner_hits?: any; + sort?: string[]; + }>; + }; + aggregations?: any; +} + +export interface DatabaseExplanation { + value: number; + description: string; + details: DatabaseExplanation[]; +} + +export interface DatabaseShardsResponse { + total: number; + successful: number; + failed: number; + skipped: number; +} + +export interface DatabaseGetDocumentResponse { + _index: string; + _type: string; + _id: string; + _version: number; + found: boolean; + _source: Source; +} + +export interface DatabaseBulkResponse { + took: number; + errors: boolean; + items: Array< + DatabaseDeleteDocumentResponse | DatabaseIndexDocumentResponse | DatabaseUpdateDocumentResponse + >; +} + +export interface DatabaseBulkIndexDocumentsParams extends DatabaseGenericParams { + waitForActiveShards?: string; + refresh?: DatabaseRefresh; + routing?: string; + timeout?: string; + type?: string; + fields?: DatabaseNameList; + _source?: DatabaseNameList; + _sourceExclude?: DatabaseNameList; + _sourceInclude?: DatabaseNameList; + pipeline?: string; + index?: string; +} + +export interface DatabaseMGetParams extends DatabaseGenericParams { + storedFields?: DatabaseNameList; + preference?: string; + realtime?: boolean; + refresh?: boolean; + _source?: DatabaseNameList; + _sourceExclude?: DatabaseNameList; + _sourceInclude?: DatabaseNameList; + index: string; + type?: string; +} + +export interface DatabaseMGetResponse { + docs?: Array>; +} + +export interface DatabasePutTemplateParams extends DatabaseGenericParams { + name: string; + body: any; +} + +export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams { + waitForActiveShards?: string; + parent?: string; + refresh?: DatabaseRefresh; + routing?: string; + timeout?: string; + version?: number; + versionType?: DatabaseVersionType; + index: string; + type: string; + id: string; +} + +export interface DatabaseIndexDocumentResponse { + found: boolean; + _index: string; + _type: string; + _id: string; + _version: number; + result: string; +} + +export interface DatabaseUpdateDocumentResponse { + found: boolean; + _index: string; + _type: string; + _id: string; + _version: number; + result: string; +} + +export interface DatabaseDeleteDocumentResponse { + found: boolean; + _index: string; + _type: string; + _id: string; + _version: number; + result: string; +} + +export interface DatabaseIndexDocumentParams extends DatabaseGenericParams { + waitForActiveShards?: string; + opType?: 'index' | 'create'; + parent?: string; + refresh?: string; + routing?: string; + timeout?: string; + timestamp?: Date | number; + ttl?: string; + version?: number; + versionType?: DatabaseVersionType; + pipeline?: string; + id?: string; + index: string; + type: string; + body: T; +} + +export interface DatabaseGetResponse { + found: boolean; + _source: T; +} +export interface DatabaseCreateDocumentParams extends DatabaseGenericParams { + waitForActiveShards?: string; + parent?: string; + refresh?: DatabaseRefresh; + routing?: string; + timeout?: string; + timestamp?: Date | number; + ttl?: string; + version?: number; + versionType?: DatabaseVersionType; + pipeline?: string; + id?: string; + index: string; + type: string; +} + +export interface DatabaseCreateDocumentResponse { + created: boolean; + result: string; +} + +export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams { + waitForActiveShards?: string; + parent?: string; + refresh?: DatabaseRefresh; + routing?: string; + timeout?: string; + version?: number; + versionType?: DatabaseVersionType; + index: string; + type: string; + id: string; +} + +export interface DatabaseGetParams extends DatabaseGenericParams { + storedFields?: DatabaseNameList; + parent?: string; + preference?: string; + realtime?: boolean; + refresh?: boolean; + routing?: string; + _source?: DatabaseNameList; + _sourceExclude?: DatabaseNameList; + _sourceInclude?: DatabaseNameList; + version?: number; + versionType?: DatabaseVersionType; + id: string; + index: string; + type: string; +} + +export type DatabaseNameList = string | string[] | boolean; +export type DatabaseRefresh = boolean | 'true' | 'false' | 'wait_for' | ''; +export type DatabaseVersionType = 'internal' | 'external' | 'external_gte' | 'force'; +export type ExpandWildcards = 'open' | 'closed' | 'none' | 'all'; +export type DefaultOperator = 'AND' | 'OR'; +export type DatabaseConflicts = 'abort' | 'proceed'; + +export interface DatabaseGenericParams { + requestTimeout?: number; + maxRetries?: number; + method?: string; + body?: any; + ignore?: number | number[]; + filterPath?: string | string[]; +} + +export interface DatabaseDeleteDocumentResponse { + found: boolean; + _index: string; + _type: string; + _id: string; + _version: number; + result: string; +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts new file mode 100644 index 0000000000000..ba593a793e34b --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts @@ -0,0 +1,107 @@ +/* + * 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 { internalAuthData } from '../../../utils/wrap_request'; +import { FrameworkUser } from '../framework/adapter_types'; +import { + DatabaseAdapter, + DatabaseBulkIndexDocumentsParams, + DatabaseCreateDocumentParams, + DatabaseCreateDocumentResponse, + DatabaseDeleteDocumentParams, + DatabaseDeleteDocumentResponse, + DatabaseGetDocumentResponse, + DatabaseGetParams, + DatabaseIndexDocumentParams, + DatabaseKbnESCluster, + DatabaseKbnESPlugin, + DatabaseMGetParams, + DatabaseMGetResponse, + DatabasePutTemplateParams, + DatabaseSearchParams, + DatabaseSearchResponse, +} from './adapter_types'; + +export class KibanaDatabaseAdapter implements DatabaseAdapter { + private es: DatabaseKbnESCluster; + + constructor(kbnElasticSearch: DatabaseKbnESPlugin) { + this.es = kbnElasticSearch.getCluster('admin'); + } + public async putTemplate(user: FrameworkUser, params: DatabasePutTemplateParams): Promise { + const callES = this.getCallType(user); + const result = await callES('indices.putTemplate', params); + return result; + } + + public async get( + user: FrameworkUser, + params: DatabaseGetParams + ): Promise> { + const callES = this.getCallType(user); + const result = await callES('get', params); + return result; + // todo + } + + public async mget( + user: FrameworkUser, + params: DatabaseMGetParams + ): Promise> { + const callES = this.getCallType(user); + const result = await callES('mget', params); + return result; + // todo + } + + public async bulk(user: FrameworkUser, params: DatabaseBulkIndexDocumentsParams): Promise { + const callES = this.getCallType(user); + const result = await callES('bulk', params); + return result; + } + + public async create( + user: FrameworkUser, + params: DatabaseCreateDocumentParams + ): Promise { + const callES = this.getCallType(user); + const result = await callES('create', params); + return result; + } + public async index(user: FrameworkUser, params: DatabaseIndexDocumentParams): Promise { + const callES = this.getCallType(user); + const result = await callES('index', params); + return result; + } + public async delete( + user: FrameworkUser, + params: DatabaseDeleteDocumentParams + ): Promise { + const callES = this.getCallType(user); + const result = await callES('delete', params); + return result; + } + + public async search( + user: FrameworkUser, + params: DatabaseSearchParams + ): Promise> { + const callES = this.getCallType(user); + const result = await callES('search', params); + return result; + } + + private getCallType(user: FrameworkUser): any { + if (user.kind === 'authenticated') { + return this.es.callWithRequest.bind(null, { + headers: user[internalAuthData], + }); + } else if (user.kind === 'internal') { + return this.es.callWithInternalUser; + } else { + throw new Error('Invalid user type'); + } + } +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/memory_database_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/memory_database_adapter.ts new file mode 100644 index 0000000000000..9851027ce2218 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/database/memory_database_adapter.ts @@ -0,0 +1,69 @@ +/* + * 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 { FrameworkUser } from '../framework/adapter_types'; +import { + DatabaseAdapter, + DatabaseBulkIndexDocumentsParams, + DatabaseCreateDocumentParams, + DatabaseCreateDocumentResponse, + DatabaseDeleteDocumentParams, + DatabaseDeleteDocumentResponse, + DatabaseGetDocumentResponse, + DatabaseGetParams, + DatabaseIndexDocumentParams, + DatabaseMGetParams, + DatabaseMGetResponse, + DatabasePutTemplateParams, + DatabaseSearchParams, + DatabaseSearchResponse, +} from './adapter_types'; + +export class MemoryDatabaseAdapter implements DatabaseAdapter { + public async putTemplate(user: FrameworkUser, params: DatabasePutTemplateParams): Promise { + return null; + } + + public async get( + user: FrameworkUser, + params: DatabaseGetParams + ): Promise> { + throw new Error('get not implamented in memory'); + } + + public async mget( + user: FrameworkUser, + params: DatabaseMGetParams + ): Promise> { + throw new Error('mget not implamented in memory'); + } + + public async bulk(user: FrameworkUser, params: DatabaseBulkIndexDocumentsParams): Promise { + throw new Error('mget not implamented in memory'); + } + + public async create( + user: FrameworkUser, + params: DatabaseCreateDocumentParams + ): Promise { + throw new Error('create not implamented in memory'); + } + public async index(user: FrameworkUser, params: DatabaseIndexDocumentParams): Promise { + throw new Error('index not implamented in memory'); + } + public async delete( + user: FrameworkUser, + params: DatabaseDeleteDocumentParams + ): Promise { + throw new Error('delete not implamented in memory'); + } + + public async search( + user: FrameworkUser, + params: DatabaseSearchParams + ): Promise> { + throw new Error('search not implamented in memory'); + } +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.test.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.test.ts new file mode 100644 index 0000000000000..b483379d444c0 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.test.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ +// file.skip + +// @ts-ignore +import { createEsTestCluster } from '@kbn/test'; +import { config as beatsPluginConfig, configPrefix } from '../../../../..'; +// @ts-ignore +import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server'; +import { KibanaBackendFrameworkAdapter } from '../kibana_framework_adapter'; +import { contractTests } from './test_contract'; + +const kbnServer = kbnTestServer.createRootWithCorePlugins({ server: { maxPayloadBytes: 100 } }); +const legacyServer = kbnTestServer.getKbnServer(kbnServer); + +contractTests('Kibana Framework Adapter', { + before: async () => { + await kbnServer.start(); + + const config = legacyServer.server.config(); + config.extendSchema(beatsPluginConfig, {}, configPrefix); + + config.set('xpack.beats.encryptionKey', 'foo'); + }, + after: async () => { + await kbnServer.shutdown(); + }, + adapterSetup: () => { + return new KibanaBackendFrameworkAdapter(legacyServer.server); + }, +}); diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/test_contract.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/test_contract.ts new file mode 100644 index 0000000000000..5d870d1cadc4b --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/test_contract.ts @@ -0,0 +1,34 @@ +/* + * 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 { BackendFrameworkAdapter } from '../adapter_types'; + +interface ContractConfig { + before?(): Promise; + after?(): Promise; + adapterSetup(): BackendFrameworkAdapter; +} + +export const contractTests = (testName: string, config: ContractConfig) => { + describe.skip(testName, () => { + // let frameworkAdapter: BackendFrameworkAdapter; + beforeAll(async () => { + jest.setTimeout(100000); // 1 second + + if (config.before) { + await config.before(); + } + }); + afterAll(async () => config.after && (await config.after())); + beforeEach(async () => { + // FIXME: one of these always should exist, type ContractConfig as such + // const frameworkAdapter = config.adapterSetup(); + }); + + it('Should have tests here', () => { + expect(true).toEqual(true); + }); + }); +}; diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts new file mode 100644 index 0000000000000..0e690b53fe8c2 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts @@ -0,0 +1,75 @@ +/* + * 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 { internalAuthData } from '../../../utils/wrap_request'; +export interface BackendFrameworkAdapter { + internalUser: FrameworkInternalUser; + version: string; + getSetting(settingPath: string): any; + exposeStaticDir(urlPath: string, dir: string): void; + registerRoute( + route: FrameworkRouteOptions + ): void; +} + +export interface FrameworkAuthenticatedUser { + kind: 'authenticated'; + [internalAuthData]: AuthDataType; +} + +export interface FrameworkUnAuthenticatedUser { + kind: 'unauthenticated'; +} + +export interface FrameworkInternalUser { + kind: 'internal'; +} + +export type FrameworkUser = + | FrameworkAuthenticatedUser + | FrameworkUnAuthenticatedUser + | FrameworkInternalUser; + +export interface FrameworkRequest< + InternalRequest extends FrameworkWrappableRequest = FrameworkWrappableRequest +> { + user: FrameworkUser; + headers: InternalRequest['headers']; + info: InternalRequest['info']; + payload: InternalRequest['payload']; + params: InternalRequest['params']; + query: InternalRequest['query']; +} + +export interface FrameworkRouteOptions< + RouteRequest extends FrameworkWrappableRequest, + RouteResponse +> { + path: string; + method: string | string[]; + vhost?: string; + licenseRequired?: boolean; + handler: FrameworkRouteHandler; + config?: {}; +} + +export type FrameworkRouteHandler = ( + request: FrameworkRequest, + reply: any +) => void; + +export interface FrameworkWrappableRequest< + Payload = any, + Params = any, + Query = any, + Headers = any, + Info = any +> { + headers: Headers; + info: Info; + payload: Payload; + params: Params; + query: Query; +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts new file mode 100644 index 0000000000000..dd122e59bdaef --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts @@ -0,0 +1,88 @@ +/* + * 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 { wrapRequest } from '../../../utils/wrap_request'; +import { FrameworkInternalUser } from './adapter_types'; +import { + BackendFrameworkAdapter, + FrameworkRouteOptions, + FrameworkWrappableRequest, +} from './adapter_types'; + +interface TestSettings { + enrollmentTokensTtlInSeconds: number; + encryptionKey: string; +} + +export class HapiBackendFrameworkAdapter implements BackendFrameworkAdapter { + public readonly internalUser: FrameworkInternalUser = { + kind: 'internal', + }; + public version: string; + private settings: TestSettings; + private server: any; + + constructor( + settings: TestSettings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes + }, + hapiServer?: any + ) { + this.server = hapiServer; + this.settings = settings; + this.version = 'testing'; + } + + public getSetting(settingPath: string) { + switch (settingPath) { + case 'xpack.beats.enrollmentTokensTtlInSeconds': + return this.settings.enrollmentTokensTtlInSeconds; + case 'xpack.beats.encryptionKey': + return this.settings.encryptionKey; + } + } + + public exposeStaticDir(urlPath: string, dir: string): void { + if (!this.server) { + throw new Error('Must pass a hapi server into the adapter to use exposeStaticDir'); + } + this.server.route({ + handler: { + directory: { + path: dir, + }, + }, + method: 'GET', + path: urlPath, + }); + } + + public registerRoute( + route: FrameworkRouteOptions + ) { + if (!this.server) { + throw new Error('Must pass a hapi server into the adapter to use registerRoute'); + } + const wrappedHandler = (licenseRequired: boolean) => (request: any, reply: any) => { + return route.handler(wrapRequest(request), reply); + }; + + this.server.route({ + handler: wrappedHandler(route.licenseRequired || false), + method: route.method, + path: route.path, + config: { + ...route.config, + auth: false, + }, + }); + } + + public async injectRequstForTesting({ method, url, headers, payload }: any) { + return await this.server.inject({ method, url, headers, payload }); + } +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts new file mode 100644 index 0000000000000..a819eaa37b35d --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -0,0 +1,161 @@ +/* + * 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. + */ + +// @ts-ignore +import Boom from 'boom'; +// @ts-ignore +import { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status'; +import { PLUGIN } from '../../../../common/constants/plugin'; +import { wrapRequest } from '../../../utils/wrap_request'; +import { + BackendFrameworkAdapter, + FrameworkInternalUser, + FrameworkRouteOptions, + FrameworkWrappableRequest, +} from './adapter_types'; + +export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { + public readonly internalUser: FrameworkInternalUser = { + kind: 'internal', + }; + public version: string; + private server: any; + private cryptoHash: string | null; + + constructor(hapiServer: any) { + this.server = hapiServer; + if (hapiServer.plugins.kibana) { + this.version = hapiServer.plugins.kibana.status.plugin.version; + } else { + this.version = 'unknown'; + } + this.cryptoHash = null; + this.validateConfig(); + + const xpackMainPlugin = hapiServer.plugins.xpack_main; + const thisPlugin = hapiServer.plugins.beats_management; + + mirrorPluginStatus(xpackMainPlugin, thisPlugin); + xpackMainPlugin.status.once('green', () => { + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + xpackMainPlugin.info + .feature(PLUGIN.ID) + .registerLicenseCheckResultsGenerator(this.checkLicense); + }); + } + + public getSetting(settingPath: string) { + // TODO type check server properly + if (settingPath === 'xpack.beats.encryptionKey') { + // @ts-ignore + return this.server.config().get(settingPath) || this.cryptoHash; + } + // @ts-ignore + return this.server.config().get(settingPath) || this.cryptoHash; + } + + public exposeStaticDir(urlPath: string, dir: string): void { + this.server.route({ + handler: { + directory: { + path: dir, + }, + }, + method: 'GET', + path: urlPath, + }); + } + + public registerRoute( + route: FrameworkRouteOptions + ) { + const wrappedHandler = (licenseRequired: boolean) => (request: any, reply: any) => { + const xpackMainPlugin = this.server.plugins.xpack_main; + const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); + if (licenseRequired && !licenseCheckResults.licenseValid) { + reply(Boom.forbidden(licenseCheckResults.message)); + } + return route.handler(wrapRequest(request), reply); + }; + + this.server.route({ + handler: wrappedHandler(route.licenseRequired || false), + method: route.method, + path: route.path, + config: route.config, + }); + } + + private validateConfig() { + // @ts-ignore + const config = this.server.config(); + const encryptionKey = config.get('xpack.beats.encryptionKey'); + + if (!encryptionKey) { + this.server.log( + 'Using a default encryption key for xpack.beats.encryptionKey. It is recommended that you set xpack.beats.encryptionKey in kibana.yml with a unique token' + ); + this.cryptoHash = 'xpack_beats_default_encryptionKey'; + } + } + + private checkLicense(xPackInfo: any) { + // If, for some reason, we cannot get the license information + // from Elasticsearch, assume worst case and disable the Logstash pipeline UI + if (!xPackInfo || !xPackInfo.isAvailable()) { + return { + securityEnabled: false, + licenseValid: false, + message: + 'You cannot manage Beats centeral management because license information is not available at this time.', + }; + } + + const VALID_LICENSE_MODES = ['trial', 'gold', 'platinum']; + + const isLicenseValid = xPackInfo.license.isOneOf(VALID_LICENSE_MODES); + const isLicenseActive = xPackInfo.license.isActive(); + const licenseType = xPackInfo.license.getType(); + const isSecurityEnabled = xPackInfo.feature('security').isEnabled(); + + // Security is not enabled in ES + if (!isSecurityEnabled) { + const message = + 'Security must be enabled in order to use Beats centeral management features.' + + ' Please set xpack.security.enabled: true in your elasticsearch.yml.'; + return { + securityEnabled: false, + licenseValid: true, + message, + }; + } + + // License is not valid + if (!isLicenseValid) { + return { + securityEnabled: true, + licenseValid: false, + message: `Your ${licenseType} license does not support Beats centeral management features. Please upgrade your license.`, + }; + } + + // License is valid but not active, we go into a read-only mode. + if (!isLicenseActive) { + return { + securityEnabled: true, + licenseValid: false, + message: `You cannot edit, create, or delete your Beats centeral management configurations because your ${licenseType} license has expired.`, + }; + } + + // License is valid and active + return { + securityEnabled: true, + licenseValid: true, + }; + } +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts new file mode 100644 index 0000000000000..f826115f9efd9 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts @@ -0,0 +1,14 @@ +/* + * 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 { BeatTag } from '../../../../common/domain_types'; +import { FrameworkUser } from '../framework/adapter_types'; + +export interface CMTagsAdapter { + getAll(user: FrameworkUser, ESQuery?: any): Promise; + delete(user: FrameworkUser, tagIds: string[]): Promise; + getTagsWithIds(user: FrameworkUser, tagIds: string[]): Promise; + upsertTag(user: FrameworkUser, tag: BeatTag): Promise<{}>; +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts new file mode 100644 index 0000000000000..d44f3915555ad --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts @@ -0,0 +1,147 @@ +/* + * 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 { flatten, get } from 'lodash'; +import { INDEX_NAMES } from '../../../../common/constants'; +import { FrameworkUser } from '../framework/adapter_types'; + +import { BeatTag, CMBeat } from '../../../../common/domain_types'; +import { DatabaseAdapter } from '../database/adapter_types'; +import { CMTagsAdapter } from './adapter_types'; + +export class ElasticsearchTagsAdapter implements CMTagsAdapter { + private database: DatabaseAdapter; + + constructor(database: DatabaseAdapter) { + this.database = database; + } + + public async getAll(user: FrameworkUser, ESQuery?: any) { + const params = { + _source: true, + index: INDEX_NAMES.BEATS, + type: '_doc', + body: { + query: { + bool: { + must: { + term: { + type: 'tag', + }, + }, + }, + }, + }, + }; + if (ESQuery) { + params.body.query = { + ...params.body.query, + ...ESQuery, + }; + } + const response = await this.database.search(user, params); + const tags = get(response, 'hits.hits', []); + + return tags.map((tag: any) => tag._source.tag); + } + + public async delete(user: FrameworkUser, tagIds: string[]) { + const ids = tagIds.map(tag => tag); + + const params = { + ignore: [404], + index: INDEX_NAMES.BEATS, + type: '_doc', + body: { + query: { + terms: { 'beat.tags': tagIds }, + }, + }, + }; + + const beatsResponse = await this.database.search(user, params); + + const beats = get(beatsResponse, 'hits.hits', []).map( + (beat: any) => beat._source.beat + ); + + const inactiveBeats = beats.filter(beat => beat.active === false); + const activeBeats = beats.filter(beat => beat.active === true); + if (activeBeats.length !== 0) { + return false; + } + const beatIds = inactiveBeats.map((beat: CMBeat) => beat.id); + + const bulkBeatsUpdates = flatten( + beatIds.map(beatId => { + const script = ` + def beat = ctx._source.beat; + if (beat.tags != null) { + beat.tags.removeAll([params.tag]); + }`; + + return flatten( + ids.map(tagId => [ + { update: { _id: `beat:${beatId}` } }, + { script: { source: script.replace(' ', ''), params: { tagId } } }, + ]) + ); + }) + ); + + const bulkTagsDelete = ids.map(tagId => ({ delete: { _id: `tag:${tagId}` } })); + + await this.database.bulk(user, { + body: flatten([...bulkBeatsUpdates, ...bulkTagsDelete]), + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }); + + return true; + } + + public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { + const ids = tagIds.map(tag => `tag:${tag}`); + + // TODO abstract to kibana adapter as the more generic getDocs + const params = { + _source: true, + body: { + ids, + }, + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + const response = await this.database.mget(user, params); + + return get(response, 'docs', []) + .filter((b: any) => b.found) + .map((b: any) => ({ + ...b._source.tag, + id: b._id.replace('tag:', ''), + })); + } + + public async upsertTag(user: FrameworkUser, tag: BeatTag) { + const body = { + tag, + type: 'tag', + }; + + const params = { + body, + id: `tag:${tag.id}`, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }; + const response = await this.database.index(user, params); + + // TODO this is not something that works for TS... change this return type + return get(response, 'result'); + } +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts new file mode 100644 index 0000000000000..a730f55db98ee --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts @@ -0,0 +1,43 @@ +/* + * 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 { BeatTag } from '../../../../common/domain_types'; +import { FrameworkUser } from '../framework/adapter_types'; +import { CMTagsAdapter } from './adapter_types'; + +export class MemoryTagsAdapter implements CMTagsAdapter { + private tagsDB: BeatTag[] = []; + + constructor(tagsDB: BeatTag[]) { + this.tagsDB = tagsDB; + } + + public async getAll(user: FrameworkUser) { + return this.tagsDB; + } + public async delete(user: FrameworkUser, tagIds: string[]) { + this.tagsDB = this.tagsDB.filter(tag => !tagIds.includes(tag.id)); + + return true; + } + public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { + return this.tagsDB.filter(tag => tagIds.includes(tag.id)); + } + + public async upsertTag(user: FrameworkUser, tag: BeatTag) { + const existingTagIndex = this.tagsDB.findIndex(t => t.id === tag.id); + if (existingTagIndex !== -1) { + this.tagsDB[existingTagIndex] = tag; + } else { + this.tagsDB.push(tag); + } + return tag; + } + + public setDB(tagsDB: BeatTag[]) { + this.tagsDB = tagsDB; + } +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts new file mode 100644 index 0000000000000..2fe8c811c396e --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts @@ -0,0 +1,17 @@ +/* + * 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 { FrameworkUser } from '../framework/adapter_types'; + +export interface TokenEnrollmentData { + token: string | null; + expires_on: string; +} + +export interface CMTokensAdapter { + deleteEnrollmentToken(enrollmentToken: string): Promise; + getEnrollmentToken(enrollmentToken: string): Promise; + upsertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]): Promise; +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts new file mode 100644 index 0000000000000..6aa9ceff46629 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts @@ -0,0 +1,75 @@ +/* + * 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 { flatten, get } from 'lodash'; +import { INDEX_NAMES } from '../../../../common/constants'; +import { DatabaseAdapter } from '../database/adapter_types'; +import { BackendFrameworkAdapter, FrameworkUser } from '../framework/adapter_types'; +import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; + +export class ElasticsearchTokensAdapter implements CMTokensAdapter { + private database: DatabaseAdapter; + private framework: BackendFrameworkAdapter; + + constructor(database: DatabaseAdapter, framework: BackendFrameworkAdapter) { + this.database = database; + this.framework = framework; + } + + public async deleteEnrollmentToken(enrollmentToken: string) { + const params = { + id: `enrollment_token:${enrollmentToken}`, + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + + await this.database.delete(this.framework.internalUser, params); + } + + public async getEnrollmentToken(tokenString: string): Promise { + const params = { + id: `enrollment_token:${tokenString}`, + ignore: [404], + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + + const response = await this.database.get(this.framework.internalUser, params); + const tokenDetails = get(response, '_source.enrollment_token', { + expires_on: '0', + token: null, + }); + + // Elasticsearch might return fast if the token is not found. OR it might return fast + // if the token *is* found. Either way, an attacker could using a timing attack to figure + // out whether a token is valid or not. So we introduce a random delay in returning from + // this function to obscure the actual time it took for Elasticsearch to find the token. + const randomDelayInMs = 25 + Math.round(Math.random() * 200); // between 25 and 225 ms + return new Promise(resolve => + setTimeout(() => resolve(tokenDetails), randomDelayInMs) + ); + } + + public async upsertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]) { + const body = flatten( + tokens.map(token => [ + { index: { _id: `enrollment_token:${token.token}` } }, + { + enrollment_token: token, + type: 'enrollment_token', + }, + ]) + ); + + await this.database.bulk(user, { + body, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }); + return tokens; + } +} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts new file mode 100644 index 0000000000000..fabbafc040969 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts @@ -0,0 +1,46 @@ +/* + * 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 { FrameworkAuthenticatedUser } from '../framework/adapter_types'; +import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; + +export class MemoryTokensAdapter implements CMTokensAdapter { + private tokenDB: TokenEnrollmentData[]; + + constructor(tokenDB: TokenEnrollmentData[]) { + this.tokenDB = tokenDB; + } + + public async deleteEnrollmentToken(enrollmentToken: string) { + const index = this.tokenDB.findIndex(token => token.token === enrollmentToken); + + if (index > -1) { + this.tokenDB.splice(index, 1); + } + } + + public async getEnrollmentToken(tokenString: string): Promise { + return new Promise(resolve => { + return resolve(this.tokenDB.find(token => token.token === tokenString)); + }); + } + + public async upsertTokens(user: FrameworkAuthenticatedUser, tokens: TokenEnrollmentData[]) { + tokens.forEach(token => { + const existingIndex = this.tokenDB.findIndex(t => t.token === token.token); + if (existingIndex !== -1) { + this.tokenDB[existingIndex] = token; + } else { + this.tokenDB.push(token); + } + }); + return tokens; + } + + public setDB(tokenDB: TokenEnrollmentData[]) { + this.tokenDB = tokenDB; + } +} diff --git a/x-pack/plugins/beats_management/server/lib/compose/kibana.ts b/x-pack/plugins/beats_management/server/lib/compose/kibana.ts new file mode 100644 index 0000000000000..bc00278251610 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/compose/kibana.ts @@ -0,0 +1,47 @@ +/* + * 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 { ElasticsearchBeatsAdapter } from '../adapters/beats/elasticsearch_beats_adapter'; +import { KibanaDatabaseAdapter } from '../adapters/database/kibana_database_adapter'; +import { ElasticsearchTagsAdapter } from '../adapters/tags/elasticsearch_tags_adapter'; +import { ElasticsearchTokensAdapter } from '../adapters/tokens/elasticsearch_tokens_adapter'; + +import { KibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; + +import { CMBeatsDomain } from '../domains/beats'; +import { CMTagsDomain } from '../domains/tags'; +import { CMTokensDomain } from '../domains/tokens'; + +import { CMDomainLibs, CMServerLibs } from '../lib'; + +export function compose(server: any): CMServerLibs { + const framework = new KibanaBackendFrameworkAdapter(server); + const database = new KibanaDatabaseAdapter(server.plugins.elasticsearch); + + const tags = new CMTagsDomain(new ElasticsearchTagsAdapter(database)); + const tokens = new CMTokensDomain(new ElasticsearchTokensAdapter(database, framework), { + framework, + }); + const beats = new CMBeatsDomain(new ElasticsearchBeatsAdapter(database), { + tags, + tokens, + framework, + }); + + const domainLibs: CMDomainLibs = { + beats, + tags, + tokens, + }; + + const libs: CMServerLibs = { + framework, + database, + ...domainLibs, + }; + + return libs; +} diff --git a/x-pack/plugins/beats_management/server/lib/compose/testing.ts b/x-pack/plugins/beats_management/server/lib/compose/testing.ts new file mode 100644 index 0000000000000..41fc18b80aeef --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/compose/testing.ts @@ -0,0 +1,44 @@ +/* + * 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 { MemoryBeatsAdapter } from '../adapters/beats/memory_beats_adapter'; +import { MemoryTagsAdapter } from '../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter'; + +import { HapiBackendFrameworkAdapter } from '../adapters/framework/hapi_framework_adapter'; + +import { CMBeatsDomain } from '../domains/beats'; +import { CMTagsDomain } from '../domains/tags'; +import { CMTokensDomain } from '../domains/tokens'; + +import { CMDomainLibs, CMServerLibs } from '../lib'; + +export function compose(server: any): CMServerLibs { + const framework = new HapiBackendFrameworkAdapter(undefined, server); + + const tags = new CMTagsDomain(new MemoryTagsAdapter(server.tagsDB || [])); + const tokens = new CMTokensDomain(new MemoryTokensAdapter(server.tokensDB || []), { + framework, + }); + const beats = new CMBeatsDomain(new MemoryBeatsAdapter(server.beatsDB || []), { + tags, + tokens, + framework, + }); + + const domainLibs: CMDomainLibs = { + beats, + tags, + tokens, + }; + + const libs: CMServerLibs = { + framework, + ...domainLibs, + }; + + return libs; +} diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts new file mode 100644 index 0000000000000..a84f01bf4fd0f --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts @@ -0,0 +1,233 @@ +/* + * 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 { FrameworkInternalUser } from '../../../adapters/framework/adapter_types'; + +import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; +import { HapiBackendFrameworkAdapter } from '../../../adapters/framework/hapi_framework_adapter'; +import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; + +import { BeatTag, CMBeat } from '../../../../../common/domain_types'; + +import { CMBeatsDomain } from '../../beats'; +import { CMTagsDomain } from '../../tags'; +import { CMTokensDomain } from '../../tokens'; + +import Chance from 'chance'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const internalUser: FrameworkInternalUser = { kind: 'internal' }; + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +describe('Beats Domain Lib', () => { + let beatsLib: CMBeatsDomain; + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + + describe('assign_tags_to_beats', () => { + beforeEach(async () => { + beatsDB = [ + { + access_token: '9a6c99ae0fd84b068819701169cd8a4b', + active: true, + enrollment_token: '23423423423', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'qux', + type: 'filebeat', + }, + { + access_token: '188255eb560a4448b72656c5e99cae6f', + active: true, + enrollment_token: 'reertrte', + host_ip: '22.33.11.44', + host_name: 'baz.bar.com', + id: 'baz', + type: 'metricbeat', + }, + { + access_token: '93c4a4dd08564c189a7ec4e4f046b975', + active: true, + enrollment_token: '23s423423423', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'foo', + tags: ['production', 'qa'], + type: 'metricbeat', + verified_on: '2018-05-15T16:25:38.924Z', + }, + { + access_token: '3c4a4dd08564c189a7ec4e4f046b9759', + enrollment_token: 'gdfsgdf', + active: true, + host_ip: '11.22.33.44', + host_name: 'foo.com', + id: 'bar', + type: 'filebeat', + }, + ]; + tagsDB = [ + { + configuration_blocks: [], + id: 'production', + last_updated: new Date(), + }, + { + configuration_blocks: [], + id: 'development', + last_updated: new Date(), + }, + { + configuration_blocks: [], + id: 'qa', + last_updated: new Date(), + }, + ]; + const framework = new HapiBackendFrameworkAdapter(settings); + + const tokensLib = new CMTokensDomain(new MemoryTokensAdapter([]), { + framework, + }); + + const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); + + beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { + tags: tagsLib, + tokens: tokensLib, + framework, + }); + }); + + it('should add a single tag to a single beat', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ + { beatId: 'bar', tag: 'production' }, + ]); + + expect(apiResponse.assignments).toEqual([{ status: 200, result: 'updated' }]); + }); + + it('should not re-add an existing tag to a beat', async () => { + const tags = ['production']; + + let beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).toEqual([...tags, 'qa']); + + // Adding the existing tag + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ + { beatId: 'foo', tag: 'production' }, + ]); + + expect(apiResponse.assignments).toEqual([{ status: 200, result: 'updated' }]); + + beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).toEqual([...tags, 'qa']); + }); + + it('should add a single tag to a multiple beats', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'development' }, + ]); + + expect(apiResponse.assignments).toEqual([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).toEqual(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat.tags).toEqual(['development']); + }); + + it('should add multiple tags to a single beat', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ + { beatId: 'bar', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ]); + + expect(apiResponse.assignments).toEqual([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + const beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat.tags).toEqual(['development', 'production']); + }); + + it('should add multiple tags to a multiple beats', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ]); + + expect(apiResponse.assignments).toEqual([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).toEqual(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat.tags).toEqual(['production']); + }); + + it('should return errors for non-existent beats', async () => { + const nonExistentBeatId = chance.word(); + + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ + { beatId: nonExistentBeatId, tag: 'production' }, + ]); + + expect(apiResponse.assignments).toEqual([ + { status: 404, result: `Beat ${nonExistentBeatId} not found` }, + ]); + }); + + it('should return errors for non-existent tags', async () => { + const nonExistentTag = chance.word(); + + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ + { beatId: 'bar', tag: nonExistentTag }, + ]); + + expect(apiResponse.assignments).toEqual([ + { status: 404, result: `Tag ${nonExistentTag} not found` }, + ]); + + const beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat).not.toHaveProperty('tags'); + }); + + it('should return errors for non-existent beats and tags', async () => { + const nonExistentBeatId = chance.word(); + const nonExistentTag = chance.word(); + + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ + { beatId: nonExistentBeatId, tag: nonExistentTag }, + ]); + + expect(apiResponse.assignments).toEqual([ + { + result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found`, + status: 404, + }, + ]); + + const beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat).not.toHaveProperty('tags'); + }); + }); +}); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts new file mode 100644 index 0000000000000..d115c49244c65 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; +import { HapiBackendFrameworkAdapter } from '../../../adapters/framework/hapi_framework_adapter'; +import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; +import { BeatEnrollmentStatus } from '../../../lib'; + +import { BeatTag, CMBeat } from '../../../../../common/domain_types'; +import { TokenEnrollmentData } from '../../../adapters/tokens/adapter_types'; + +import { CMBeatsDomain } from '../../beats'; +import { CMTagsDomain } from '../../tags'; +import { CMTokensDomain } from '../../tokens'; + +import Chance from 'chance'; +import { sign as signToken } from 'jsonwebtoken'; +import { omit } from 'lodash'; +import moment from 'moment'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +describe('Beats Domain Lib', () => { + let beatsLib: CMBeatsDomain; + let tokensLib: CMTokensDomain; + + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + let tokensDB: TokenEnrollmentData[] = []; + let validEnrollmentToken: string; + let beatId: string; + let beat: Partial; + + describe('enroll_beat', () => { + beforeEach(async () => { + validEnrollmentToken = chance.word(); + beatId = chance.word(); + + beatsDB = []; + tagsDB = []; + tokensDB = [ + { + expires_on: moment() + .add(4, 'hours') + .toJSON(), + token: validEnrollmentToken, + }, + ]; + + const version = + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); + + beat = { + host_name: 'foo.bar.com', + type: 'filebeat', + version, + }; + + const framework = new HapiBackendFrameworkAdapter(settings); + + tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { + framework, + }); + + const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); + + beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { + tags: tagsLib, + tokens: tokensLib, + framework, + }); + }); + + it('should enroll beat, returning an access token', async () => { + const { token } = await tokensLib.getEnrollmentToken(validEnrollmentToken); + + expect(token).toEqual(validEnrollmentToken); + const { accessToken, status } = await beatsLib.enrollBeat( + validEnrollmentToken, + beatId, + '192.168.1.1', + omit(beat, 'enrollment_token') + ); + expect(status).toEqual(BeatEnrollmentStatus.Success); + + expect(beatsDB.length).toEqual(1); + expect(beatsDB[0]).toHaveProperty('host_ip'); + expect(beatsDB[0]).toHaveProperty('verified_on'); + + expect(accessToken).toEqual(beatsDB[0].access_token); + + await tokensLib.deleteEnrollmentToken(validEnrollmentToken); + + expect(tokensDB.length).toEqual(0); + }); + + it('should reject an invalid enrollment token', async () => { + const { token } = await tokensLib.getEnrollmentToken(chance.word()); + + expect(token).toEqual(null); + }); + + it('should reject an expired enrollment token', async () => { + const { token } = await tokensLib.getEnrollmentToken( + signToken({}, settings.encryptionKey, { + expiresIn: '-1min', + }) + ); + + expect(token).toEqual(null); + }); + + it('should delete the given enrollment token so it may not be reused', async () => { + expect(tokensDB[0].token).toEqual(validEnrollmentToken); + await tokensLib.deleteEnrollmentToken(validEnrollmentToken); + expect(tokensDB.length).toEqual(0); + + const { token } = await tokensLib.getEnrollmentToken(validEnrollmentToken); + + expect(token).toEqual(null); + }); + }); +}); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts new file mode 100644 index 0000000000000..a35da72889111 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { BeatTag, CMBeat } from '../../../../../common/domain_types'; +import { FrameworkInternalUser } from '../../../adapters/framework/adapter_types'; +import { compose } from '../../../compose/testing'; +import { CMServerLibs } from '../../../lib'; + +const internalUser: FrameworkInternalUser = { kind: 'internal' }; + +describe('Beats Domain Lib', () => { + let libs: CMServerLibs; + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + + describe('remove_tags_from_beats', () => { + beforeEach(async () => { + beatsDB = [ + { + access_token: '9a6c99ae0fd84b068819701169cd8a4b', + active: true, + enrollment_token: '123kuil;4', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'qux', + type: 'filebeat', + }, + { + access_token: '188255eb560a4448b72656c5e99cae6f', + active: true, + enrollment_token: '12fghjyu34', + host_ip: '22.33.11.44', + host_name: 'baz.bar.com', + id: 'baz', + type: 'metricbeat', + }, + { + access_token: '93c4a4dd08564c189a7ec4e4f046b975', + active: true, + enrollment_token: '12nfhgj34', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'foo', + tags: ['production', 'qa'], + type: 'metricbeat', + verified_on: '2018-05-15T16:25:38.924Z', + }, + { + access_token: '3c4a4dd08564c189a7ec4e4f046b9759', + active: true, + + enrollment_token: '123sfd4', + host_ip: '11.22.33.44', + host_name: 'foo.com', + id: 'bar', + type: 'filebeat', + }, + ]; + tagsDB = [ + { + configuration_blocks: [], + id: 'production', + last_updated: new Date(), + }, + { + configuration_blocks: [], + id: 'development', + last_updated: new Date(), + }, + { + configuration_blocks: [], + id: 'qa', + last_updated: new Date(), + }, + ]; + + libs = compose({ + tagsDB, + beatsDB, + }); + }); + + it('should remove a single tag from a single beat', async () => { + const apiResponse = await libs.beats.removeTagsFromBeats(internalUser, [ + { beatId: 'foo', tag: 'production' }, + ]); + + expect(apiResponse.removals).toEqual([{ status: 200, result: 'updated' }]); + // @ts-ignore + expect(beatsDB.find(b => b.id === 'foo').tags).toEqual(['qa']); + }); + + it('should remove a single tag from a multiple beats', async () => { + const apiResponse = await libs.beats.removeTagsFromBeats(internalUser, [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'development' }, + ]); + + expect(apiResponse.removals).toEqual([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + // @ts-ignore + expect(beatsDB.find(b => b.id === 'foo').tags).toEqual(['production', 'qa']); + expect(beatsDB.find(b => b.id === 'bar')).not.toHaveProperty('tags'); + }); + }); +}); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/update.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/update.test.ts new file mode 100644 index 0000000000000..0808d399326df --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/update.test.ts @@ -0,0 +1,116 @@ +/* + * 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 Chance from 'chance'; +import { BeatTag, CMBeat } from '../../../../../common/domain_types'; +import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; +import { HapiBackendFrameworkAdapter } from '../../../adapters/framework/hapi_framework_adapter'; +import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; +import { TokenEnrollmentData } from '../../../adapters/tokens/adapter_types'; +import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; +import { CMBeatsDomain } from '../../beats'; +import { CMTagsDomain } from '../../tags'; +import { CMTokensDomain } from '../../tokens'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const settings = { + encryptionKey: `it's_a_secret`, + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +describe('Beats Domain lib', () => { + describe('update_beat', () => { + let beatsLib: CMBeatsDomain; + let tokensLib: CMTokensDomain; + let token: TokenEnrollmentData; + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + let tokensDB: TokenEnrollmentData[]; + let beatId: string; + let beat: Partial; + + const getBeatsLib = async () => { + const framework = new HapiBackendFrameworkAdapter(settings); + + tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { framework }); + const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); + + beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { + framework, + tags: tagsLib, + tokens: tokensLib, + }); + + await tokensLib.createEnrollmentTokens(framework.internalUser, 1); + token = tokensDB[0]; + }; + + beforeEach(async () => { + beatId = chance.word(); + beat = { + host_name: 'foo.bar.com', + type: 'filebeat', + version: '6.4.0', + }; + beatsDB = []; + tagsDB = []; + tokensDB = []; + + getBeatsLib(); + }); + + it('should return a not-found message if beat does not exist', async () => { + const tokenString = token.token || ''; + const result = await beatsLib.update(tokenString, beatId, beat); + + expect(result).toBe('beat-not-found'); + }); + + it('should return an invalid message if token validation fails', async () => { + const beatToFind: CMBeat = { + id: beatId, + enrollment_token: '', + active: true, + access_token: token.token || '', + type: 'filebeat', + host_ip: 'localhost', + host_name: 'foo.bar.com', + }; + beatsDB = [beatToFind]; + + getBeatsLib(); + + const result = await beatsLib.update('something_invalid', beatId, beat); + + expect(result).toBe('invalid-access-token'); + }); + + it('should update the beat when a valid token is provided', async () => { + const beatToFind: CMBeat = { + id: beatId, + enrollment_token: '', + active: true, + access_token: token.token || '', + type: 'metricbeat', + host_ip: 'localhost', + host_name: 'bar.foo.com', + version: '6.3.5', + }; + beatsDB = [beatToFind]; + getBeatsLib(); + // @ts-ignore + await beatsLib.update(token, beatId, beat); + expect(beatsDB).toHaveLength(1); + const updatedBeat = beatsDB[0]; + expect(updatedBeat.id).toBe(beatId); + expect(updatedBeat.host_name).toBe('foo.bar.com'); + expect(updatedBeat.version).toBe('6.4.0'); + expect(updatedBeat.type).toBe('filebeat'); + }); + }); +}); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts new file mode 100644 index 0000000000000..91c504cd9f503 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { HapiBackendFrameworkAdapter } from '../../adapters/framework/hapi_framework_adapter'; +import { TokenEnrollmentData } from '../../adapters/tokens/adapter_types'; +import { MemoryTokensAdapter } from '../../adapters/tokens/memory_tokens_adapter'; +import { CMTokensDomain } from '../tokens'; + +import Chance from 'chance'; +import moment from 'moment'; +import { BackendFrameworkAdapter } from '../../adapters/framework/adapter_types'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +describe('Token Domain Lib', () => { + let tokensLib: CMTokensDomain; + let tokensDB: TokenEnrollmentData[] = []; + let framework: BackendFrameworkAdapter; + + beforeEach(async () => { + tokensDB = []; + framework = new HapiBackendFrameworkAdapter(settings); + + tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { + framework, + }); + }); + + it('should generate webtokens with a qty of 1', async () => { + const tokens = await tokensLib.createEnrollmentTokens(framework.internalUser, 1); + + expect(tokens.length).toBe(1); + + expect(typeof tokens[0]).toBe('string'); + }); + + it('should create the specified number of tokens', async () => { + const numTokens = chance.integer({ min: 1, max: 20 }); + const tokensFromApi = await tokensLib.createEnrollmentTokens(framework.internalUser, numTokens); + + expect(tokensFromApi.length).toEqual(numTokens); + expect(tokensFromApi).toEqual(tokensDB.map((t: TokenEnrollmentData) => t.token)); + }); + + it('should set token expiration to 10 minutes from now by default', async () => { + await tokensLib.createEnrollmentTokens(framework.internalUser, 1); + + const token = tokensDB[0]; + + // We do a fuzzy check to see if the token expires between 9 and 10 minutes + // from now because a bit of time has elapsed been the creation of the + // tokens and this check. + const tokenExpiresOn = moment(token.expires_on).valueOf(); + + // Because sometimes the test runs so fast it it equal, and we dont use expect.js version that has toBeLessThanOrEqualTo + const tenMinutesFromNow = moment() + .add('10', 'minutes') + .add('1', 'seconds') + .valueOf(); + + const almostTenMinutesFromNow = moment(tenMinutesFromNow) + .subtract('2', 'seconds') + .valueOf(); + expect(tokenExpiresOn).toBeLessThan(tenMinutesFromNow); + expect(tokenExpiresOn).toBeGreaterThan(almostTenMinutesFromNow); + }); +}); diff --git a/x-pack/plugins/beats_management/server/lib/domains/beats.ts b/x-pack/plugins/beats_management/server/lib/domains/beats.ts new file mode 100644 index 0000000000000..a1b890119fc3e --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/domains/beats.ts @@ -0,0 +1,246 @@ +/* + * 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 { uniq } from 'lodash'; +import moment from 'moment'; +import { findNonExistentItems } from '../../utils/find_non_existent_items'; + +import { CMBeat } from '../../../common/domain_types'; +import { BeatsTagAssignment, CMBeatsAdapter } from '../adapters/beats/adapter_types'; +import { FrameworkUser } from '../adapters/framework/adapter_types'; + +import { CMAssignmentReturn } from '../adapters/beats/adapter_types'; +import { BeatsRemovalReturn } from '../adapters/beats/adapter_types'; +import { BeatEnrollmentStatus, CMDomainLibs, CMServerLibs, UserOrToken } from '../lib'; + +export class CMBeatsDomain { + private tags: CMDomainLibs['tags']; + private tokens: CMDomainLibs['tokens']; + private framework: CMServerLibs['framework']; + + constructor( + private readonly adapter: CMBeatsAdapter, + libs: { + tags: CMDomainLibs['tags']; + tokens: CMDomainLibs['tokens']; + framework: CMServerLibs['framework']; + } + ) { + this.adapter = adapter; + this.tags = libs.tags; + this.tokens = libs.tokens; + this.framework = libs.framework; + } + + public async getById(user: FrameworkUser, beatId: string): Promise { + const beat = await this.adapter.get(user, beatId); + return beat && beat.active ? beat : null; + } + + public async getAll(user: FrameworkUser, ESQuery?: any) { + return (await this.adapter.getAll(user, ESQuery)).filter( + (beat: CMBeat) => beat.active === true + ); + } + + public async getAllWithTag(user: FrameworkUser, tagId: string) { + return (await this.adapter.getAllWithTags(user, [tagId])).filter( + (beat: CMBeat) => beat.active === true + ); + } + + public async getByEnrollmentToken(user: FrameworkUser, enrollmentToken: string) { + const beat = await this.adapter.getBeatWithToken(user, enrollmentToken); + return beat && beat.active ? beat : null; + } + + public async update(userOrToken: UserOrToken, beatId: string, beatData: Partial) { + const beat = await this.adapter.get(this.framework.internalUser, beatId); + + // TODO make return type enum + if (beat === null) { + return 'beat-not-found'; + } + + if (typeof userOrToken === 'string') { + const { verified: isAccessTokenValid } = this.tokens.verifyToken( + beat ? beat.access_token : '', + userOrToken + ); + if (!isAccessTokenValid) { + return 'invalid-access-token'; + } + } + + const user = typeof userOrToken === 'string' ? this.framework.internalUser : userOrToken; + + await this.adapter.update(user, { + ...beat, + ...beatData, + }); + } + + // TODO more strongly type this + public async enrollBeat( + enrollmentToken: string, + beatId: string, + remoteAddress: string, + beat: Partial + ): Promise<{ status: string; accessToken?: string }> { + const { token, expires_on } = await this.tokens.getEnrollmentToken(enrollmentToken); + + if (expires_on && moment(expires_on).isBefore(moment())) { + return { status: BeatEnrollmentStatus.ExpiredEnrollmentToken }; + } + if (!token) { + return { status: BeatEnrollmentStatus.InvalidEnrollmentToken }; + } + + const existingBeat = await this.getById(this.framework.internalUser, beatId); + if (existingBeat) { + return { status: BeatEnrollmentStatus.Success }; + } + + const accessToken = this.tokens.generateAccessToken(); + const verifiedOn = moment().toJSON(); + + await this.adapter.insert(this.framework.internalUser, { + ...beat, + active: true, + enrollment_token: enrollmentToken, + verified_on: verifiedOn, + access_token: accessToken, + host_ip: remoteAddress, + id: beatId, + } as CMBeat); + + await this.tokens.deleteEnrollmentToken(enrollmentToken); + + return { status: BeatEnrollmentStatus.Success, accessToken }; + } + + public async removeTagsFromBeats( + user: FrameworkUser, + removals: BeatsTagAssignment[] + ): Promise { + const beatIds = uniq(removals.map(removal => removal.beatId)); + const tagIds = uniq(removals.map(removal => removal.tag)); + + const response = { + removals: removals.map(() => ({ status: null })), + }; + + const beats = await this.adapter.getWithIds(user, beatIds); + const tags = await this.tags.getTagsWithIds(user, tagIds); + + // Handle assignments containing non-existing beat IDs or tags + const nonExistentBeatIds = findNonExistentItems(beats, beatIds); + const nonExistentTags = await findNonExistentItems(tags, tagIds); + + addNonExistentItemToResponse( + response, + removals, + nonExistentBeatIds, + nonExistentTags, + 'removals' + ); + + // TODO abstract this + const validRemovals = removals + .map((removal, idxInRequest) => ({ + beatId: removal.beatId, + idxInRequest, // so we can add the result of this removal to the correct place in the response + tag: removal.tag, + })) + .filter((removal, idx) => response.removals[idx].status === null); + + if (validRemovals.length > 0) { + const removalResults = await this.adapter.removeTagsFromBeats(user, validRemovals); + return addToResultsToResponse('removals', response, removalResults); + } + return response; + } + + public async assignTagsToBeats( + user: FrameworkUser, + assignments: BeatsTagAssignment[] + ): Promise { + const beatIds = uniq(assignments.map(assignment => assignment.beatId)); + const tagIds = uniq(assignments.map(assignment => assignment.tag)); + + const response = { + assignments: assignments.map(() => ({ status: null })), + }; + const beats = await this.adapter.getWithIds(user, beatIds); + const tags = await this.tags.getTagsWithIds(user, tagIds); + // Handle assignments containing non-existing beat IDs or tags + const nonExistentBeatIds = findNonExistentItems(beats, beatIds); + const nonExistentTags = findNonExistentItems(tags, tagIds); + + // TODO break out back into route / function response + // TODO causes function to error if a beat or tag does not exist + addNonExistentItemToResponse( + response, + assignments, + nonExistentBeatIds, + nonExistentTags, + 'assignments' + ); + + // TODO abstract this + const validAssignments = assignments + .map((assignment, idxInRequest) => ({ + beatId: assignment.beatId, + idxInRequest, // so we can add the result of this assignment to the correct place in the response + tag: assignment.tag, + })) + .filter((assignment, idx) => response.assignments[idx].status === null); + + if (validAssignments.length > 0) { + const assignmentResults = await this.adapter.assignTagsToBeats(user, validAssignments); + + // TODO This should prob not mutate + return addToResultsToResponse('assignments', response, assignmentResults); + } + return response; + } +} + +// TODO abstract to the route, also the key arg is a temp fix +function addNonExistentItemToResponse( + response: any, + assignments: any, + nonExistentBeatIds: any, + nonExistentTags: any, + key: string +) { + assignments.forEach(({ beatId, tag }: BeatsTagAssignment, idx: any) => { + const isBeatNonExistent = nonExistentBeatIds.includes(beatId); + + const isTagNonExistent = nonExistentTags.includes(tag); + + if (isBeatNonExistent && isTagNonExistent) { + response[key][idx].status = 404; + response[key][idx].result = `Beat ${beatId} and tag ${tag} not found`; + } else if (isBeatNonExistent) { + response[key][idx].status = 404; + response[key][idx].result = `Beat ${beatId} not found`; + } else if (isTagNonExistent) { + response[key][idx].status = 404; + response[key][idx].result = `Tag ${tag} not found`; + } + }); +} + +// TODO dont mutate response +function addToResultsToResponse(key: string, response: any, assignmentResults: any) { + assignmentResults.forEach((assignmentResult: any) => { + const { idxInRequest, status, result } = assignmentResult; + response[key][idxInRequest].status = status; + response[key][idxInRequest].result = result; + }); + return response; +} diff --git a/x-pack/plugins/beats_management/server/lib/domains/tags.ts b/x-pack/plugins/beats_management/server/lib/domains/tags.ts new file mode 100644 index 0000000000000..79ff2007d1160 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/domains/tags.ts @@ -0,0 +1,96 @@ +/* + * 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 { intersection, uniq, values } from 'lodash'; +import { UNIQUENESS_ENFORCING_TYPES } from '../../../common/constants'; +import { ConfigurationBlock } from '../../../common/domain_types'; +import { FrameworkUser } from '../adapters/framework/adapter_types'; + +import { entries } from '../../utils/polyfills'; +import { CMTagsAdapter } from '../adapters/tags/adapter_types'; + +export class CMTagsDomain { + constructor(private readonly adapter: CMTagsAdapter) {} + + public async getAll(user: FrameworkUser, ESQuery?: any) { + return await this.adapter.getAll(user, ESQuery); + } + + public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { + return await this.adapter.getTagsWithIds(user, tagIds); + } + + public async delete(user: FrameworkUser, tagIds: string[]) { + return await this.adapter.delete(user, tagIds); + } + + public async saveTag( + user: FrameworkUser, + tagId: string, + config: { color: string; configuration_blocks: ConfigurationBlock[] } + ) { + const { isValid, message } = await this.validateConfigurationBlocks( + config.configuration_blocks + ); + if (!isValid) { + return { isValid, result: message }; + } + + const tag = { + ...config, + id: tagId, + last_updated: new Date(), + }; + return { + isValid: true, + result: await this.adapter.upsertTag(user, tag), + }; + } + + private validateConfigurationBlocks(configurationBlocks: any) { + const types = uniq(configurationBlocks.map((block: any) => block.type)); + + // If none of the types in the given configuration blocks are uniqueness-enforcing, + // we don't need to perform any further validation checks. + const uniquenessEnforcingTypes = intersection(types, UNIQUENESS_ENFORCING_TYPES); + if (uniquenessEnforcingTypes.length === 0) { + return { isValid: true }; + } + + // Count the number of uniqueness-enforcing types in the given configuration blocks + const typeCountMap = configurationBlocks.reduce((map: any, block: any) => { + const { type } = block; + if (!uniquenessEnforcingTypes.includes(type)) { + return map; + } + + const count = map[type] || 0; + return { + ...map, + [type]: count + 1, + }; + }, {}); + + // If there is no more than one of any uniqueness-enforcing types in the given + // configuration blocks, we don't need to perform any further validation checks. + if (values(typeCountMap).filter(count => count > 1).length === 0) { + return { isValid: true }; + } + + const message = entries(typeCountMap) + .filter(([, count]) => count > 1) + .map( + ([type, count]) => + `Expected only one configuration block of type '${type}' but found ${count}` + ) + .join(' '); + + return { + isValid: false, + message, + }; + } +} diff --git a/x-pack/plugins/beats_management/server/lib/domains/tokens.ts b/x-pack/plugins/beats_management/server/lib/domains/tokens.ts new file mode 100644 index 0000000000000..529a526bea75d --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/domains/tokens.ts @@ -0,0 +1,137 @@ +/* + * 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 { timingSafeEqual } from 'crypto'; +import { sign as signToken, verify as verifyToken } from 'jsonwebtoken'; +import moment from 'moment'; +import uuid from 'uuid'; +import { BackendFrameworkAdapter } from '../adapters/framework/adapter_types'; +import { FrameworkUser } from '../adapters/framework/adapter_types'; +import { CMTokensAdapter } from '../adapters/tokens/adapter_types'; + +const RANDOM_TOKEN_1 = 'b48c4bda384a40cb91c6eb9b8849e77f'; +const RANDOM_TOKEN_2 = '80a3819e3cd64f4399f1d4886be7a08b'; + +export class CMTokensDomain { + private adapter: CMTokensAdapter; + private framework: BackendFrameworkAdapter; + + constructor(adapter: CMTokensAdapter, libs: { framework: BackendFrameworkAdapter }) { + this.adapter = adapter; + this.framework = libs.framework; + } + + public async getEnrollmentToken(enrollmentToken: string) { + const fullToken = await this.adapter.getEnrollmentToken(enrollmentToken); + + if (!fullToken) { + return { + token: null, + expired: true, + expires_on: null, + }; + } + + const { verified, expired } = this.verifyToken(enrollmentToken, fullToken.token || '', false); + + if (!verified) { + return { + expired, + token: null, + expires_on: null, + }; + } + + return { ...fullToken, expired }; + } + + public async deleteEnrollmentToken(enrollmentToken: string) { + return await this.adapter.deleteEnrollmentToken(enrollmentToken); + } + + public verifyToken(recivedToken: string, token2: string, decode = true) { + let tokenDecoded = true; + let expired = false; + + if (decode) { + const enrollmentTokenSecret = this.framework.getSetting('xpack.beats.encryptionKey'); + + try { + verifyToken(recivedToken, enrollmentTokenSecret); + tokenDecoded = true; + } catch (err) { + if (err.name === 'TokenExpiredError') { + expired = true; + } + tokenDecoded = false; + } + } + + if ( + typeof recivedToken !== 'string' || + typeof token2 !== 'string' || + recivedToken.length !== token2.length + ) { + // This prevents a more subtle timing attack where we know already the tokens aren't going to + // match but still we don't return fast. Instead we compare two pre-generated random tokens using + // the same comparison algorithm that we would use to compare two equal-length tokens. + return { + expired, + verified: + timingSafeEqual( + Buffer.from(RANDOM_TOKEN_1, 'utf8'), + Buffer.from(RANDOM_TOKEN_2, 'utf8') + ) && tokenDecoded, + }; + } + + return { + expired, + verified: + timingSafeEqual(Buffer.from(recivedToken, 'utf8'), Buffer.from(token2, 'utf8')) && + tokenDecoded, + }; + } + + public generateAccessToken() { + const enrollmentTokenSecret = this.framework.getSetting('xpack.beats.encryptionKey'); + + const tokenData = { + created: moment().toJSON(), + randomHash: this.createRandomHash(), + }; + + return signToken(tokenData, enrollmentTokenSecret); + } + + public async createEnrollmentTokens( + user: FrameworkUser, + numTokens: number = 1 + ): Promise { + const tokens = []; + const enrollmentTokensTtlInSeconds = this.framework.getSetting( + 'xpack.beats.enrollmentTokensTtlInSeconds' + ); + + const enrollmentTokenExpiration = moment() + .add(enrollmentTokensTtlInSeconds, 'seconds') + .toJSON(); + + while (tokens.length < numTokens) { + tokens.push({ + expires_on: enrollmentTokenExpiration, + token: this.createRandomHash(), + }); + } + + await this.adapter.upsertTokens(user, tokens); + + return tokens.map(token => token.token); + } + + private createRandomHash() { + return uuid.v4().replace(/-/g, ''); + } +} diff --git a/x-pack/plugins/beats_management/server/lib/lib.ts b/x-pack/plugins/beats_management/server/lib/lib.ts new file mode 100644 index 0000000000000..b8d51374741fe --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/lib.ts @@ -0,0 +1,31 @@ +/* + * 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 { DatabaseAdapter } from './adapters/database/adapter_types'; +import { BackendFrameworkAdapter, FrameworkUser } from './adapters/framework/adapter_types'; + +import { CMBeatsDomain } from './domains/beats'; +import { CMTagsDomain } from './domains/tags'; +import { CMTokensDomain } from './domains/tokens'; + +export type UserOrToken = FrameworkUser | string; + +export interface CMDomainLibs { + beats: CMBeatsDomain; + tags: CMTagsDomain; + tokens: CMTokensDomain; +} + +export interface CMServerLibs extends CMDomainLibs { + framework: BackendFrameworkAdapter; + database?: DatabaseAdapter; +} + +export enum BeatEnrollmentStatus { + Success = 'Success', + ExpiredEnrollmentToken = 'Expired enrollment token', + InvalidEnrollmentToken = 'Invalid enrollment token', +} diff --git a/x-pack/plugins/beats_management/server/management_server.ts b/x-pack/plugins/beats_management/server/management_server.ts new file mode 100644 index 0000000000000..bc14bacdb9e7b --- /dev/null +++ b/x-pack/plugins/beats_management/server/management_server.ts @@ -0,0 +1,42 @@ +/* + * 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 { CMServerLibs } from './lib/lib'; +import { createGetBeatConfigurationRoute } from './rest_api/beats/configuration'; +import { createBeatEnrollmentRoute } from './rest_api/beats/enroll'; +import { createGetBeatRoute } from './rest_api/beats/get'; +import { createListAgentsRoute } from './rest_api/beats/list'; +import { createTagAssignmentsRoute } from './rest_api/beats/tag_assignment'; +import { createTagRemovalsRoute } from './rest_api/beats/tag_removal'; +import { createBeatUpdateRoute } from './rest_api/beats/update'; +import { createDeleteTagsWithIdsRoute } from './rest_api/tags/delete'; +import { createGetTagsWithIdsRoute } from './rest_api/tags/get'; +import { createListTagsRoute } from './rest_api/tags/list'; +import { createSetTagRoute } from './rest_api/tags/set'; +import { createTokensRoute } from './rest_api/tokens/create'; +import { beatsIndexTemplate } from './utils/index_templates'; + +export const initManagementServer = (libs: CMServerLibs) => { + if (libs.database) { + libs.database.putTemplate(libs.framework.internalUser, { + name: 'beats-template', + body: beatsIndexTemplate, + }); + } + + libs.framework.registerRoute(createGetBeatRoute(libs)); + libs.framework.registerRoute(createGetTagsWithIdsRoute(libs)); + libs.framework.registerRoute(createListTagsRoute(libs)); + libs.framework.registerRoute(createDeleteTagsWithIdsRoute(libs)); + libs.framework.registerRoute(createGetBeatConfigurationRoute(libs)); + libs.framework.registerRoute(createTagAssignmentsRoute(libs)); + libs.framework.registerRoute(createListAgentsRoute(libs)); + libs.framework.registerRoute(createTagRemovalsRoute(libs)); + libs.framework.registerRoute(createBeatEnrollmentRoute(libs)); + libs.framework.registerRoute(createSetTagRoute(libs)); + libs.framework.registerRoute(createTokensRoute(libs)); + libs.framework.registerRoute(createBeatUpdateRoute(libs)); +}; diff --git a/x-pack/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts b/x-pack/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts new file mode 100644 index 0000000000000..331403e8145c3 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts @@ -0,0 +1,253 @@ +/* + * 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 { CMServerLibs } from '../../lib/lib'; +import { HapiBackendFrameworkAdapter } from './../../lib/adapters/framework/hapi_framework_adapter'; +import { testHarnes } from './test_harnes'; + +describe('assign_tags_to_beats', () => { + let serverLibs: CMServerLibs; + + beforeAll(async () => { + jest.setTimeout(100000); // 1 second + + serverLibs = await testHarnes.getServerLibs(); + }); + beforeEach(async () => await testHarnes.loadData()); + + it('should add a single tag to a single beat', async () => { + const { + result, + statusCode, + } = await (serverLibs.framework as HapiBackendFrameworkAdapter).injectRequstForTesting({ + method: 'POST', + url: '/api/beats/agents_tags/assignments', + headers: { + 'kbn-xsrf': 'xxx', + authorization: 'loggedin', + }, + payload: { + assignments: [{ beatId: 'bar', tag: 'production' }], + }, + }); + + expect(statusCode).toEqual(200); + expect(result.assignments).toEqual([{ status: 200, result: 'updated' }]); + }); + + it('should not re-add an existing tag to a beat', async () => { + const { + result, + statusCode, + } = await (serverLibs.framework as HapiBackendFrameworkAdapter).injectRequstForTesting({ + method: 'POST', + url: '/api/beats/agents_tags/assignments', + headers: { + 'kbn-xsrf': 'xxx', + authorization: 'loggedin', + }, + payload: { + assignments: [{ beatId: 'foo', tag: 'production' }], + }, + }); + + expect(statusCode).toEqual(200); + + expect(result.assignments).toEqual([{ status: 200, result: 'updated' }]); + + let beat; + + beat = await serverLibs.beats.getById( + { + kind: 'internal', + }, + 'foo' + ); + expect(beat!.tags).toEqual(['production', 'qa']); // as + }); + + it('should add a single tag to a multiple beats', async () => { + const { + result, + statusCode, + } = await (serverLibs.framework as HapiBackendFrameworkAdapter).injectRequstForTesting({ + method: 'POST', + url: '/api/beats/agents_tags/assignments', + headers: { + 'kbn-xsrf': 'xxx', + authorization: 'loggedin', + }, + payload: { + assignments: [{ beatId: 'foo', tag: 'development' }, { beatId: 'bar', tag: 'development' }], + }, + }); + + expect(statusCode).toEqual(200); + + expect(result.assignments).toEqual([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let beat; + + beat = await serverLibs.beats.getById( + { + kind: 'internal', + }, + 'foo' + ); + expect(beat!.tags).toEqual(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + // Beat bar + beat = await serverLibs.beats.getById( + { + kind: 'internal', + }, + 'bar' + ); + + expect(beat!.tags).toEqual(['development']); + }); + + it('should add multiple tags to a single beat', async () => { + const { + result, + statusCode, + } = await (serverLibs.framework as HapiBackendFrameworkAdapter).injectRequstForTesting({ + method: 'POST', + url: '/api/beats/agents_tags/assignments', + headers: { + 'kbn-xsrf': 'xxx', + authorization: 'loggedin', + }, + payload: { + assignments: [{ beatId: 'bar', tag: 'development' }, { beatId: 'bar', tag: 'production' }], + }, + }); + + expect(statusCode).toEqual(200); + + expect(result.assignments).toEqual([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + const beat = await serverLibs.beats.getById( + { + kind: 'internal', + }, + 'bar' + ); + + expect(beat!.tags).toEqual(['development', 'production']); + }); + + // it('should add multiple tags to a multiple beats', async () => { + // const { body: apiResponse } = await supertest + // .post('/api/beats/agents_tags/assignments') + // .set('kbn-xsrf', 'xxx') + // .send({ + // assignments: [{ beatId: 'foo', tag: 'development' }, { beatId: 'bar', tag: 'production' }], + // }) + // .expect(200); + + // expect(apiResponse.assignments).to.eql([ + // { status: 200, result: 'updated' }, + // { status: 200, result: 'updated' }, + // ]); + + // let esResponse; + // let beat; + + // // Beat foo + // esResponse = await es.get({ + // index: ES_INDEX_NAME, + // type: ES_TYPE_NAME, + // id: `beat:foo`, + // }); + + // beat = esResponse._source.beat; + // expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + // // Beat bar + // esResponse = await es.get({ + // index: ES_INDEX_NAME, + // type: ES_TYPE_NAME, + // id: `beat:bar`, + // }); + + // beat = esResponse._source.beat; + // expect(beat.tags).to.eql(['production']); + // }); + + // it('should return errors for non-existent beats', async () => { + // const nonExistentBeatId = chance.word(); + + // const { body: apiResponse } = await supertest + // .post('/api/beats/agents_tags/assignments') + // .set('kbn-xsrf', 'xxx') + // .send({ + // assignments: [{ beatId: nonExistentBeatId, tag: 'production' }], + // }) + // .expect(200); + + // expect(apiResponse.assignments).to.eql([ + // { status: 404, result: `Beat ${nonExistentBeatId} not found` }, + // ]); + // }); + + // it('should return errors for non-existent tags', async () => { + // const nonExistentTag = chance.word(); + + // const { body: apiResponse } = await supertest + // .post('/api/beats/agents_tags/assignments') + // .set('kbn-xsrf', 'xxx') + // .send({ + // assignments: [{ beatId: 'bar', tag: nonExistentTag }], + // }) + // .expect(200); + + // expect(apiResponse.assignments).to.eql([ + // { status: 404, result: `Tag ${nonExistentTag} not found` }, + // ]); + + // const esResponse = await es.get({ + // index: ES_INDEX_NAME, + // type: ES_TYPE_NAME, + // id: `beat:bar`, + // }); + + // const beat = esResponse._source.beat; + // expect(beat).to.not.have.property('tags'); + // }); + + // it('should return errors for non-existent beats and tags', async () => { + // const nonExistentBeatId = chance.word(); + // const nonExistentTag = chance.word(); + + // const { body: apiResponse } = await supertest + // .post('/api/beats/agents_tags/assignments') + // .set('kbn-xsrf', 'xxx') + // .send({ + // assignments: [{ beatID: nonExistentBeatId, tag: nonExistentTag }], + // }) + // .expect(200); + + // expect(apiResponse.assignments).to.eql([ + // { status: 404, result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found` }, + // ]); + + // const esResponse = await es.get({ + // index: ES_INDEX_NAME, + // type: ES_TYPE_NAME, + // id: `beat:bar`, + // }); + + // const beat = esResponse._source.beat; + // expect(beat).to.not.have.property('tags'); + // }); +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/__tests__/data.json b/x-pack/plugins/beats_management/server/rest_api/__tests__/data.json new file mode 100644 index 0000000000000..f263eff1a5bd4 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/__tests__/data.json @@ -0,0 +1,158 @@ +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:qux", + "source": { + "type": "beat", + "beat": { + "type": "filebeat", + "active": true, + "host_ip": "1.2.3.4", + "host_name": "foo.bar.com", + "id": "qux", + "name": "qux_filebeat", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:baz", + "source": { + "type": "beat", + "beat": { + "type": "metricbeat", + "active": true, + "host_ip": "22.33.11.44", + "host_name": "baz.bar.com", + "id": "baz", + "name": "baz_metricbeat", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:foo", + "source": { + "type": "beat", + "beat": { + "type": "metricbeat", + "active": true, + "host_ip": "1.2.3.4", + "host_name": "foo.bar.com", + "id": "foo", + "name": "foo_metricbeat", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI", + "verified_on": "2018-05-15T16:25:38.924Z", + "tags": [ + "production", + "qa" + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:bar", + "source": { + "type": "beat", + "beat": { + "type": "filebeat", + "active": true, + "host_ip": "11.22.33.44", + "host_name": "foo.com", + "id": "bar", + "name": "bar_filebeat", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "tag:production", + "source": { + "type": "tag", + "tag": { + "configuration_blocks": [ + { + "type": "output", + "description": "some description", + "configs": [{ + "hosts": ["localhost:9200"], + "username": "some-username" + }] + }, + { + "type": "metricbeat.modules", + "configs": [{ + "module": "memcached", + "hosts": ["localhost:11211"] + }] + } + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "tag:development", + "source": { + "type": "tag", + "tag": { + "configuration_blocks": [] + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "tag:qa", + "source": { + "type": "tag", + "tag": { + "configuration_blocks": [ + { + "type": "metricbeat.modules", + "configs": [{ + "module": "memcached", + "node.namespace": "node", + "hosts": ["localhost:4949"] + }] + } + ] + } + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts b/x-pack/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts new file mode 100644 index 0000000000000..18b59e0d9aeee --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts @@ -0,0 +1,103 @@ +/* + * 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 { badRequest } from 'boom'; +import { readFile } from 'fs'; +// @ts-ignore +import Hapi from 'hapi'; +import { resolve } from 'path'; +import { promisify } from 'util'; +import { BeatTag, CMBeat } from '../../../common/domain_types'; +import { TokenEnrollmentData } from '../../lib/adapters/tokens/adapter_types'; +import { compose } from '../../lib/compose/testing'; +import { CMServerLibs } from '../../lib/lib'; +import { initManagementServer } from './../../management_server'; + +const readFileAsync = promisify(readFile); +let serverLibs: CMServerLibs; + +export const testHarnes = { + description: 'API Development Tests', + loadData: async () => { + if (!serverLibs) { + throw new Error('Server libs not composed yet...'); + } + const contents = await readFileAsync(resolve(__dirname, './data.json'), 'utf8'); + const database = contents.split(/\n\n/); + + // @ts-ignore the private access + serverLibs.beats.adapter.setDB( + database.reduce((inserts: CMBeat[], source) => { + const type = 'beat'; + const data = JSON.parse(source); + + if (data.value.source.type === type) { + inserts.push({ + id: data.value.id.substring(data.value.id.indexOf(':') + 1), + ...data.value.source[type], + }); + } + return inserts; + }, []) + ); + + // @ts-ignore the private access + serverLibs.tags.adapter.setDB( + database.reduce((inserts: BeatTag[], source) => { + const type = 'tag'; + const data = JSON.parse(source); + + if (data.value.source.type === type) { + inserts.push({ + id: data.value.id.substring(data.value.id.indexOf(':') + 1), + ...data.value.source[type], + }); + } + return inserts; + }, []) + ); + + // @ts-ignore the private access + serverLibs.tokens.adapter.setDB( + database.reduce((inserts: TokenEnrollmentData[], source) => { + const type = 'token'; + const data = JSON.parse(source); + + if (data.value.source.type === type) { + inserts.push({ + id: data.value.id.substring(data.value.id.indexOf(':') + 1), + ...data.value.source[type], + }); + } + return inserts; + }, []) + ); + }, + getServerLibs: async () => { + if (!serverLibs) { + const server = new Hapi.Server(); + server.connection({ port: 111111 }); + const versionHeader = 'kbn-version'; + const xsrfHeader = 'kbn-xsrf'; + + server.ext('onPostAuth', (req: any, reply: any) => { + const isSafeMethod = req.method === 'get' || req.method === 'head'; + const hasVersionHeader = versionHeader in req.headers; + const hasXsrfHeader = xsrfHeader in req.headers; + + if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) { + return reply(badRequest(`Request must contain a ${xsrfHeader} header.`)); + } + + return reply.continue(); + }); + + serverLibs = compose(server); + initManagementServer(serverLibs); + } + return serverLibs; + }, +}; diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts b/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts new file mode 100644 index 0000000000000..7d283633b9f09 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts @@ -0,0 +1,69 @@ +/* + * 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 { omit } from 'lodash'; +import { BeatTag, ConfigurationBlock } from '../../../common/domain_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; +import { ReturnedConfigurationBlock } from './../../../common/domain_types'; + +export const createGetBeatConfigurationRoute = (libs: CMServerLibs) => ({ + method: 'GET', + path: '/api/beats/agent/{beatId}/configuration', + config: { + validate: { + headers: Joi.object({ + 'kbn-beats-access-token': Joi.string().required(), + }).options({ allowUnknown: true }), + }, + auth: false, + }, + handler: async (request: any, reply: any) => { + const beatId = request.params.beatId; + const accessToken = request.headers['kbn-beats-access-token']; + + let beat; + let tags; + try { + beat = await libs.beats.getById(libs.framework.internalUser, beatId); + if (beat === null) { + return reply({ message: `Beat "${beatId}" not found` }).code(404); + } + + const isAccessTokenValid = beat.access_token === accessToken; + if (!isAccessTokenValid) { + return reply({ message: 'Invalid access token' }).code(401); + } + + tags = await libs.tags.getTagsWithIds(libs.framework.internalUser, beat.tags || []); + } catch (err) { + return reply(wrapEsError(err)); + } + + const configurationBlocks = tags.reduce( + (blocks: ReturnedConfigurationBlock[], tag: BeatTag) => { + blocks = blocks.concat( + tag.configuration_blocks.reduce( + (acc: ReturnedConfigurationBlock[], block: ConfigurationBlock) => { + acc.push({ + ...omit(block, ['configs']), + config: block.configs[0], + }); + return acc; + }, + [] + ) + ); + return blocks; + }, + [] + ); + + reply({ + configuration_blocks: configurationBlocks, + }); + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts b/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts new file mode 100644 index 0000000000000..03b46603c4d6c --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts @@ -0,0 +1,64 @@ +/* + * 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 { omit } from 'lodash'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; +import { CMServerLibs } from '../../lib/lib'; +import { BeatEnrollmentStatus } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: write to Kibana audit log file +export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ + method: 'POST', + path: '/api/beats/agent/{beatId}', + licenseRequired: true, + config: { + auth: false, + validate: { + headers: Joi.object({ + 'kbn-beats-enrollment-token': Joi.string().required(), + }).options({ + allowUnknown: true, + }), + payload: Joi.object({ + host_name: Joi.string().required(), + name: Joi.string().required(), + type: Joi.string().required(), + version: Joi.string().required(), + }).required(), + }, + }, + handler: async (request: FrameworkRequest, reply: any) => { + const { beatId } = request.params; + const enrollmentToken = request.headers['kbn-beats-enrollment-token']; + + try { + const { status, accessToken } = await libs.beats.enrollBeat( + enrollmentToken, + beatId, + request.info.remoteAddress, + omit(request.payload, 'enrollment_token') + ); + + switch (status) { + case BeatEnrollmentStatus.ExpiredEnrollmentToken: + return reply({ + message: BeatEnrollmentStatus.ExpiredEnrollmentToken, + }).code(400); + case BeatEnrollmentStatus.InvalidEnrollmentToken: + return reply({ + message: BeatEnrollmentStatus.InvalidEnrollmentToken, + }).code(400); + case BeatEnrollmentStatus.Success: + default: + return reply({ access_token: accessToken }).code(201); + } + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/get.ts b/x-pack/plugins/beats_management/server/rest_api/beats/get.ts new file mode 100644 index 0000000000000..fc0437bce8329 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/beats/get.ts @@ -0,0 +1,43 @@ +/* + * 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 { CMBeat } from '../../../common/domain_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +export const createGetBeatRoute = (libs: CMServerLibs) => ({ + method: 'GET', + path: '/api/beats/agent/{beatId}/{token?}', + + handler: async (request: any, reply: any) => { + const beatId = request.params.beatId; + + let beat: CMBeat | null; + if (beatId === 'unknown') { + try { + beat = await libs.beats.getByEnrollmentToken(request.user, request.params.token); + if (beat === null) { + return reply().code(200); + } + } catch (err) { + return reply(wrapEsError(err)); + } + } else { + try { + beat = await libs.beats.getById(request.user, beatId); + if (beat === null) { + return reply({ message: 'Beat not found' }).code(404); + } + } catch (err) { + return reply(wrapEsError(err)); + } + } + + delete beat.access_token; + + reply(beat); + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/list.ts b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts new file mode 100644 index 0000000000000..cdaaf05679a33 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts @@ -0,0 +1,62 @@ +/* + * 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 * as Joi from 'joi'; +import { CMBeat } from '../../../common/domain_types'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +export const createListAgentsRoute = (libs: CMServerLibs) => ({ + method: 'GET', + path: '/api/beats/agents/{listByAndValue*}', + validate: { + headers: Joi.object({ + 'kbn-beats-enrollment-token': Joi.string().required(), + }).options({ + allowUnknown: true, + }), + query: Joi.object({ + ESQuery: Joi.string(), + }), + }, + licenseRequired: true, + handler: async (request: FrameworkRequest, reply: any) => { + const listByAndValueParts = request.params.listByAndValue + ? request.params.listByAndValue.split('/') + : []; + let listBy: 'tag' | null = null; + let listByValue: string | null = null; + + if (listByAndValueParts.length === 2) { + listBy = listByAndValueParts[0]; + listByValue = listByAndValueParts[1]; + } + + try { + let beats: CMBeat[]; + + switch (listBy) { + case 'tag': + beats = await libs.beats.getAllWithTag(request.user, listByValue || ''); + break; + + default: + beats = await libs.beats.getAll( + request.user, + request.query && request.query.ESQuery ? JSON.parse(request.query.ESQuery) : undefined + ); + + break; + } + + reply({ beats }); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts new file mode 100644 index 0000000000000..b0a73f1706571 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts @@ -0,0 +1,42 @@ +/* + * 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 { BeatsTagAssignment } from '../../../public/lib/adapters/beats/adapter_types'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; + +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: write to Kibana audit log file +export const createTagAssignmentsRoute = (libs: CMServerLibs) => ({ + method: 'POST', + path: '/api/beats/agents_tags/assignments', + licenseRequired: true, + config: { + validate: { + payload: Joi.object({ + assignments: Joi.array().items( + Joi.object({ + beatId: Joi.string().required(), + tag: Joi.string().required(), + }) + ), + }).required(), + }, + }, + handler: async (request: FrameworkRequest, reply: any) => { + const { assignments }: { assignments: BeatsTagAssignment[] } = request.payload; + + try { + const response = await libs.beats.assignTagsToBeats(request.user, assignments); + reply(response); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts new file mode 100644 index 0000000000000..e8d395b27eaa6 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts @@ -0,0 +1,40 @@ +/* + * 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 { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: write to Kibana audit log file +export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ + method: 'POST', + path: '/api/beats/agents_tags/removals', + licenseRequired: true, + config: { + validate: { + payload: Joi.object({ + removals: Joi.array().items( + Joi.object({ + beatId: Joi.string().required(), + tag: Joi.string().required(), + }) + ), + }).required(), + }, + }, + handler: async (request: FrameworkRequest, reply: any) => { + const { removals } = request.payload; + + try { + const response = await libs.beats.removeTagsFromBeats(request.user, removals); + reply(response); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts new file mode 100644 index 0000000000000..6e64c0bea502a --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts @@ -0,0 +1,72 @@ +/* + * 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 { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: write to Kibana audit log file (include who did the verification as well) +export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ + method: 'PUT', + path: '/api/beats/agent/{beatId}', + licenseRequired: true, + config: { + auth: { + mode: 'optional', + }, + validate: { + headers: Joi.object({ + 'kbn-beats-access-token': Joi.string(), + }).options({ + allowUnknown: true, + }), + params: Joi.object({ + beatId: Joi.string(), + }), + payload: Joi.object({ + active: Joi.bool(), + ephemeral_id: Joi.string(), + host_name: Joi.string(), + local_configuration_yml: Joi.string(), + metadata: Joi.object(), + name: Joi.string(), + type: Joi.string(), + version: Joi.string(), + }), + }, + }, + handler: async (request: FrameworkRequest, reply: any) => { + const { beatId } = request.params; + const accessToken = request.headers['kbn-beats-access-token']; + const remoteAddress = request.info.remoteAddress; + const userOrToken = accessToken || request.user; + + if (request.user.kind === 'unauthenticated' && request.payload.active !== undefined) { + return reply({ message: 'access-token is not a valid auth type to change beat status' }).code( + 401 + ); + } + + try { + const status = await libs.beats.update(userOrToken, beatId, { + ...request.payload, + host_ip: remoteAddress, + }); + + switch (status) { + case 'beat-not-found': + return reply({ message: 'Beat not found', success: false }).code(404); + case 'invalid-access-token': + return reply({ message: 'Invalid access token', success: false }).code(401); + } + + reply({ success: true }).code(204); + } catch (err) { + return reply(wrapEsError(err)); + } + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts b/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts new file mode 100644 index 0000000000000..34dde929b4586 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts @@ -0,0 +1,27 @@ +/* + * 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 { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +export const createDeleteTagsWithIdsRoute = (libs: CMServerLibs) => ({ + method: 'DELETE', + path: '/api/beats/tags/{tagIds}', + licenseRequired: true, + handler: async (request: any, reply: any) => { + const tagIdString: string = request.params.tagIds; + const tagIds = tagIdString.split(',').filter((id: string) => id.length > 0); + + let success: boolean; + try { + success = await libs.tags.delete(request.user, tagIds); + } catch (err) { + return reply(wrapEsError(err)); + } + + reply({ success }); + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/get.ts b/x-pack/plugins/beats_management/server/rest_api/tags/get.ts new file mode 100644 index 0000000000000..c65640ee3eb2f --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/tags/get.ts @@ -0,0 +1,28 @@ +/* + * 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 { BeatTag } from '../../../common/domain_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +export const createGetTagsWithIdsRoute = (libs: CMServerLibs) => ({ + method: 'GET', + path: '/api/beats/tags/{tagIds}', + licenseRequired: true, + handler: async (request: any, reply: any) => { + const tagIdString: string = request.params.tagIds; + const tagIds = tagIdString.split(',').filter((id: string) => id.length > 0); + + let tags: BeatTag[]; + try { + tags = await libs.tags.getTagsWithIds(request.user, tagIds); + } catch (err) { + return reply(wrapEsError(err)); + } + + reply(tags); + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/list.ts b/x-pack/plugins/beats_management/server/rest_api/tags/list.ts new file mode 100644 index 0000000000000..6ef3f70c7f6f2 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/tags/list.ts @@ -0,0 +1,39 @@ +/* + * 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 * as Joi from 'joi'; +import { BeatTag } from '../../../common/domain_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +export const createListTagsRoute = (libs: CMServerLibs) => ({ + method: 'GET', + path: '/api/beats/tags', + validate: { + headers: Joi.object({ + 'kbn-beats-enrollment-token': Joi.string().required(), + }).options({ + allowUnknown: true, + }), + query: Joi.object({ + ESQuery: Joi.string(), + }), + }, + licenseRequired: true, + handler: async (request: any, reply: any) => { + let tags: BeatTag[]; + try { + tags = await libs.tags.getAll( + request.user + // request.query ? JSON.parse(request.query.ESQuery) : undefined + ); + } catch (err) { + return reply(wrapEsError(err)); + } + + reply(tags); + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/set.ts b/x-pack/plugins/beats_management/server/rest_api/tags/set.ts new file mode 100644 index 0000000000000..79503e995ac60 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/tags/set.ts @@ -0,0 +1,56 @@ +/* + * 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, values } from 'lodash'; +import { ConfigurationBlockTypes } from '../../../common/constants'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: write to Kibana audit log file +export const createSetTagRoute = (libs: CMServerLibs) => ({ + method: 'PUT', + path: '/api/beats/tag/{tag}', + licenseRequired: true, + config: { + validate: { + params: Joi.object({ + tag: Joi.string(), + }), + payload: Joi.object({ + color: Joi.string(), + configuration_blocks: Joi.array().items( + Joi.object({ + configs: Joi.array() + .items(Joi.object()) + .required(), + description: Joi.string().allow(''), + type: Joi.string() + .only(values(ConfigurationBlockTypes)) + .required(), + }) + ), + }).allow(null), + }, + }, + handler: async (request: FrameworkRequest, reply: any) => { + const defaultConfig = { configuration_blocks: [], color: '#DD0A73' }; + const config = get(request, 'payload', defaultConfig) || defaultConfig; + + try { + const { isValid, result } = await libs.tags.saveTag(request.user, request.params.tag, config); + if (!isValid) { + return reply({ result, success: false }).code(400); + } + + reply({ success: true }).code(result === 'created' ? 201 : 200); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts b/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts new file mode 100644 index 0000000000000..08205bcba43dd --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts @@ -0,0 +1,40 @@ +/* + * 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 } from 'lodash'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: write to Kibana audit log file +const DEFAULT_NUM_TOKENS = 1; +export const createTokensRoute = (libs: CMServerLibs) => ({ + method: 'POST', + path: '/api/beats/enrollment_tokens', + licenseRequired: true, + config: { + validate: { + payload: Joi.object({ + num_tokens: Joi.number() + .optional() + .default(DEFAULT_NUM_TOKENS) + .min(1), + }).allow(null), + }, + }, + handler: async (request: FrameworkRequest, reply: any) => { + const numTokens = get(request, 'payload.num_tokens', DEFAULT_NUM_TOKENS); + + try { + const tokens = await libs.tokens.createEnrollmentTokens(request.user, numTokens); + reply({ tokens }); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, +}); diff --git a/x-pack/plugins/beats_management/server/utils/README.md b/x-pack/plugins/beats_management/server/utils/README.md new file mode 100644 index 0000000000000..8a6a27aa29867 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/README.md @@ -0,0 +1 @@ +Utils should be data processing functions and other tools.... all in all utils is basicly everything that is not an adaptor, or presenter and yet too much to put in a lib. \ No newline at end of file diff --git a/x-pack/plugins/beats_management/server/utils/error_wrappers/index.ts b/x-pack/plugins/beats_management/server/utils/error_wrappers/index.ts new file mode 100644 index 0000000000000..3756b0c74fb10 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/error_wrappers/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { wrapEsError } from './wrap_es_error'; diff --git a/x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.test.ts b/x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.test.ts new file mode 100644 index 0000000000000..e7fb1c2adda21 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { wrapEsError } from './wrap_es_error'; + +describe('wrap_es_error', () => { + describe('#wrapEsError', () => { + let originalError: any; + beforeEach(() => { + originalError = new Error('I am an error'); + originalError.statusCode = 404; + }); + + it('should return a Boom object', () => { + const wrappedError = wrapEsError(originalError); + + expect(wrappedError.isBoom).toEqual(true); + }); + + it('should return the correct Boom object', () => { + const wrappedError = wrapEsError(originalError); + + expect(wrappedError.output.statusCode).toEqual(originalError.statusCode); + expect(wrappedError.output.payload.message).toEqual(originalError.message); + }); + + it('should return invalid permissions message for 403 errors', () => { + const securityError = new Error('I am an error'); + // @ts-ignore + securityError.statusCode = 403; + const wrappedError = wrapEsError(securityError); + + expect(wrappedError.isBoom).toEqual(true); + expect(wrappedError.message).toEqual( + 'Insufficient user permissions for managing Beats configuration' + ); + }); + }); +}); diff --git a/x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.ts b/x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.ts new file mode 100644 index 0000000000000..f0b683ce4aa66 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.ts @@ -0,0 +1,31 @@ +/* + * 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. + */ + +// @ts-ignore +import Boom from 'boom'; + +/** + * Wraps ES errors into a Boom error response and returns it + * This also handles the permissions issue gracefully + * + * @param err Object ES error + * @return Object Boom error response + */ +export function wrapEsError(err: any) { + const statusCode = err.statusCode; + if (statusCode === 403) { + return Boom.forbidden('Insufficient user permissions for managing Beats configuration'); + } + + // This is due to a typings error in the Boom typedef. + // @ts-ignore + if (Boom.wrap) { + // @ts-ignore + return Boom.wrap(err, err.statusCode); + } + + return Boom.boomify(err, { statusCode: err.statusCode }); +} diff --git a/x-pack/plugins/beats_management/server/utils/find_non_existent_items.ts b/x-pack/plugins/beats_management/server/utils/find_non_existent_items.ts new file mode 100644 index 0000000000000..0e9b4f0b6fa5e --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/find_non_existent_items.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +interface RandomItem { + id: string; + [key: string]: any; +} + +export function findNonExistentItems(items: RandomItem[], requestedItems: any) { + return requestedItems.reduce((nonExistentItems: string[], requestedItem: string, idx: number) => { + if (items.findIndex((item: RandomItem) => item && item.id === requestedItem) === -1) { + nonExistentItems.push(requestedItems[idx]); + } + return nonExistentItems; + }, []); +} diff --git a/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json b/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json new file mode 100644 index 0000000000000..8b5806b3c24cd --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json @@ -0,0 +1,103 @@ +{ + "index_patterns": [".management-beats"], + "version": 65000, + "settings": { + "index": { + "number_of_shards": 1, + "auto_expand_replicas": "0-1", + "codec": "best_compression" + } + }, + "mappings": { + "_doc": { + "dynamic": "strict", + "properties": { + "type": { + "type": "keyword" + }, + "enrollment_token": { + "properties": { + "token": { + "type": "keyword" + }, + "expires_on": { + "type": "date" + } + } + }, + "tag": { + "properties": { + "id": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "configuration_blocks": { + "type": "nested", + "properties": { + "type": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "configs": { + "type": "nested", + "dynamic": true + } + } + } + } + }, + "beat": { + "properties": { + "id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "enrollment_token": { + "type": "keyword" + }, + "access_token": { + "type": "keyword" + }, + "verified_on": { + "type": "date" + }, + "type": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "host_ip": { + "type": "ip" + }, + "host_name": { + "type": "keyword" + }, + "ephemeral_id": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "metadata": { + "dynamic": "true", + "type": "object" + }, + "name": { + "type": "keyword" + } + } + } + } + } + } +} diff --git a/x-pack/plugins/beats_management/server/utils/index_templates/index.ts b/x-pack/plugins/beats_management/server/utils/index_templates/index.ts new file mode 100644 index 0000000000000..eeaef7a68d49f --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/index_templates/index.ts @@ -0,0 +1,8 @@ +/* + * 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 beatsIndexTemplate from './beats_template.json'; +export { beatsIndexTemplate }; diff --git a/x-pack/plugins/beats_management/server/utils/polyfills.ts b/x-pack/plugins/beats_management/server/utils/polyfills.ts new file mode 100644 index 0000000000000..5291e2c72be7d --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/polyfills.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export const entries = (obj: any) => { + const ownProps = Object.keys(obj); + let i = ownProps.length; + const resArray = new Array(i); // preallocate the Array + + while (i--) { + resArray[i] = [ownProps[i], obj[ownProps[i]]]; + } + + return resArray; +}; diff --git a/x-pack/plugins/beats_management/server/utils/wrap_request.ts b/x-pack/plugins/beats_management/server/utils/wrap_request.ts new file mode 100644 index 0000000000000..66bdbf578e141 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/wrap_request.ts @@ -0,0 +1,36 @@ +/* + * 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 { + FrameworkRequest, + FrameworkWrappableRequest, +} from '../lib/adapters/framework/adapter_types'; + +export const internalAuthData = Symbol('internalAuthData'); + +export function wrapRequest( + req: InternalRequest +): FrameworkRequest { + const { params, payload, query, headers, info } = req; + + const isAuthenticated = headers.authorization != null; + + return { + user: isAuthenticated + ? { + kind: 'authenticated', + [internalAuthData]: headers, + } + : { + kind: 'unauthenticated', + }, + headers, + info, + params, + payload, + query, + }; +} diff --git a/x-pack/plugins/beats_management/tsconfig.json b/x-pack/plugins/beats_management/tsconfig.json new file mode 100644 index 0000000000000..67fefc7286ca4 --- /dev/null +++ b/x-pack/plugins/beats_management/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["**/node_modules/**"], + "paths": { + "react": ["../../../node_modules/@types/react"] + } +} diff --git a/x-pack/plugins/beats_management/types/eui.d.ts b/x-pack/plugins/beats_management/types/eui.d.ts new file mode 100644 index 0000000000000..1b5ad7aa0a374 --- /dev/null +++ b/x-pack/plugins/beats_management/types/eui.d.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +/** + * /!\ These type definitions are temporary until the upstream @elastic/eui + * package includes them. + */ + +import * as eui from '@elastic/eui'; +import { Moment } from 'moment'; +import { ChangeEventHandler, MouseEventHandler, ReactType, Ref, SFC } from 'react'; + +declare module '@elastic/eui' { + +} diff --git a/x-pack/plugins/beats_management/types/formsy.d.ts b/x-pack/plugins/beats_management/types/formsy.d.ts new file mode 100644 index 0000000000000..f153e80d13e53 --- /dev/null +++ b/x-pack/plugins/beats_management/types/formsy.d.ts @@ -0,0 +1,47 @@ +/* + * 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. + */ + +declare module 'formsy-react' { + import React, { SFC } from 'react'; + let Formsy: SFC; + export interface FormsyInputProps { + getErrorMessage(): any; + getValue(): any; + hasValue(): boolean; + isFormDisabled(): boolean; + isFormSubmitted(): boolean; + isPristine(): boolean; + isRequired(): boolean; + isValid(): boolean; + isValidValue(): boolean; + resetValue(): void; + setValidations(validations: any, required: boolean): void; + setValue(value: any): void; + showError(): boolean; + showRequired(): boolean; + } + + export default Formsy; + export type FormData = { [fieldName in keyof FormShape]: string }; + export type FieldValue = string | null | undefined; + + type ValidationMethod = ( + values: FormData, + value: string | null | undefined + ) => void; + + export function addValidationRule( + validationName: string, + validationMethod: ValidationMethod + ): void; + export function withFormsy(component: any): any; + + // function withFormsy( + // component: + // | React.Component + // | SFC + // ): React.Component; +} diff --git a/x-pack/plugins/beats_management/types/kibana.d.ts b/x-pack/plugins/beats_management/types/kibana.d.ts new file mode 100644 index 0000000000000..e95dc0df93bea --- /dev/null +++ b/x-pack/plugins/beats_management/types/kibana.d.ts @@ -0,0 +1,57 @@ +/* + * 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. + */ + +declare module 'ui/index_patterns' { + export type IndexPattern = any; + + export interface StaticIndexPatternField { + name: string; + type: string; + aggregatable: boolean; + searchable: boolean; + } + + export interface StaticIndexPattern { + fields: StaticIndexPatternField[]; + title: string; + } +} + +declare module 'ui/autocomplete_providers' { + import { StaticIndexPattern } from 'ui/index_patterns'; + + export type AutocompleteProvider = ( + args: { + config: { + get(configKey: string): any; + }; + indexPatterns: StaticIndexPattern[]; + boolFilter: any; + } + ) => GetSuggestions; + + export type GetSuggestions = ( + args: { + query: string; + selectionStart: number; + selectionEnd: number; + } + ) => Promise; + + export type AutocompleteSuggestionType = 'field' | 'value' | 'operator' | 'conjunction'; + + export interface AutocompleteSuggestion { + description: string; + end: number; + start: number; + text: string; + type: AutocompleteSuggestionType; + } + + export function addAutocompleteProvider(language: string, provider: AutocompleteProvider): void; + + export function getAutocompleteProvider(language: string): AutocompleteProvider | undefined; +} diff --git a/x-pack/plugins/beats_management/wallaby.js b/x-pack/plugins/beats_management/wallaby.js new file mode 100644 index 0000000000000..bb57d22afafb3 --- /dev/null +++ b/x-pack/plugins/beats_management/wallaby.js @@ -0,0 +1,60 @@ +/* + * 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. + */ +const path = require('path'); +process.env.NODE_PATH = path.join(__dirname, '..', '..', 'node_modules'); + +module.exports = function (wallaby) { + return { + debug: true, + files: [ + './tsconfig.json', + //'plugins/beats/public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', + 'server/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', + 'common/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', + 'public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', + '!**/*.test.ts', + ], + + tests: ['**/*.test.ts', '**/*.test.tsx'], + env: { + type: 'node', + runner: 'node', + }, + testFramework: 'jest', + compilers: { + '**/*.ts?(x)': wallaby.compilers.typeScript({ + typescript: require('typescript'), // eslint-disable-line + }), + '**/*.js': wallaby.compilers.babel({ + babelrc: false, + presets: [require.resolve('@kbn/babel-preset/node_preset')], + }), + }, + + setup: wallaby => { + const path = require('path'); + + const kibanaDirectory = path.resolve(wallaby.localProjectDir, '..', '..', '..'); + + wallaby.testFramework.configure({ + rootDir: wallaby.localProjectDir, + moduleNameMapper: { + '^ui/(.*)': `${kibanaDirectory}/src/ui/public/$1`, + // eslint-disable-next-line + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`, + '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, + }, + testURL: 'http://localhost', + setupFiles: [`${kibanaDirectory}/x-pack/dev-tools/jest/setup/enzyme.js`], + snapshotSerializers: [`${kibanaDirectory}/node_modules/enzyme-to-json/serializer`], + transform: { + '^.+\\.js$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, + //"^.+\\.tsx?$": `${kibanaDirectory}/src/dev/jest/ts_transform.js`, + }, + }); + }, + }; +}; diff --git a/x-pack/plugins/canvas/public/angular/services/index.js b/x-pack/plugins/canvas/public/angular/services/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/canvas/tasks/mocks/noop.js b/x-pack/plugins/canvas/tasks/mocks/noop.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/grokdebugger/common/constants/index.js b/x-pack/plugins/grokdebugger/common/constants/index.js index 12e440d7ed858..c37101b73078a 100644 --- a/x-pack/plugins/grokdebugger/common/constants/index.js +++ b/x-pack/plugins/grokdebugger/common/constants/index.js @@ -6,4 +6,4 @@ export { ROUTES } from './routes'; export { PLUGIN } from './plugin'; -export { EDITOR } from './editor'; +export { EDITOR } from './editor'; \ No newline at end of file diff --git a/x-pack/plugins/grokdebugger/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/plugins/grokdebugger/server/lib/call_with_request_factory/call_with_request_factory.js index b9a77a1a0362b..7359a831994f9 100644 --- a/x-pack/plugins/grokdebugger/server/lib/call_with_request_factory/call_with_request_factory.js +++ b/x-pack/plugins/grokdebugger/server/lib/call_with_request_factory/call_with_request_factory.js @@ -6,7 +6,7 @@ import { once } from 'lodash'; -const callWithRequest = once((server) => { +const callWithRequest = once(server => { const cluster = server.plugins.elasticsearch.getCluster('data'); return cluster.callWithRequest; }); diff --git a/x-pack/plugins/index_management/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/plugins/index_management/server/lib/call_with_request_factory/call_with_request_factory.js index b9a77a1a0362b..7359a831994f9 100644 --- a/x-pack/plugins/index_management/server/lib/call_with_request_factory/call_with_request_factory.js +++ b/x-pack/plugins/index_management/server/lib/call_with_request_factory/call_with_request_factory.js @@ -6,7 +6,7 @@ import { once } from 'lodash'; -const callWithRequest = once((server) => { +const callWithRequest = once(server => { const cluster = server.plugins.elasticsearch.getCluster('data'); return cluster.callWithRequest; }); diff --git a/x-pack/plugins/infra/scripts/generate_types_from_graphql.js b/x-pack/plugins/infra/scripts/generate_types_from_graphql.js index 5270985905002..f36979c159376 100644 --- a/x-pack/plugins/infra/scripts/generate_types_from_graphql.js +++ b/x-pack/plugins/infra/scripts/generate_types_from_graphql.js @@ -5,13 +5,13 @@ */ const { join, resolve } = require('path'); -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved const { generate } = require('graphql-code-generator'); const GRAPHQL_GLOBS = [ join('public', 'containers', '**', '*.gql_query.ts{,x}'), join('public', 'store', '**', '*.gql_query.ts{,x}'), - join('common', 'graphql', '**', '*.gql_query.ts{,x}') + join('common', 'graphql', '**', '*.gql_query.ts{,x}'), ]; const CONFIG_PATH = resolve(__dirname, 'gql_gen.json'); const OUTPUT_INTROSPECTION_PATH = resolve('common', 'graphql', 'introspection.json'); diff --git a/x-pack/plugins/logstash/README.md b/x-pack/plugins/logstash/README.md old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/constants/es_scroll_settings.js b/x-pack/plugins/logstash/common/constants/es_scroll_settings.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/constants/index.js b/x-pack/plugins/logstash/common/constants/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/constants/index_names.js b/x-pack/plugins/logstash/common/constants/index_names.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/constants/monitoring.js b/x-pack/plugins/logstash/common/constants/monitoring.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/constants/pagination.js b/x-pack/plugins/logstash/common/constants/pagination.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/constants/pipeline.js b/x-pack/plugins/logstash/common/constants/pipeline.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/constants/plugin.js b/x-pack/plugins/logstash/common/constants/plugin.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/constants/routes.js b/x-pack/plugins/logstash/common/constants/routes.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/constants/tooltips.js b/x-pack/plugins/logstash/common/constants/tooltips.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/constants/type_names.js b/x-pack/plugins/logstash/common/constants/type_names.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/lib/__tests__/get_moment.js b/x-pack/plugins/logstash/common/lib/__tests__/get_moment.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/lib/get_moment.js b/x-pack/plugins/logstash/common/lib/get_moment.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/common/lib/index.js b/x-pack/plugins/logstash/common/lib/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/index.js b/x-pack/plugins/logstash/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/lib/get_search_value/get_search_value.js b/x-pack/plugins/logstash/public/lib/get_search_value/get_search_value.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/lib/get_search_value/index.js b/x-pack/plugins/logstash/public/lib/get_search_value/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/lib/register_home_feature/index.js b/x-pack/plugins/logstash/public/lib/register_home_feature/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/lib/register_home_feature/register_home_feature.js b/x-pack/plugins/logstash/public/lib/register_home_feature/register_home_feature.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/lib/update_management_sections/index.js b/x-pack/plugins/logstash/public/lib/update_management_sections/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/lib/update_management_sections/update_logstash_sections.js b/x-pack/plugins/logstash/public/lib/update_management_sections/update_logstash_sections.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/models/cluster/cluster.js b/x-pack/plugins/logstash/public/models/cluster/cluster.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/models/cluster/index.js b/x-pack/plugins/logstash/public/models/cluster/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/models/pipeline/index.js b/x-pack/plugins/logstash/public/models/pipeline/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/models/pipeline/pipeline.js b/x-pack/plugins/logstash/public/models/pipeline/pipeline.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/models/pipeline_list_item/index.js b/x-pack/plugins/logstash/public/models/pipeline_list_item/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js b/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/index.js b/x-pack/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/pipeline_edit.js b/x-pack/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/pipeline_edit.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/index.js b/x-pack/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/upgrade_failure.js b/x-pack/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/upgrade_failure.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_edit/index.js b/x-pack/plugins/logstash/public/sections/pipeline_edit/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.html b/x-pack/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.html old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.js b/x-pack/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/index.js b/x-pack/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/pipeline_list.js b/x-pack/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/pipeline_list.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_list/index.js b/x-pack/plugins/logstash/public/sections/pipeline_list/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.html b/x-pack/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.html old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.js b/x-pack/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/sections/pipeline_list/register_management_section.js b/x-pack/plugins/logstash/public/sections/pipeline_list/register_management_section.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/cluster/cluster_service.factory.js b/x-pack/plugins/logstash/public/services/cluster/cluster_service.factory.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/cluster/cluster_service.js b/x-pack/plugins/logstash/public/services/cluster/cluster_service.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/cluster/index.js b/x-pack/plugins/logstash/public/services/cluster/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/license/index.js b/x-pack/plugins/logstash/public/services/license/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/license/license_service.factory.js b/x-pack/plugins/logstash/public/services/license/license_service.factory.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/license/logstash_license_service.js b/x-pack/plugins/logstash/public/services/license/logstash_license_service.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/monitoring/index.js b/x-pack/plugins/logstash/public/services/monitoring/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.factory.js b/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.factory.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js b/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/pipeline/index.js b/x-pack/plugins/logstash/public/services/pipeline/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/pipeline/pipeline_service.factory.js b/x-pack/plugins/logstash/public/services/pipeline/pipeline_service.factory.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/pipeline/pipeline_service.js b/x-pack/plugins/logstash/public/services/pipeline/pipeline_service.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/pipelines/index.js b/x-pack/plugins/logstash/public/services/pipelines/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/pipelines/pipelines_service.factory.js b/x-pack/plugins/logstash/public/services/pipelines/pipelines_service.factory.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/pipelines/pipelines_service.js b/x-pack/plugins/logstash/public/services/pipelines/pipelines_service.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/security/index.js b/x-pack/plugins/logstash/public/services/security/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/security/logstash_security_service.js b/x-pack/plugins/logstash/public/services/security/logstash_security_service.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/security/security_service.factory.js b/x-pack/plugins/logstash/public/services/security/security_service.factory.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/upgrade/index.js b/x-pack/plugins/logstash/public/services/upgrade/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/upgrade/upgrade_service.factory.js b/x-pack/plugins/logstash/public/services/upgrade/upgrade_service.factory.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/public/services/upgrade/upgrade_service.js b/x-pack/plugins/logstash/public/services/upgrade/upgrade_service.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/plugins/logstash/server/lib/call_with_request_factory/call_with_request_factory.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/call_with_request_factory/index.js b/x-pack/plugins/logstash/server/lib/call_with_request_factory/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/check_license/__tests__/check_license.js b/x-pack/plugins/logstash/server/lib/check_license/__tests__/check_license.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/check_license/check_license.js b/x-pack/plugins/logstash/server/lib/check_license/check_license.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/check_license/index.js b/x-pack/plugins/logstash/server/lib/check_license/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_custom_error.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_es_error.js b/x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_es_error.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_unknown_error.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/error_wrappers/index.js b/x-pack/plugins/logstash/server/lib/error_wrappers/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/error_wrappers/wrap_custom_error.js b/x-pack/plugins/logstash/server/lib/error_wrappers/wrap_custom_error.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/error_wrappers/wrap_es_error.js b/x-pack/plugins/logstash/server/lib/error_wrappers/wrap_es_error.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/error_wrappers/wrap_unknown_error.js b/x-pack/plugins/logstash/server/lib/error_wrappers/wrap_unknown_error.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js b/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js b/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/index.js b/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/plugins/logstash/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/license_pre_routing_factory/index.js b/x-pack/plugins/logstash/server/lib/license_pre_routing_factory/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/license_pre_routing_factory/license_pre_routing_factory.js b/x-pack/plugins/logstash/server/lib/license_pre_routing_factory/license_pre_routing_factory.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/register_license_checker/index.js b/x-pack/plugins/logstash/server/lib/register_license_checker/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/lib/register_license_checker/register_license_checker.js b/x-pack/plugins/logstash/server/lib/register_license_checker/register_license_checker.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/models/cluster/__tests__/cluster.js b/x-pack/plugins/logstash/server/models/cluster/__tests__/cluster.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/models/cluster/cluster.js b/x-pack/plugins/logstash/server/models/cluster/cluster.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/models/cluster/index.js b/x-pack/plugins/logstash/server/models/cluster/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/models/pipeline/__tests__/pipeline.js b/x-pack/plugins/logstash/server/models/pipeline/__tests__/pipeline.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/models/pipeline/index.js b/x-pack/plugins/logstash/server/models/pipeline/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/models/pipeline/pipeline.js b/x-pack/plugins/logstash/server/models/pipeline/pipeline.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/models/pipeline_list_item/__tests__/pipeline_list_item.js b/x-pack/plugins/logstash/server/models/pipeline_list_item/__tests__/pipeline_list_item.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/models/pipeline_list_item/index.js b/x-pack/plugins/logstash/server/models/pipeline_list_item/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.js b/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/cluster/index.js b/x-pack/plugins/logstash/server/routes/api/cluster/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/cluster/register_cluster_routes.js b/x-pack/plugins/logstash/server/routes/api/cluster/register_cluster_routes.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/cluster/register_load_route.js b/x-pack/plugins/logstash/server/routes/api/cluster/register_load_route.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/pipeline/index.js b/x-pack/plugins/logstash/server/routes/api/pipeline/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/pipeline/register_delete_route.js b/x-pack/plugins/logstash/server/routes/api/pipeline/register_delete_route.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/pipeline/register_load_route.js b/x-pack/plugins/logstash/server/routes/api/pipeline/register_load_route.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/pipeline/register_pipeline_routes.js b/x-pack/plugins/logstash/server/routes/api/pipeline/register_pipeline_routes.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/pipeline/register_save_route.js b/x-pack/plugins/logstash/server/routes/api/pipeline/register_save_route.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/pipelines/index.js b/x-pack/plugins/logstash/server/routes/api/pipelines/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/pipelines/register_delete_route.js b/x-pack/plugins/logstash/server/routes/api/pipelines/register_delete_route.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/pipelines/register_list_route.js b/x-pack/plugins/logstash/server/routes/api/pipelines/register_list_route.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/pipelines/register_pipelines_routes.js b/x-pack/plugins/logstash/server/routes/api/pipelines/register_pipelines_routes.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/upgrade/index.js b/x-pack/plugins/logstash/server/routes/api/upgrade/index.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/upgrade/register_execute_route.js b/x-pack/plugins/logstash/server/routes/api/upgrade/register_execute_route.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/logstash/server/routes/api/upgrade/register_upgrade_routes.js b/x-pack/plugins/logstash/server/routes/api/upgrade/register_upgrade_routes.js old mode 100644 new mode 100755 diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/constants/states.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/constants/states.js index b90428c5ab841..795cfd07fc807 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/constants/states.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/constants/states.js @@ -4,18 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ - - export const CHART_STATE = { NOT_STARTED: 0, LOADING: 1, LOADED: 2, - NO_RESULTS: 3 + NO_RESULTS: 3, }; export const JOB_STATE = { NOT_STARTED: 0, RUNNING: 1, FINISHED: 2, - STOPPING: 3 + STOPPING: 3, }; diff --git a/x-pack/plugins/monitoring/public/index.css b/x-pack/plugins/monitoring/public/index.css new file mode 100644 index 0000000000000..afaa9dfd315d9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/index.css @@ -0,0 +1,457 @@ +#monitoring-app .tab-no-data, #monitoring-app .tab-overview, #monitoring-app .tab-license { + background: #F5F5F5; } + +#monitoring-app .pui-tooltip-inner { + font-size: 12.0px; } + +#monitoring-app .monitoring-tooltip__trigger, +#monitoring-app .monitoring-tooltip__trigger:hover { + color: #2D2D2D; } + +#monitoring-app .betaIcon { + color: #666; } + +#monitoring-app .xpack-breadcrumbs { + min-height: 37px; + padding: 8px 10px; + margin: 0; } + +.monRhythmChart { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; } + +.monRhythmChart__title { + color: #2D2D2D; + margin: 0 0 8px; } + +.monRhythmChart__content { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; } + +.monRhythmChart__visualization { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + position: relative; } + .monRhythmChart__visualization > div { + min-width: 1px; + width: 100%; + height: 100%; } + .monRhythmChart__visualization div { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: transparent; } + +.monRhythmChart__legendItem { + font-size: 12.0px; + cursor: pointer; + color: #2D2D2D; } + .monRhythmChart__legendItem-isDisabled { + opacity: 0.5; } + +.monRhythmChart__legendHorizontal { + margin-top: 4px; } + +.monRhythmChart__legendLabel { + overflow: hidden; + white-space: nowrap; } + +.monRhythmChart__legendValue { + overflow: hidden; + white-space: nowrap; + margin-left: 4px; } + +.noData__content { + max-width: 600px; + text-align: center; + position: relative; } + +.monSparkline { + height: 2em; } + +.monSparklineTooltip { + font-weight: normal; + background: rgba(63, 63, 63, 0.7); + font-size: 12.0px; + padding: 4px; + border-radius: 4px; + pointer-events: none; } + +.monSparklineTooltip__xValue { + color: rgba(255, 255, 255, 0.7); } + +.monSparklineTooltip__yValue { + color: #FFF; } + +.monSparklineTooltip__caret { + font-size: 18.0px; + color: rgba(63, 63, 63, 0.7); + display: none; } + +.monSparklineTooltip__container { + position: fixed; + z-index: 2000; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; } + +.monSummaryStatus { + background-color: #F5F5F5; + border-bottom: 1px solid #D9D9D9; + padding: 16px; } + +/* + * A table that stretches the full window width and columns size appropriately to content. + * The .monitoringTable class is on the KuiControlledTable instance. + * The table within it requires the shrinkToContent flag as well as width set to 100% + */ +.monTable .kuiTable { + width: 100%; } + +.monTableCell__clusterCellExpired, +.monTableCell__offline { + color: #2D2D2D; } + +.monTableCell__clusterCellLiscense { + font-size: 16px; } + +.monTableCell__clusterCellExpiration { + color: #666; + font-size: 14px; + font-size: 1rem; + line-height: 1.5; } + +.monTableCell__name, +.monTableCell__status, +.monTableCell__version { + font-size: 16.0px; + font-size: 1.14286rem; + line-height: 1.5; } + +.monTableCell__transportAddress { + color: #666; + font-size: 14px; + font-size: 1rem; + line-height: 1.5; } + +.monTableCell__number { + font-size: 24.0px; + font-size: 1.71429rem; + line-height: 1.25; + font-weight: 600; } + +.monTableCell__splitNumber { + font-size: 16.0px; + font-size: 1.14286rem; + line-height: 1.5; } + +.monTableCell__metricCellMetric { + display: inline-block; + font-size: 24.0px; + font-size: 1.71429rem; + line-height: 1.25; + font-weight: 600; } + +.monTableCell__metricCellSlopeArrow { + display: inline-block; + margin-left: 4px; + font-size: 24.0px; + font-size: 1.71429rem; + line-height: 1.25; + font-weight: 600; } + +.monTableCell__metricCellMixMax { + display: inline-block; + text-align: right; + margin-left: 4px; + color: #666; + font-size: 14px; + font-size: 1rem; + line-height: 1.5; } + +monitoring-main[page="pipeline"] { + background: #F5F5F5; + min-height: 100vh; } + +.monPipelineViewer { + max-width: 1000px; } + +.monPipelineViewer__statement { + padding-left: 12px; } + +.monPipelineViewer__plugin { + margin-left: 4px; } + +.monPipelineViewer__spaceContainer { + background-color: #FFF; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + border-bottom: solid 2px #FFF; } + +.monPipelineViewer__spacer { + width: 12px; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + margin-left: 12px; + border-left: 1px #D9D9D9 dashed; } + .monPipelineViewer__spacer:last-child { + width: 0px; } + .monPipelineViewer__spacer:first-child { + margin-left: 23px; } + +.monPipelineViewer__metric { + text-align: right; } + .monPipelineViewer__metric--cpuTime { + width: 40px; } + .monPipelineViewer__metric--events, .monPipelineViewer__metric--eventsEmitted { + width: 160px; } + .monPipelineViewer__metric--eventMillis { + width: 80px; } + +.monPipelineViewer__queueMessage { + margin-left: 24px; + color: #666; } + +.monPipelineViewer__list .monPipelineViewer__listItem { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 32px; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + padding-right: 12px; } + .monPipelineViewer__list .monPipelineViewer__listItem:nth-child(2n+1) { + background: whitesmoke; } + +.monPipelineViewer__conditional { + font-weight: bold; } + +img.lspvDetailDrawerIcon { + display: inline; + margin: 0 4px 0 0; + width: auto; + vertical-align: middle; } + +.lspvDetailDrawerSparklineContainer { + width: 7vw; } + +@media only screen and (min-width: 768px) and (max-width: 991px) { + .monPipelineViewer .monPipelineViewer__spacer { + border: none; } + .monPipelineViewer .monPipelineViewer__metricFlexItem { + margin-bottom: 4px !important; } + .monPipelineViewer .monPipelineViewer__metric { + text-align: left; + padding-left: 32px; } } + +.monChart__container { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + height: 200px; + margin-bottom: 16px; } + +.monChart__tooltipTrigger { + float: right; + position: relative; + top: 4px; + right: 16px; } + +.monChart__tooltipLabel, +.monChart__tooltipValue { + text-align: left; + font-size: 12.0px; + padding: 4px; + word-wrap: break-word; + white-space: normal; } + +.monChart__tooltipLabel { + font-weight: 700; } + +monitoring-shard-allocation { + display: block; + border-top: 8px solid #F5F5F5; } + +.monClusterTitle { + font-size: 18.0px; + margin: 0; } + +.monCluster cluster-view { + display: block; } + +.monCluster .parent { + padding-top: 14px; + border-left: 3px solid #017F75 !important; } + .monCluster .parent.red { + border-left: 3px solid #A30000 !important; } + .monCluster .parent.yellow { + border-left: 3px solid #E5830E !important; } + +.monCluster td.unassigned { + vertical-align: middle; + width: 150px; } + +.monCluster .children { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -webkit-flex-flow: row wrap; + -ms-flex-flow: row wrap; + flex-flow: row wrap; } + +.monCluster .child { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + background-color: #666; + margin: 5px; } + .monCluster .child.index { + border-left: 4px solid #017F75; } + .monCluster .child.index.red { + border-left: 4px solid #A30000; } + .monCluster .child.index.yellow { + border-left: 4px solid #E5830E; } + .monCluster .child .title { + padding: 5px 7px; + float: left; + text-align: center; + font-size: 12px; + font: 10px sans-serif; + color: #FFF; } + .monCluster .child .title a { + color: #FFF; + text-decoration: none; } + .monCluster .child .title i { + margin-left: 5px; } + .monCluster .child.unassigned .title { + color: #999; + display: none; } + +.monCluster th { + text-align: left; } + +.monCluster td:first-child { + width: 200px; } + +.monCluster .shard { + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + padding: 5px 7px; + background-color: #0079a5; + font: 10px sans-serif; + border-left: 1px solid #FFF; + position: relative; + color: #FFF; } + .monCluster .shard .shard-tooltip { + padding: 5px; + bottom: 25px; + left: 0; + background-color: #D9D9D9; + position: absolute; + color: #666; + border: 1px solid #D9D9D9; + white-space: nowrap; } + .monCluster .shard.replica { + background-color: #268db3; } + .monCluster .shard.unassigned { + background-color: #999 !important; + color: #000; } + .monCluster .shard.emergency { + background-color: #A30000 !important; + color: #000; } + .monCluster .shard.relocating { + background-color: #490092; } + .monCluster .shard.initializing { + background-color: #6426a2; } + +.monCluster .legend { + font-size: 12px; + background-color: #FFF; + color: #3F3F3F; + padding: 5px; } + .monCluster .legend .title { + margin-left: 5px; + font-weight: bold; } + .monCluster .legend span.shard { + float: none; + display: inline-block; + margin: 0 5px 0 10px; + padding: 0 4px; } +/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIl9oYWNrcy5zY3NzIiwiLi4vLi4vLi4vLi4vbm9kZV9tb2R1bGVzL0BlbGFzdGljL2V1aS9zcmMvZ2xvYmFsX3N0eWxpbmcvdmFyaWFibGVzL19jb2xvcnMuc2NzcyIsIi4uLy4uLy4uLy4uL25vZGVfbW9kdWxlcy9AZWxhc3RpYy9ldWkvc3JjL3RoZW1lcy9rNi9rNl9nbG9iYWxzLnNjc3MiLCIuLi8uLi8uLi8uLi9ub2RlX21vZHVsZXMvQGVsYXN0aWMvZXVpL3NyYy90aGVtZXMvazYvazZfY29sb3JzX2xpZ2h0LnNjc3MiLCJjb21wb25lbnRzL2NoYXJ0L19jaGFydC5zY3NzIiwiLi4vLi4vLi4vLi4vbm9kZV9tb2R1bGVzL0BlbGFzdGljL2V1aS9zcmMvZ2xvYmFsX3N0eWxpbmcvdmFyaWFibGVzL19zaXplLnNjc3MiLCJjb21wb25lbnRzL25vX2RhdGEvX25vX2RhdGEuc2NzcyIsImNvbXBvbmVudHMvc3BhcmtsaW5lL19zcGFya2xpbmUuc2NzcyIsIi4uLy4uLy4uLy4uL25vZGVfbW9kdWxlcy9AZWxhc3RpYy9ldWkvc3JjL2dsb2JhbF9zdHlsaW5nL3ZhcmlhYmxlcy9fYm9yZGVycy5zY3NzIiwiLi4vLi4vLi4vLi4vbm9kZV9tb2R1bGVzL0BlbGFzdGljL2V1aS9zcmMvZ2xvYmFsX3N0eWxpbmcvdmFyaWFibGVzL196X2luZGV4LnNjc3MiLCJjb21wb25lbnRzL3N1bW1hcnlfc3RhdHVzL19zdW1tYXJ5X3N0YXR1cy5zY3NzIiwiY29tcG9uZW50cy90YWJsZS9fdGFibGUuc2NzcyIsIi4uLy4uLy4uLy4uL25vZGVfbW9kdWxlcy9AZWxhc3RpYy9ldWkvc3JjL2dsb2JhbF9zdHlsaW5nL3ZhcmlhYmxlcy9fdHlwb2dyYXBoeS5zY3NzIiwiLi4vLi4vLi4vLi4vbm9kZV9tb2R1bGVzL0BlbGFzdGljL2V1aS9zcmMvZ2xvYmFsX3N0eWxpbmcvbWl4aW5zL190eXBvZ3JhcGh5LnNjc3MiLCJjb21wb25lbnRzL2xvZ3N0YXNoL3BpcGVsaW5lX3ZpZXdlci92aWV3cy9fcGlwZWxpbmVfdmlld2VyLnNjc3MiLCIuLi8uLi8uLi8uLi9ub2RlX21vZHVsZXMvQGVsYXN0aWMvZXVpL3NyYy9nbG9iYWxfc3R5bGluZy9mdW5jdGlvbnMvX2NvbG9ycy5zY3NzIiwiLi4vLi4vLi4vLi4vbm9kZV9tb2R1bGVzL0BlbGFzdGljL2V1aS9zcmMvZ2xvYmFsX3N0eWxpbmcvbWl4aW5zL19yZXNwb25zaXZlLnNjc3MiLCJkaXJlY3RpdmVzL2NoYXJ0L19jaGFydC5zY3NzIiwiZGlyZWN0aXZlcy9lbGFzdGljc2VhcmNoL3NoYXJkX2FsbG9jYXRpb24vX3NoYXJkX2FsbG9jYXRpb24uc2NzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtFQUdJLG9CQ1kyQixFRFg1Qjs7QUFKSDtFQVFJLGtCRURvRCxFRkVyRDs7QUFUSDs7RUFhSSxlR2JrQixFSGNuQjs7QUFkSDtFQWtCSSxZQ0FvQixFRENyQjs7QUFuQkg7RUF1QkksaUJBQWdCO0VBQ2hCLGtCQUFpQjtFQUNqQixVQUFTLEVBQ1Y7O0FJcEJIO0VBQ0UsbUJBQWtCO0VBQ2xCLHFCQUFhO0VBQWIsc0JBQWE7RUFBYixxQkFBYTtFQUFiLGNBQWE7RUFDYiw2QkFBc0I7RUFBdEIsOEJBQXNCO0VBQXRCLCtCQUFzQjtNQUF0QiwyQkFBc0I7VUFBdEIsdUJBQXNCO0VBQ3RCLG9CQUFjO0VBQWQsdUJBQWM7TUFBZCxtQkFBYztVQUFkLGVBQWMsRUFDZjs7QUFFRDtFQUNFLGVEZG9CO0VDZXBCLGdCQ1p5QixFRGExQjs7QUFFRDtFQUNFLG1CQUFrQjtFQUNsQixPQUFNO0VBQ04sU0FBUTtFQUNSLFVBQVM7RUFDVCxRQUFPO0VBQ1AscUJBQWE7RUFBYixzQkFBYTtFQUFiLHFCQUFhO0VBQWIsY0FBYTtFQUNiLG9CQUFjO0VBQWQsdUJBQWM7TUFBZCxtQkFBYztVQUFkLGVBQWMsRUFDZjs7QUFFRDtFQUNFLHFCQUFhO0VBQWIsc0JBQWE7RUFBYixxQkFBYTtFQUFiLGNBQWE7RUFDYiw2QkFBc0I7RUFBdEIsOEJBQXNCO0VBQXRCLCtCQUFzQjtNQUF0QiwyQkFBc0I7VUFBdEIsdUJBQXNCO0VBQ3RCLG9CQUFjO0VBQWQsdUJBQWM7TUFBZCxtQkFBYztVQUFkLGVBQWM7RUFDZCxtQkFBa0IsRUFhbkI7RUFqQkQ7SUFRSSxlQUFjO0lBQ2QsWUFBVztJQUNYLGFBQVksRUFDYjtFQVhIO0lBM0JFLDBCQUFpQjtPQUFqQix1QkFBaUI7UUFBakIsc0JBQWlCO1lBQWpCLGtCQUFpQjtJQUNqQiw0QkFBMkI7SUFDM0IseUNBQXdDLEVBeUN2Qzs7QUFHSDtFQUNFLGtCRnpDc0Q7RUUwQ3RELGdCQUFlO0VBQ2YsZURsRG9CLEVDdURyQjtFQUhDO0lBQ0UsYUFBWSxFQUNiOztBQUdIO0VBQ0UsZ0JDeEQwQixFRHlEM0I7O0FBRUQ7RUFDRSxpQkFBZ0I7RUFDaEIsb0JBQW1CLEVBQ3BCOztBQUNEO0VBQ0UsaUJBQWdCO0VBQ2hCLG9CQUFtQjtFQUNuQixpQkNsRTBCLEVEbUUzQjs7QUVyRUQ7RUFDRSxpQkFBZ0I7RUFDaEIsbUJBQWtCO0VBQ2xCLG1CQUFrQixFQUNuQjs7QUNKRDtFQUNFLFlBQVcsRUFDWjs7QUFHRDtFQUNFLG9CQUFtQjtFQUNuQixrQ0FBcUQ7RUFDckQsa0JMRHNEO0VLRXRELGFGUDBCO0VFUTFCLG1CQ0ptQjtFREtuQixxQkFBb0IsRUFDckI7O0FBRUQ7RUFDRSxnQ0FBeUMsRUFDMUM7O0FBRUQ7RUFDRSxZTmJrQixFTWNuQjs7QUFFRDtFQUNFLGtCTGJzRDtFS2N0RCw2QkFBZ0Q7RUFDaEQsY0FBYSxFQUNkOztBQUVEO0VBQ0UsZ0JBQWU7RUFDZixjRWJ3QjtFRmN4QixxQkFBYTtFQUFiLHNCQUFhO0VBQWIscUJBQWE7RUFBYixjQUFhO0VBQ2IsK0JBQW1CO0VBQW5CLDhCQUFtQjtFQUFuQiw0QkFBbUI7TUFBbkIsd0JBQW1CO1VBQW5CLG9CQUFtQjtFQUNuQix5QkFBdUI7RUFBdkIsZ0NBQXVCO01BQXZCLHNCQUF1QjtVQUF2Qix3QkFBdUI7RUFDdkIsMEJBQW1CO0VBQW5CLDRCQUFtQjtNQUFuQix1QkFBbUI7VUFBbkIsb0JBQW1CLEVBQ3BCOztBR25DRDtFQUNFLDBCVGM2QjtFU2I3QixpQ1RjMEI7RVNiMUIsY0xIZ0IsRUtJakI7O0FDSkQ7Ozs7R0FJRztBQUNIO0VBQ0UsWUFBVyxFQUNaOztBQUVEOztFQUVFLGVSWG9CLEVRWXJCOztBQUVEO0VBQ0UsZ0JBQWUsRUFDaEI7O0FBQ0Q7RUFDRSxZVkFzQjtFV0p0QixnQlZOc0Q7RVVPdEQsZ0JBUHFDO0VDaURyQyxpQkRYcUIsRUQxQnRCOztBQUVEOzs7RUNSRSxrQlZMc0Q7RVVNdEQsc0JBUHFDO0VDMkRyQyxpQkRyQnFCLEVEcEJ0Qjs7QUFFRDtFQUNFLFlWWHNCO0VXSnRCLGdCVk5zRDtFVU90RCxnQkFQcUM7RUNpRHJDLGlCRFhxQixFRGZ0Qjs7QUFFRDtFQ25CRSxrQlZIc0Q7RVVJdEQsc0JBUHFDO0VDcUVyQyxrQkFBaUI7RUFDakIsaUJYL0RzQixFU29CdkI7O0FBRUQ7RUN2QkUsa0JWTHNEO0VVTXRELHNCQVBxQztFQzJEckMsaUJEckJxQixFRFB0Qjs7QUFFRDtFQUNFLHNCQUFxQjtFQzVCckIsa0JWSHNEO0VVSXRELHNCQVBxQztFQ3FFckMsa0JBQWlCO0VBQ2pCLGlCWC9Ec0IsRVM2QnZCOztBQUVEO0VBQ0Usc0JBQXFCO0VBQ3JCLGlCTjlDMEI7RU9ZMUIsa0JWSHNEO0VVSXRELHNCQVBxQztFQ3FFckMsa0JBQWlCO0VBQ2pCLGlCWC9Ec0IsRVNtQ3ZCOztBQUVEO0VBQ0Usc0JBQXFCO0VBQ3JCLGtCQUFpQjtFQUNqQixpQk5yRDBCO0VNc0QxQixZVnRDc0I7RVdKdEIsZ0JWTnNEO0VVT3RELGdCQVBxQztFQ2lEckMsaUJEWHFCLEVEWXRCOztBR3pERDtFQUNFLG9CYmE2QjtFYVo3QixrQkFBaUIsRUFDbEI7O0FBRUQ7RUFDRSxrQkFBaUIsRUFDbEI7O0FBRUQ7RUFDRSxtQlRQMEIsRVNRM0I7O0FBRUQ7RUFDRSxpQlRiMEIsRVNjM0I7O0FBRUQ7RUFDRSx1QmJMdUI7RWFNdkIsNEJBQW1CO01BQW5CLDZCQUFtQjtVQUFuQixvQkFBbUI7RUFDbkIscUJBQWE7RUFBYixzQkFBYTtFQUFiLHFCQUFhO0VBQWIsY0FBYTtFQUViLDhCYlR1QixFYVV4Qjs7QUFFRDtFQUNFLFlUdkIwQjtFU3dCMUIsNEJBQW1CO01BQW5CLDZCQUFtQjtVQUFuQixvQkFBbUI7RUFDbkIsa0JUekIwQjtFUzBCMUIsZ0NBQXVDLEVBV3hDO0VBZkQ7SUFRSSxXQUFVLEVBQ1g7RUFUSDtJQWFJLGtCQUE0QixFQUM3Qjs7QUFHSDtFQUNFLGtCQUFpQixFQWFsQjtFQVhDO0lBQ0UsWVR4Q3dCLEVTeUN6QjtFQUVEO0lBQ0UsYUFBc0IsRUFDdkI7RUFFRDtJQUNFLFlBQXNCLEVBQ3ZCOztBQUdIO0VBQ0Usa0JUdkQwQjtFU3dEMUIsWWIzQ3NCLEVhNEN2Qjs7QUFFRDtFQUVJLHFCQUFhO0VBQWIsc0JBQWE7RUFBYixxQkFBYTtFQUFiLGNBQWE7RUFDYixpQlQ3RHNCO0VTOER0QiwwQkFBbUI7RUFBbkIsNEJBQW1CO01BQW5CLHVCQUFtQjtVQUFuQixvQkFBbUI7RUFDbkIsb0JUakV3QixFU3NFekI7RUFWSDtJQVFNLHVCQ3RFK0IsRUR1RWhDOztBQUlMO0VBQ0Usa0JBQWlCLEVBQ2xCOztBQUVEO0VBQ0UsZ0JBQWU7RUFDZixrQkFBd0I7RUFDeEIsWUFBVztFQUNYLHVCQUFzQixFQUN2Qjs7QUFHRDtFQUNFLFdBQVUsRUFDWDs7QUVwRFM7RUZ1RFI7SUFFSSxhQUFZLEVBQ2I7RUFISDtJQU1JLDhCQUFvQyxFQUNyQztFQVBIO0lBVUksaUJBQWdCO0lBQ2hCLG1CVG5Hb0IsRVNvR3JCLEVBQUE7O0FHMUdMO0VBQ0UscUJBQWE7RUFBYixzQkFBYTtFQUFiLHFCQUFhO0VBQWIsY0FBYTtFQUNiLDZCQUFzQjtFQUF0Qiw4QkFBc0I7RUFBdEIsK0JBQXNCO01BQXRCLDJCQUFzQjtVQUF0Qix1QkFBc0I7RUFDdEIsb0JBQWM7RUFBZCx1QkFBYztNQUFkLG1CQUFjO1VBQWQsZUFBYztFQUNkLGNBQWE7RUFDYixvQlpMZ0IsRVlNakI7O0FBRUQ7RUFDRSxhQUFZO0VBQ1osbUJBQWtCO0VBQ2xCLFNaVDBCO0VZVTFCLFlaWmdCLEVZYWpCOztBQUVEOztFQUVFLGlCQUFnQjtFQUNoQixrQmZYc0Q7RWVZdEQsYVpqQjBCO0VZa0IxQixzQkFBcUI7RUFDckIsb0JBQW1CLEVBQ3BCOztBQUVEO0VBQ0UsaUJMNkJ5QixFSzVCMUI7O0FDekJEO0VBQ0UsZUFBYztFQUNkLDhCakJZNkIsRWlCWDlCOztBQUVEO0VBQ0Usa0JoQkdzRDtFZ0JGdEQsVUFBUyxFQUNWOztBQUdEO0VBRUksZUFBYyxFQUNmOztBQUhIO0VBS0ksa0JBQWlCO0VBQ2pCLDBDQUFrRCxFQU9uRDtFQWJIO0lBUU0sMENBQWlELEVBQ2xEO0VBVEw7SUFXTSwwQ0FBa0QsRUFDbkQ7O0FBWkw7RUFlSSx1QkFBc0I7RUFDdEIsYUFBWSxFQUNiOztBQWpCSDtFQW1CSSxxQkFBYTtFQUFiLHNCQUFhO0VBQWIscUJBQWE7RUFBYixjQUFhO0VBQ2IsK0JBQW1CO0VBQW5CLDhCQUFtQjtFQUFuQiw0QkFBbUI7TUFBbkIsd0JBQW1CO1VBQW5CLG9CQUFtQixFQUNwQjs7QUFyQkg7RUF1QkkscUJBQWE7RUFBYixzQkFBYTtFQUFiLHFCQUFhO0VBQWIsY0FBYTtFQUNiLDJCQUFrQjtNQUFsQiw0QkFBa0I7VUFBbEIsbUJBQWtCO0VBVWxCLHVCakI1Qm9CO0VpQjZCcEIsWUFBVyxFQXNCWjtFQXpESDtJQTBCTSwrQmpCbkNxQixFaUIwQ3RCO0lBakNMO01BNEJRLCtCakI5QmdCLEVpQitCakI7SUE3QlA7TUErQlEsK0JqQmhDaUIsRWlCaUNsQjtFQWhDUDtJQXFDTSxpQkFBZ0I7SUFDaEIsWUFBVztJQUNYLG1CQUFrQjtJQUNsQixnQkFBZTtJQUNmLHNCQUFxQjtJQUNyQixZakJoRGMsRWlCd0RmO0lBbERMO01BNENRLFlqQmxEWTtNaUJtRFosc0JBQXFCLEVBQ3RCO0lBOUNQO01BZ0RRLGlCQUFnQixFQUNqQjtFQWpEUDtJQXFEUSxZQUFXO0lBQ1gsY0FBYSxFQUNkOztBQXZEUDtFQTRESSxpQkFBZ0IsRUFDakI7O0FBN0RIO0VBZ0VJLGFBQVksRUFDYjs7QUFqRUg7RUFvRUksMkJBQWtCO01BQWxCLDRCQUFrQjtVQUFsQixtQkFBa0I7RUFDbEIsaUJBQWdCO0VBQ2hCLDBCakJoRnFCO0VpQmlGckIsc0JBQXFCO0VBQ3JCLDRCakJ0RXFCO0VpQnVFckIsbUJBQWtCO0VBQ2xCLFlqQmhGZ0IsRWlCa0hqQjtFQTVHSDtJQTZFTSxhQUFZO0lBQ1osYUFBWTtJQUNaLFFBQU87SUFDUCwwQmpCNUVzQjtJaUI2RXRCLG1CQUFrQjtJQUNsQixZakI1RWtCO0lpQjZFbEIsMEJqQi9Fc0I7SWlCZ0Z0QixvQkFBbUIsRUFDcEI7RUFyRkw7SUF3Rk0sMEJIbEcrQixFR21HaEM7RUF6Rkw7SUE0Rk0sa0NBQWlEO0lBQ2pELFlqQnJGa0IsRWlCc0ZuQjtFQTlGTDtJQWlHTSxxQ0FBNEM7SUFDNUMsWWpCMUZrQixFaUIyRm5CO0VBbkdMO0lBc0dNLDBCakIvRWdCLEVpQmdGakI7RUF2R0w7SUEwR00sMEJIcEgrQixFR3FIaEM7O0FBM0dMO0VBK0dJLGdCQUFlO0VBQ2YsdUJqQjlHcUI7RWlCbUhyQixlakI5RzBCO0VpQitHMUIsYUFBWSxFQU9iO0VBN0hIO0lBa0hNLGlCQUFnQjtJQUNoQixrQkFBaUIsRUFDbEI7RUFwSEw7SUF3SE0sWUFBVztJQUNYLHNCQUFxQjtJQUNyQixxQkFBb0I7SUFDcEIsZUFBYyxFQUNmIiwiZmlsZSI6InRvLmNzcyJ9 */ \ No newline at end of file diff --git a/x-pack/plugins/watcher/common/constants/index_names.js b/x-pack/plugins/watcher/common/constants/index_names.js index 120de88bfc02e..663d932fbe133 100644 --- a/x-pack/plugins/watcher/common/constants/index_names.js +++ b/x-pack/plugins/watcher/common/constants/index_names.js @@ -6,5 +6,5 @@ export const INDEX_NAMES = { WATCHES: '.watches', - WATCHER_HISTORY: '.watcher-history-*' + WATCHER_HISTORY: '.watcher-history-*', }; diff --git a/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js new file mode 100644 index 0000000000000..f5a117bfb359a --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js @@ -0,0 +1,257 @@ +/* + * 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 expect from 'expect.js'; +import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const chance = getService('chance'); + + describe('assign_tags_to_beats', () => { + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('should add a single tag to a single beat', async () => { + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/assignments') + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [{ beatId: 'bar', tag: 'production' }], + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([{ status: 200, result: 'updated' }]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar`, + }); + + const beat = esResponse._source.beat; + expect(beat.tags).to.eql(['production']); + }); + + it('should not re-add an existing tag to a beat', async () => { + const tags = ['production']; + + let esResponse; + let beat; + + // Before adding the existing tag + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo`, + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql([...tags, 'qa']); + + // Adding the existing tag + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/assignments') + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [{ beatId: 'foo', tag: 'production' }], + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([{ status: 200, result: 'updated' }]); + + // After adding the existing tag + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo`, + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql([...tags, 'qa']); + }); + + it('should add a single tag to a multiple beats', async () => { + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/assignments') + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'development' }, + ], + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let esResponse; + let beat; + + // Beat foo + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo`, + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + // Beat bar + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar`, + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['development']); + }); + + it('should add multiple tags to a single beat', async () => { + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/assignments') + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beatId: 'bar', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ], + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar`, + }); + + const beat = esResponse._source.beat; + expect(beat.tags).to.eql(['development', 'production']); + }); + + it('should add multiple tags to a multiple beats', async () => { + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/assignments') + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ], + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let esResponse; + let beat; + + // Beat foo + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo`, + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + // Beat bar + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar`, + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['production']); + }); + + it('should return errors for non-existent beats', async () => { + const nonExistentBeatId = chance.word(); + + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/assignments') + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [{ beatId: nonExistentBeatId, tag: 'production' }], + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 404, result: `Beat ${nonExistentBeatId} not found` }, + ]); + }); + + it('should return errors for non-existent tags', async () => { + const nonExistentTag = chance.word(); + + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/assignments') + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [{ beatId: 'bar', tag: nonExistentTag }], + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 404, result: `Tag ${nonExistentTag} not found` }, + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar`, + }); + + const beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + + it('should return errors for non-existent beats and tags', async () => { + const nonExistentBeatId = chance.word(); + const nonExistentTag = chance.word(); + + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/assignments') + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [{ beatId: nonExistentBeatId, tag: nonExistentTag }], + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 404, result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found` }, + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar`, + }); + + const beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/constants.js b/x-pack/test/api_integration/apis/beats/constants.js new file mode 100644 index 0000000000000..00327aface171 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/constants.js @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export const ES_INDEX_NAME = '.management-beats'; +export const ES_TYPE_NAME = '_doc'; + diff --git a/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js b/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js new file mode 100644 index 0000000000000..86b80323773b4 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js @@ -0,0 +1,100 @@ +/* + * 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 expect from 'expect.js'; +import moment from 'moment'; +import { + ES_INDEX_NAME, + ES_TYPE_NAME +} from './constants'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const chance = getService('chance'); + const es = getService('es'); + + describe('create_enrollment_token', () => { + it('should create one token by default', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/enrollment_tokens' + ) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + + const tokensFromApi = apiResponse.tokens; + + const esResponse = await es.search({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + q: 'type:enrollment_token' + }); + + const tokensInEs = esResponse.hits.hits + .map(hit => hit._source.enrollment_token.token); + + expect(tokensFromApi.length).to.eql(1); + expect(tokensFromApi).to.eql(tokensInEs); + }); + + it('should create the specified number of tokens', async () => { + const numTokens = chance.integer({ min: 1, max: 2000 }); + + const { body: apiResponse } = await supertest + .post( + '/api/beats/enrollment_tokens' + ) + .set('kbn-xsrf', 'xxx') + .send({ + num_tokens: numTokens + }) + .expect(200); + + const tokensFromApi = apiResponse.tokens; + + const esResponse = await es.search({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + q: 'type:enrollment_token', + size: numTokens + }); + + const tokensInEs = esResponse.hits.hits + .map(hit => hit._source.enrollment_token.token); + + expect(tokensFromApi.length).to.eql(numTokens); + expect(tokensFromApi).to.eql(tokensInEs); + }); + + it('should set token expiration to 10 minutes from now by default', async () => { + await supertest + .post( + '/api/beats/enrollment_tokens' + ) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + + const esResponse = await es.search({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + q: 'type:enrollment_token' + }); + + const tokenInEs = esResponse.hits.hits[0]._source.enrollment_token; + + // We do a fuzzy check to see if the token expires between 9 and 10 minutes + // from now because a bit of time has elapsed been the creation of the + // tokens and this check. + const tokenExpiresOn = moment(tokenInEs.expires_on).valueOf(); + const tenMinutesFromNow = moment().add('10', 'minutes').valueOf(); + const almostTenMinutesFromNow = moment(tenMinutesFromNow).subtract('2', 'seconds').valueOf(); + expect(tokenExpiresOn).to.be.lessThan(tenMinutesFromNow); + expect(tokenExpiresOn).to.be.greaterThan(almostTenMinutesFromNow); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js new file mode 100644 index 0000000000000..983182184a630 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/enroll_beat.js @@ -0,0 +1,187 @@ +/* + * 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 expect from 'expect.js'; +import moment from 'moment'; + +import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const chance = getService('chance'); + const es = getService('es'); + + describe('enroll_beat', () => { + let validEnrollmentToken; + let beatId; + let beat; + + beforeEach(async () => { + validEnrollmentToken = chance.word(); + + beatId = chance.word(); + const version = + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); + + beat = { + type: 'filebeat', + host_name: 'foo.bar.com', + name: chance.word(), + version, + }; + + await es.index({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `enrollment_token:${validEnrollmentToken}`, + body: { + type: 'enrollment_token', + enrollment_token: { + token: validEnrollmentToken, + expires_on: moment() + .add(4, 'hours') + .toJSON(), + }, + }, + }); + }); + + it('should enroll beat in a verified state', async () => { + await supertest + .post(`/api/beats/agent/${beatId}`) + .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', validEnrollmentToken) + .send(beat) + .expect(201); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:${beatId}`, + }); + + expect(esResponse._source.beat).to.have.property('verified_on'); + expect(esResponse._source.beat).to.have.property('host_ip'); + }); + + it('should contain an access token in the response', async () => { + const { body: apiResponse } = await supertest + .post(`/api/beats/agent/${beatId}`) + .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', validEnrollmentToken) + .send(beat) + .expect(201); + + const accessTokenFromApi = apiResponse.access_token; + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:${beatId}`, + }); + + const accessTokenInEs = esResponse._source.beat.access_token; + + expect(accessTokenFromApi.length).to.be.greaterThan(0); + expect(accessTokenFromApi).to.eql(accessTokenInEs); + }); + + it('should reject an invalid enrollment token', async () => { + const { body: apiResponse } = await supertest + .post(`/api/beats/agent/${beatId}`) + .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', chance.word()) + .send(beat) + .expect(400); + + expect(apiResponse).to.eql({ message: 'Invalid enrollment token' }); + }); + + it('should reject an expired enrollment token', async () => { + const expiredEnrollmentToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1LCJleHAiOjE1MzAzMzAxMzV9.' + + 'Azf4czAwWZEflR7Pf8pi-DUTcve9xyxWyViNYeUSGog'; + + await es.index({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `enrollment_token:${expiredEnrollmentToken}`, + body: { + type: 'enrollment_token', + enrollment_token: { + token: expiredEnrollmentToken, + expires_on: moment() + .subtract(1, 'minute') + .toJSON(), + }, + }, + }); + + const { body: apiResponse } = await supertest + .post(`/api/beats/agent/${beatId}`) + .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', expiredEnrollmentToken) + .send(beat) + .expect(400); + + expect(apiResponse).to.eql({ message: 'Expired enrollment token' }); + }); + + it('should delete the given enrollment token so it may not be reused', async () => { + await supertest + .post(`/api/beats/agent/${beatId}`) + .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', validEnrollmentToken) + .send(beat) + .expect(201); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `enrollment_token:${validEnrollmentToken}`, + ignore: [404], + }); + + expect(esResponse.found).to.be(false); + }); + + it('should fail if the beat with the same ID is enrolled twice', async () => { + await supertest + .post(`/api/beats/agent/${beatId}`) + .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', validEnrollmentToken) + .send(beat) + .expect(201); + + await es.index({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `enrollment_token:${validEnrollmentToken}`, + body: { + type: 'enrollment_token', + enrollment_token: { + token: validEnrollmentToken, + expires_on: moment() + .add(4, 'hours') + .toJSON(), + }, + }, + }); + + await supertest + .post(`/api/beats/agent/${beatId}`) + .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', validEnrollmentToken) + .send(beat) + .expect(201); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/get_beat.js b/x-pack/test/api_integration/apis/beats/get_beat.js new file mode 100644 index 0000000000000..c15c7e66f7e29 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/get_beat.js @@ -0,0 +1,51 @@ +/* + * 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 expect from 'expect.js'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('get_beat_configuration', () => { + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('should return merged configuration for the beat', async () => { + const { body: apiResponse } = await supertest + .get('/api/beats/agent/foo/configuration') + .set( + 'kbn-beats-access-token', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + + 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI' + ) + .expect(200); + + const configurationBlocks = apiResponse.configuration_blocks; + + expect(configurationBlocks).to.be.an(Array); + expect(configurationBlocks.length).to.be(3); + + expect(configurationBlocks[1].type).to.be('metricbeat.modules'); + expect(configurationBlocks[1].config).not.to.be.an('array'); + expect(configurationBlocks[1].config).to.eql({ + module: 'memcached', + hosts: ['localhost:11211'], + }); + + expect(configurationBlocks[2].type).to.be('metricbeat.modules'); + expect(configurationBlocks[2].config).not.to.be.an('array'); + expect(configurationBlocks[2].config).to.eql({ + module: 'memcached', + hosts: ['localhost:4949'], + 'node.namespace': 'node', + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js new file mode 100644 index 0000000000000..a6552a383dbd8 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -0,0 +1,29 @@ +/* + * 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 { ES_INDEX_NAME } from './constants'; + +export default function ({ getService, loadTestFile }) { + const es = getService('es'); + + describe('beats', () => { + const cleanup = () => es.indices.delete({ + index: ES_INDEX_NAME, + ignore: [404] + }); + + beforeEach(cleanup); + + loadTestFile(require.resolve('./create_enrollment_tokens')); + loadTestFile(require.resolve('./enroll_beat')); + loadTestFile(require.resolve('./list_beats')); + loadTestFile(require.resolve('./update_beat')); + loadTestFile(require.resolve('./set_tag')); + loadTestFile(require.resolve('./assign_tags_to_beats')); + loadTestFile(require.resolve('./remove_tags_from_beats')); + loadTestFile(require.resolve('./get_beat')); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/list_beats.js b/x-pack/test/api_integration/apis/beats/list_beats.js new file mode 100644 index 0000000000000..b78939b30b62f --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/list_beats.js @@ -0,0 +1,38 @@ +/* + * 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 expect from 'expect.js'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('list_beats', () => { + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('should return all beats', async () => { + const { body: apiResponse } = await supertest.get('/api/beats/agents').expect(200); + + const beatsFromApi = apiResponse.beats; + + expect(beatsFromApi.length).to.be(4); + expect(beatsFromApi.filter(beat => beat.hasOwnProperty('verified_on')).length).to.be(1); + expect(beatsFromApi.find(beat => beat.hasOwnProperty('verified_on')).id).to.be('foo'); + }); + + it('should not return access tokens', async () => { + const { body: apiResponse } = await supertest.get('/api/beats/agents').expect(200); + + const beatsFromApi = apiResponse.beats; + + expect(beatsFromApi.length).to.be(4); + expect(beatsFromApi.filter(beat => beat.hasOwnProperty('access_token')).length).to.be(0); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js b/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js new file mode 100644 index 0000000000000..999d3614451a2 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js @@ -0,0 +1,210 @@ +/* + * 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 expect from 'expect.js'; +import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const chance = getService('chance'); + + describe('remove_tags_from_beats', () => { + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('should remove a single tag from a single beat', async () => { + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/removals') + .set('kbn-xsrf', 'xxx') + .send({ + removals: [{ beatId: 'foo', tag: 'production' }], + }) + .expect(200); + + expect(apiResponse.removals).to.eql([{ status: 200, result: 'updated' }]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo`, + }); + + const beat = esResponse._source.beat; + expect(beat.tags).to.eql(['qa']); + }); + + it('should remove a single tag from a multiple beats', async () => { + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/removals') + .set('kbn-xsrf', 'xxx') + .send({ + removals: [{ beatId: 'foo', tag: 'development' }, { beatId: 'bar', tag: 'development' }], + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let esResponse; + let beat; + + // Beat foo + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo`, + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['production', 'qa']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + // Beat bar + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar`, + }); + + beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + + it('should remove multiple tags from a single beat', async () => { + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/removals') + .set('kbn-xsrf', 'xxx') + .send({ + removals: [{ beatId: 'foo', tag: 'development' }, { beatId: 'foo', tag: 'production' }], + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo`, + }); + + const beat = esResponse._source.beat; + expect(beat.tags).to.eql(['qa']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + }); + + it('should remove multiple tags from a multiple beats', async () => { + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/removals') + .set('kbn-xsrf', 'xxx') + .send({ + removals: [{ beatId: 'foo', tag: 'production' }, { beatId: 'bar', tag: 'development' }], + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let esResponse; + let beat; + + // Beat foo + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo`, + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['qa']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + // Beat bar + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar`, + }); + + beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + + it('should return errors for non-existent beats', async () => { + const nonExistentBeatId = chance.word(); + + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/removals') + .set('kbn-xsrf', 'xxx') + .send({ + removals: [{ beatId: nonExistentBeatId, tag: 'production' }], + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 404, result: `Beat ${nonExistentBeatId} not found` }, + ]); + }); + + it('should return errors for non-existent tags', async () => { + const nonExistentTag = chance.word(); + + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/removals') + .set('kbn-xsrf', 'xxx') + .send({ + removals: [{ beatId: 'bar', tag: nonExistentTag }], + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 404, result: `Tag ${nonExistentTag} not found` }, + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar`, + }); + + const beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + + it('should return errors for non-existent beats and tags', async () => { + const nonExistentBeatId = chance.word(); + const nonExistentTag = chance.word(); + + const { body: apiResponse } = await supertest + .post('/api/beats/agents_tags/removals') + .set('kbn-xsrf', 'xxx') + .send({ + removals: [{ beatId: nonExistentBeatId, tag: nonExistentTag }], + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 404, result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found` }, + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar`, + }); + + const beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/set_tag.js b/x-pack/test/api_integration/apis/beats/set_tag.js new file mode 100644 index 0000000000000..06d98721c85bf --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/set_tag.js @@ -0,0 +1,273 @@ +/* + * 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 expect from 'expect.js'; +import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const chance = getService('chance'); + const es = getService('es'); + + describe('set_tag', () => { + it('should create an empty tag', async () => { + const tagId = 'production'; + await supertest + .put(`/api/beats/tag/${tagId}`) + .set('kbn-xsrf', 'xxx') + .send() + .expect(201); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `tag:${tagId}`, + }); + + const tagInEs = esResponse._source; + + expect(tagInEs.type).to.be('tag'); + expect(tagInEs.tag.id).to.be(tagId); + expect(tagInEs.tag.configuration_blocks).to.be.an(Array); + expect(tagInEs.tag.configuration_blocks.length).to.be(0); + }); + + it('should create a tag with one configuration block', async () => { + const tagId = 'production'; + await supertest + .put(`/api/beats/tag/${tagId}`) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: 'output', + description: 'smething', + configs: [ + { + elasticsearch: { + hosts: ['localhost:9200'], + username: 'foo', + }, + }, + ], + }, + ], + }) + .expect(201); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `tag:${tagId}`, + }); + + const tagInEs = esResponse._source; + + expect(tagInEs.type).to.be('tag'); + expect(tagInEs.tag.id).to.be(tagId); + expect(tagInEs.tag.configuration_blocks).to.be.an(Array); + expect(tagInEs.tag.configuration_blocks.length).to.be(1); + expect(tagInEs.tag.configuration_blocks[0].type).to.be('output'); + expect(tagInEs.tag.configuration_blocks[0].configs).to.eql([ + { + elasticsearch: { + hosts: ['localhost:9200'], + username: 'foo', + }, + }, + ]); + }); + + it('should create a tag with two configuration blocks', async () => { + const tagId = 'production'; + await supertest + .put(`/api/beats/tag/${tagId}`) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: 'filebeat.inputs', + configs: [ + { + paths: ['./foo'], + }, + ], + }, + { + type: 'output', + configs: [ + { + elasticsearch: { + hosts: ['localhost:9200'], + username: 'foo', + }, + }, + ], + }, + ], + }) + .expect(201); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `tag:${tagId}`, + }); + + const tagInEs = esResponse._source; + + expect(tagInEs.type).to.be('tag'); + expect(tagInEs.tag.id).to.be(tagId); + expect(tagInEs.tag.configuration_blocks).to.be.an(Array); + expect(tagInEs.tag.configuration_blocks.length).to.be(2); + expect(tagInEs.tag.configuration_blocks[0].type).to.be('filebeat.inputs'); + expect(tagInEs.tag.configuration_blocks[0].configs).to.eql([ + { + paths: ['./foo'], + }, + ]); + expect(tagInEs.tag.configuration_blocks[1].type).to.be('output'); + expect(tagInEs.tag.configuration_blocks[1].configs).to.eql([ + { + elasticsearch: { + hosts: ['localhost:9200'], + username: 'foo', + }, + }, + ]); + }); + + it('should fail when creating a tag with two configuration blocks of type output', async () => { + const tagId = 'production'; + await supertest + .put(`/api/beats/tag/${tagId}`) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: 'output', + configs: [ + { + elasticsearch: { + hosts: ['localhost:9200'], + username: 'foo', + }, + }, + ], + }, + { + type: 'output', + configs: [ + { + elasticsearch: { + hosts: ['localhost:9200'], + username: 'foo', + }, + }, + ], + }, + ], + }) + .expect(400); + }); + + it('should fail when creating a tag with an invalid configuration block type', async () => { + const tagId = 'production'; + await supertest + .put(`/api/beats/tag/${tagId}`) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: chance.word(), + configs: [ + { + elasticsearch: { + hosts: ['localhost:9200'], + username: 'foo', + }, + }, + ], + }, + ], + }) + .expect(400); + }); + + it('should update an existing tag', async () => { + const tagId = 'production'; + await supertest + .put(`/api/beats/tag/${tagId}`) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: 'filebeat.inputs', + configs: [ + { + paths: ['./test'], + }, + ], + }, + { + type: 'output', + configs: [ + { + elasticsearch: { + hosts: ['localhost:9200'], + username: 'foo', + }, + }, + ], + }, + ], + }) + .expect(201); + + await supertest + .put(`/api/beats/tag/${tagId}`) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: 'output', + configs: [ + { + elasticsearch: { + hosts: ['localhost:9000'], + username: 'foo', + }, + }, + ], + }, + ], + }) + .expect(200); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `tag:${tagId}`, + }); + + const tagInEs = esResponse._source; + + expect(tagInEs.type).to.be('tag'); + expect(tagInEs.tag.id).to.be(tagId); + expect(tagInEs.tag.configuration_blocks).to.be.an(Array); + expect(tagInEs.tag.configuration_blocks.length).to.be(1); + expect(tagInEs.tag.configuration_blocks[0].type).to.be('output'); + expect(tagInEs.tag.configuration_blocks[0].configs).to.eql([ + { + elasticsearch: { + hosts: ['localhost:9000'], + username: 'foo', + }, + }, + ]); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/update_beat.js b/x-pack/test/api_integration/apis/beats/update_beat.js new file mode 100644 index 0000000000000..163ddd931a625 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/update_beat.js @@ -0,0 +1,120 @@ +/* + * 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 expect from 'expect.js'; +import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants'; +import moment from 'moment'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const chance = getService('chance'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('update_beat', () => { + let validEnrollmentToken; + let beat; + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + beforeEach(async () => { + validEnrollmentToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + + 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI'; + const version = + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); + + beat = { + type: `${chance.word()}beat`, + host_name: `www.${chance.word()}.net`, + name: chance.word(), + version, + ephemeral_id: chance.word(), + }; + + await es.index({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `enrollment_token:${validEnrollmentToken}`, + body: { + type: 'enrollment_token', + enrollment_token: { + token: validEnrollmentToken, + expires_on: moment() + .add(4, 'hours') + .toJSON(), + }, + }, + }); + }); + + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('should update an existing verified beat', async () => { + const beatId = 'foo'; + await supertest + .put(`/api/beats/agent/${beatId}`) + .set('kbn-xsrf', 'xxx') + .set('kbn-beats-access-token', validEnrollmentToken) + .send(beat) + .expect(204); + + const beatInEs = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:${beatId}`, + }); + + expect(beatInEs._source.beat.id).to.be(beatId); + expect(beatInEs._source.beat.type).to.be(beat.type); + expect(beatInEs._source.beat.host_name).to.be(beat.host_name); + expect(beatInEs._source.beat.version).to.be(beat.version); + expect(beatInEs._source.beat.ephemeral_id).to.be(beat.ephemeral_id); + expect(beatInEs._source.beat.name).to.be(beat.name); + }); + + it('should return an error for an invalid access token', async () => { + const beatId = 'foo'; + const { body } = await supertest + .put(`/api/beats/agent/${beatId}`) + .set('kbn-xsrf', 'xxx') + .set('kbn-beats-access-token', chance.word()) + .send(beat) + .expect(401); + + expect(body.message).to.be('Invalid access token'); + + const beatInEs = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:${beatId}`, + }); + + expect(beatInEs._source.beat.id).to.be(beatId); + expect(beatInEs._source.beat.type).to.not.be(beat.type); + expect(beatInEs._source.beat.host_name).to.not.be(beat.host_name); + expect(beatInEs._source.beat.version).to.not.be(beat.version); + expect(beatInEs._source.beat.ephemeral_id).to.not.be(beat.ephemeral_id); + }); + + it('should return an error for a non-existent beat', async () => { + const beatId = chance.word(); + const { body } = await supertest + .put(`/api/beats/agent/${beatId}`) + .set('kbn-xsrf', 'xxx') + .set('kbn-beats-access-token', validEnrollmentToken) + .send(beat) + .expect(404); + + expect(body.message).to.be('Beat not found'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 85b11bb9ef71e..2bd1180ffe45b 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -12,5 +12,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./xpack_main')); loadTestFile(require.resolve('./logstash')); loadTestFile(require.resolve('./kibana')); + loadTestFile(require.resolve('./beats')); }); } diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 20b54a78d7cc8..62b4433679e04 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -30,6 +30,7 @@ export default async function ({ readConfigFile }) { esArchiver: kibanaCommonConfig.get('services.esArchiver'), usageAPI: UsageAPIProvider, kibanaServer: kibanaCommonConfig.get('services.kibanaServer'), + chance: kibanaAPITestsConfig.get('services.chance'), }, esArchiver: xPackFunctionalTestsConfig.get('esArchiver'), junit: { diff --git a/x-pack/test/functional/es_archives/beats/list/data.json b/x-pack/test/functional/es_archives/beats/list/data.json new file mode 100644 index 0000000000000..f263eff1a5bd4 --- /dev/null +++ b/x-pack/test/functional/es_archives/beats/list/data.json @@ -0,0 +1,158 @@ +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:qux", + "source": { + "type": "beat", + "beat": { + "type": "filebeat", + "active": true, + "host_ip": "1.2.3.4", + "host_name": "foo.bar.com", + "id": "qux", + "name": "qux_filebeat", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:baz", + "source": { + "type": "beat", + "beat": { + "type": "metricbeat", + "active": true, + "host_ip": "22.33.11.44", + "host_name": "baz.bar.com", + "id": "baz", + "name": "baz_metricbeat", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:foo", + "source": { + "type": "beat", + "beat": { + "type": "metricbeat", + "active": true, + "host_ip": "1.2.3.4", + "host_name": "foo.bar.com", + "id": "foo", + "name": "foo_metricbeat", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI", + "verified_on": "2018-05-15T16:25:38.924Z", + "tags": [ + "production", + "qa" + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:bar", + "source": { + "type": "beat", + "beat": { + "type": "filebeat", + "active": true, + "host_ip": "11.22.33.44", + "host_name": "foo.com", + "id": "bar", + "name": "bar_filebeat", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "tag:production", + "source": { + "type": "tag", + "tag": { + "configuration_blocks": [ + { + "type": "output", + "description": "some description", + "configs": [{ + "hosts": ["localhost:9200"], + "username": "some-username" + }] + }, + { + "type": "metricbeat.modules", + "configs": [{ + "module": "memcached", + "hosts": ["localhost:11211"] + }] + } + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "tag:development", + "source": { + "type": "tag", + "tag": { + "configuration_blocks": [] + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "tag:qa", + "source": { + "type": "tag", + "tag": { + "configuration_blocks": [ + { + "type": "metricbeat.modules", + "configs": [{ + "module": "memcached", + "node.namespace": "node", + "hosts": ["localhost:4949"] + }] + } + ] + } + } + } +} \ No newline at end of file diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 724d77e1b868f..0aab02a292860 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -294,6 +294,13 @@ resolved "https://registry.yarnpkg.com/@types/joi/-/joi-10.6.4.tgz#0989d69e792a7db13e951852e6949df6787f113f" integrity sha512-eS6EeSGueXvS16CsHa7OKkRK1xBb6L+rXuXlzbWSWvb4v7zgNFPmY8l6aWWgEkHFeITVBadeQHQhVUpx0sd1tw== +"@types/jsonwebtoken@^7.2.7": + version "7.2.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.8.tgz#8d199dab4ddb5bba3234f8311b804d2027af2b3a" + integrity sha512-XENN3YzEB8D6TiUww0O8SRznzy1v+77lH7UmuN54xq/IHIsyWjWOzZuFFTtoiRuaE782uAoRwBe/wwow+vQXZw== + dependencies: + "@types/node" "*" + "@types/lodash@^3.10.1": version "3.10.2" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-3.10.2.tgz#c1fbda1562ef5603c8192fe1fe65b017849d5873" @@ -316,20 +323,10 @@ dependencies: moment ">=2.14.0" -"@types/node@*": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5" - integrity sha512-wNBfvNjzsJl4tswIZKXCFQY0lss9nKUyJnG6T94X/eqjRgI2jHZ4evdjhQYBSan/vGtF6XVXPApOmNH2rf0KKw== - -"@types/node@^9.4.6": - version "9.6.22" - resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.22.tgz#05b55093faaadedea7a4b3f76e9a61346a6dd209" - integrity sha512-RIg9EkxzVMkNH0M4sLRngK23f5QiigJC0iODQmu4nopzstt8AjegYund3r82iMrd2BNCjcZVnklaItvKHaGfBA== - -"@types/node@^9.4.7": - version "9.6.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.18.tgz#092e13ef64c47e986802c9c45a61c1454813b31d" - integrity sha512-lywCnJQRSsu0kitHQ5nkb7Ay/ScdJPQjhWRtuf+G1DmNKJnPcdVyP0pYvdiDFKjzReC6NLWLgSyimno3kKfIig== +"@types/node@*", "@types/node@8.10.21", "@types/node@^9.4.6", "@types/node@^9.4.7": + version "8.10.21" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.21.tgz#12b3f2359b27aa05a45d886c8ba1eb8d1a77e285" + integrity sha512-87XkD9qDXm8fIax+5y7drx84cXsu34ZZqfB7Cial3Q/2lxSoJ/+DRaWckkCbxP41wFSIrrb939VhzaNxj4eY1w== "@types/p-cancelable@^0.3.0": version "0.3.0" @@ -390,10 +387,10 @@ "@types/react" "*" redux "^4.0.0" -"@types/react-router-dom@^4.2.6": - version "4.2.7" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.2.7.tgz#9d36bfe175f916dd8d7b6b0237feed6cce376b4c" - integrity sha512-6sIP3dIj6xquvcAuYDaxpbeLjr9954OuhCXnniMhnDgykAw2tVji9b0jKHofPJGUoHEMBsWzO83tjnk7vfzozA== +"@types/react-router-dom@4.2.6": + version "4.2.6" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.2.6.tgz#9f7eb3c0e6661a9607d878ff8675cc4ea95cd276" + integrity sha512-K7SdbkF8xgecp2WCeXw51IMySYvQ1EuVPKfjU1fymyTSX9bZk5Qx8T5cipwtAY8Zhb/4GIjhYKm0ZGVEbCKEzQ== dependencies: "@types/history" "*" "@types/react" "*" @@ -1985,6 +1982,11 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" @@ -3310,6 +3312,13 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + integrity sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM= + dependencies: + safe-buffer "^5.0.1" + elasticsearch@^15.1.1: version "15.1.1" resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-15.1.1.tgz#242f2378fccd601586ff763a8a933cd5d41c945f" @@ -4102,6 +4111,11 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data-to-object@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/form-data-to-object/-/form-data-to-object-0.2.0.tgz#f7a8e68ddd910a1100a65e25ac6a484143ff8168" + integrity sha1-96jmjd2RChEApl4lrGpIQUP/gWg= + form-data@^2.3.1, form-data@~2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" @@ -4134,6 +4148,14 @@ formidable@^1.1.1: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9" integrity sha1-lriIb3w8NQi5Mta9cMTTqI818ak= +formsy-react@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/formsy-react/-/formsy-react-1.1.5.tgz#ee0911bb70712eb6fb9924d56fdb974a19006955" + integrity sha512-nNWe4Vbp6aDQ/zSxJ7gVQgD5+avFRVbydcjA2Om42flxpQFrKE54AAbuyEj3Jvv+2b9LVl+WLMAPalyvLjwNcQ== + dependencies: + form-data-to-object "^0.2.0" + prop-types "^15.5.10" + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -5072,11 +5094,6 @@ hoek@5.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.3.tgz#b71d40d943d0a95da01956b547f83c4a5b4a34ac" integrity sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw== -hoist-non-react-statics@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" - integrity sha1-ND24TGAYxlB3iJgkATWhQg7iLOA= - hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0, hoist-non-react-statics@^2.5.5: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" @@ -6394,6 +6411,21 @@ jsonpointer@^4.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk= +jsonwebtoken@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643" + integrity sha512-oge/hvlmeJCH+iIz1DwcO7vKPkNGJHhgkspk8OH3VKlw+mbi42WtD4ig1+VXRln765vxptAv+xT26Fd3cteqag== + dependencies: + jws "^3.1.5" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -6419,6 +6451,23 @@ just-reduce-object@^1.0.3: resolved "https://registry.yarnpkg.com/just-reduce-object/-/just-reduce-object-1.1.0.tgz#d29d172264f8511c74462de30d72d5838b6967e6" integrity sha512-nGyg7N9FEZsyrGQNilkyVLxKPsf96iel5v0DrozQ19ML+96HntyS/53bOP68iK/kZUGvsL3FKygV8nQYYhgTFw== +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + integrity sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + integrity sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ== + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + keymirror@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/keymirror/-/keymirror-0.1.1.tgz#918889ea13f8d0a42e7c557250eee713adc95c35" @@ -6743,6 +6792,11 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -6753,11 +6807,26 @@ lodash.isarray@^3.0.0: resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U= +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + lodash.isequal@^4.1.1, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + lodash.isobject@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" @@ -6768,6 +6837,11 @@ lodash.isplainobject@^4.0.6: resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -6807,6 +6881,11 @@ lodash.omitby@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.omitby/-/lodash.omitby-4.6.0.tgz#5c15ff4754ad555016b53c041311e8f079204791" integrity sha1-XBX/R1StVVAWtTwEExHo8HkgR5E= +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.orderby@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.orderby/-/lodash.orderby-4.6.0.tgz#e697f04ce5d78522f54d9338b32b81a3393e4eb3" @@ -7425,7 +7504,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== @@ -9023,29 +9102,29 @@ react-router-breadcrumbs-hoc@1.1.2: integrity sha1-T6+2IOfGuHbZj3FR9Mha5cMVfcA= react-router-dom@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" - integrity sha512-cHMFC1ZoLDfEaMFoKTjN7fry/oczMgRt5BKfMAkTu5zEuJvUiPp1J8d0eXSVTnBh6pxlbdqDhozunOOLtmKfPA== + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" + integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== dependencies: history "^4.7.2" - invariant "^2.2.2" + invariant "^2.2.4" loose-envify "^1.3.1" - prop-types "^15.5.4" - react-router "^4.2.0" - warning "^3.0.0" + prop-types "^15.6.1" + react-router "^4.3.1" + warning "^4.0.1" -react-router@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986" - integrity sha512-DY6pjwRhdARE4TDw7XjxjZsbx9lKmIcyZoZ+SDO7SBJ1KUeWNxT22Kara2AC7u6/c2SYEHlEDLnzBCcNhLE8Vg== +react-router@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" + integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== dependencies: history "^4.7.2" - hoist-non-react-statics "^2.3.0" - invariant "^2.2.2" + hoist-non-react-statics "^2.5.0" + invariant "^2.2.4" loose-envify "^1.3.1" path-to-regexp "^1.7.0" - prop-types "^15.5.4" - warning "^3.0.0" + prop-types "^15.6.1" + warning "^4.0.1" react-select@^1.2.1: version "1.2.1" @@ -11506,6 +11585,13 @@ warning@^3.0.0: dependencies: loose-envify "^1.0.0" +warning@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607" + integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug== + dependencies: + loose-envify "^1.0.0" + watch@~0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" diff --git a/yarn.lock b/yarn.lock index 458f5d99947c5..a7d1390200468 100644 --- a/yarn.lock +++ b/yarn.lock @@ -620,26 +620,11 @@ resolved "https://registry.yarnpkg.com/@types/mustache/-/mustache-0.8.31.tgz#7c86cbf74f7733f9e3bdc28817623927eb386616" integrity sha512-72flCZJkEJHPwhmpHgg4a0ZBLssMhg5NB0yltRblRlZMo4py3B/u/d7icevc4EeN9MPQUo/dPtuVOoVy9ih6cQ== -"@types/node@*": - version "9.4.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275" - integrity sha512-4Ba90mWNx8ddbafuyGGwjkZMigi+AWfYLSDCpovwsE63ia8w93r3oJ8PIAQc3y8U+XHcnMOHPIzNe3o438Ywcw== - -"@types/node@^8.10.20": +"@types/node@*", "@types/node@8.10.21", "@types/node@^8.10.20", "@types/node@^9.4.6", "@types/node@^9.4.7": version "8.10.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.21.tgz#12b3f2359b27aa05a45d886c8ba1eb8d1a77e285" integrity sha512-87XkD9qDXm8fIax+5y7drx84cXsu34ZZqfB7Cial3Q/2lxSoJ/+DRaWckkCbxP41wFSIrrb939VhzaNxj4eY1w== -"@types/node@^9.4.6": - version "9.6.22" - resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.22.tgz#05b55093faaadedea7a4b3f76e9a61346a6dd209" - integrity sha512-RIg9EkxzVMkNH0M4sLRngK23f5QiigJC0iODQmu4nopzstt8AjegYund3r82iMrd2BNCjcZVnklaItvKHaGfBA== - -"@types/node@^9.4.7": - version "9.6.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.18.tgz#092e13ef64c47e986802c9c45a61c1454813b31d" - integrity sha512-lywCnJQRSsu0kitHQ5nkb7Ay/ScdJPQjhWRtuf+G1DmNKJnPcdVyP0pYvdiDFKjzReC6NLWLgSyimno3kKfIig== - "@types/p-cancelable@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@types/p-cancelable/-/p-cancelable-0.3.0.tgz#3e4fcc54a3dfd81d0f5b93546bb68d0df50553bb" @@ -2896,6 +2881,11 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" @@ -5141,6 +5131,13 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + integrity sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM= + dependencies: + safe-buffer "^5.0.1" + editions@^1.1.1, editions@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b" @@ -6511,6 +6508,11 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data-to-object@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/form-data-to-object/-/form-data-to-object-0.2.0.tgz#f7a8e68ddd910a1100a65e25ac6a484143ff8168" + integrity sha1-96jmjd2RChEApl4lrGpIQUP/gWg= + form-data@^2.3.1, form-data@~2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" @@ -6534,6 +6536,14 @@ formidable@^1.1.1: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" integrity sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg== +formsy-react@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/formsy-react/-/formsy-react-1.1.5.tgz#ee0911bb70712eb6fb9924d56fdb974a19006955" + integrity sha512-nNWe4Vbp6aDQ/zSxJ7gVQgD5+avFRVbydcjA2Om42flxpQFrKE54AAbuyEj3Jvv+2b9LVl+WLMAPalyvLjwNcQ== + dependencies: + form-data-to-object "^0.2.0" + prop-types "^15.5.10" + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -7664,16 +7674,16 @@ hoek@5.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.3.tgz#b71d40d943d0a95da01956b547f83c4a5b4a34ac" integrity sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw== -hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40" - integrity sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w== - hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== +hoist-non-react-statics@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40" + integrity sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w== + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -9496,6 +9506,21 @@ jsonpointer@^4.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk= +jsonwebtoken@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643" + integrity sha512-oge/hvlmeJCH+iIz1DwcO7vKPkNGJHhgkspk8OH3VKlw+mbi42WtD4ig1+VXRln765vxptAv+xT26Fd3cteqag== + dependencies: + jws "^3.1.5" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -9557,6 +9582,23 @@ just-reduce-object@^1.0.3: resolved "https://registry.yarnpkg.com/just-reduce-object/-/just-reduce-object-1.1.0.tgz#d29d172264f8511c74462de30d72d5838b6967e6" integrity sha512-nGyg7N9FEZsyrGQNilkyVLxKPsf96iel5v0DrozQ19ML+96HntyS/53bOP68iK/kZUGvsL3FKygV8nQYYhgTFw== +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + integrity sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + integrity sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ== + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + karma-chrome-launcher@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.1.1.tgz#216879c68ac04d8d5140e99619ba04b59afd46cf" @@ -10160,6 +10202,11 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -10170,6 +10217,11 @@ lodash.isarray@^3.0.0: resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U= +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + lodash.isempty@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" @@ -10180,11 +10232,31 @@ lodash.isequal@^4.0.0, lodash.isequal@^4.1.1, lodash.isequal@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + lodash.isobject@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -10244,6 +10316,11 @@ lodash.omitby@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.omitby/-/lodash.omitby-4.6.0.tgz#5c15ff4754ad555016b53c041311e8f079204791" integrity sha1-XBX/R1StVVAWtTwEExHo8HkgR5E= +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.orderby@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.orderby/-/lodash.orderby-4.6.0.tgz#e697f04ce5d78522f54d9338b32b81a3393e4eb3" @@ -10978,7 +11055,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== @@ -12565,7 +12642,12 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= -prettier@^1.13.7, prettier@^1.14.3: +prettier@^1.13.7: + version "1.14.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.2.tgz#0ac1c6e1a90baa22a62925f41963c841983282f9" + integrity sha512-McHPg0n1pIke+A/4VcaS2en+pTNjy4xF+Uuq86u/5dyDO59/TtFZtQ708QIRkEZ3qwKz3GVkVa6mpxK/CpB8Rg== + +prettier@^1.14.3: version "1.14.3" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895" integrity sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg== @@ -13356,7 +13438,7 @@ react-router-breadcrumbs-hoc@1.1.2: resolved "https://registry.yarnpkg.com/react-router-breadcrumbs-hoc/-/react-router-breadcrumbs-hoc-1.1.2.tgz#4fafb620e7c6b876d98f7151f4c85ae5c3157dc0" integrity sha1-T6+2IOfGuHbZj3FR9Mha5cMVfcA= -react-router-dom@4.2.2, react-router-dom@^4.2.2: +react-router-dom@4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" integrity sha512-cHMFC1ZoLDfEaMFoKTjN7fry/oczMgRt5BKfMAkTu5zEuJvUiPp1J8d0eXSVTnBh6pxlbdqDhozunOOLtmKfPA== @@ -13368,18 +13450,30 @@ react-router-dom@4.2.2, react-router-dom@^4.2.2: react-router "^4.2.0" warning "^3.0.0" -react-router@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986" - integrity sha512-DY6pjwRhdARE4TDw7XjxjZsbx9lKmIcyZoZ+SDO7SBJ1KUeWNxT22Kara2AC7u6/c2SYEHlEDLnzBCcNhLE8Vg== +react-router-dom@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" + integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== dependencies: history "^4.7.2" - hoist-non-react-statics "^2.3.0" - invariant "^2.2.2" + invariant "^2.2.4" + loose-envify "^1.3.1" + prop-types "^15.6.1" + react-router "^4.3.1" + warning "^4.0.1" + +react-router@^4.2.0, react-router@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" + integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== + dependencies: + history "^4.7.2" + hoist-non-react-statics "^2.5.0" + invariant "^2.2.4" loose-envify "^1.3.1" path-to-regexp "^1.7.0" - prop-types "^15.5.4" - warning "^3.0.0" + prop-types "^15.6.1" + warning "^4.0.1" react-select@^1.2.1: version "1.2.1" @@ -17052,6 +17146,13 @@ warning@^3.0.0: dependencies: loose-envify "^1.0.0" +warning@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607" + integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug== + dependencies: + loose-envify "^1.0.0" + watch@~0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986"