diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap new file mode 100644 index 0000000000000..7f5b8406b89f3 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -0,0 +1,1765 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to create cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "message": "Failed attempt to create a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User is creating cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "message": "User is creating a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createComment" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to create cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createComment" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "message": "Failed attempt to create a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createComment" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User is creating cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createComment" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "message": "User is creating a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createConfiguration" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-configure", + }, + }, + "message": "Failed attempt to create cases-configure [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createConfiguration" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "message": "Failed attempt to create a case configuration as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createConfiguration" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-configure", + }, + }, + "message": "User is creating cases-configure [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createConfiguration" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "message": "User is creating a case configuration as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteAllComments" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_delete_all", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to delete cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteAllComments" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_delete_all", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "message": "Failed attempt to delete a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteAllComments" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_delete_all", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User is deleting cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteAllComments" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_delete_all", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "message": "User is deleting a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_delete", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to delete cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_delete", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "message": "Failed attempt to delete a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_delete", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User is deleting cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_delete", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "message": "User is deleting a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteComment" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_delete", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to delete cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteComment" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_delete", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "message": "Failed attempt to delete a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteComment" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_delete", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User is deleting cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteComment" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_delete", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "message": "User is deleting a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findCases" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findCases" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findCases" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findCases" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findComments" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findComments" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findComments" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User has accessed cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findComments" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findConfigurations" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-configure", + }, + }, + "message": "Failed attempt to access cases-configure [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findConfigurations" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case configurations as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findConfigurations" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-configure", + }, + }, + "message": "User has accessed cases-configure [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findConfigurations" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case configurations as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAllComments" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get_all", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAllComments" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get_all", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAllComments" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get_all", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User has accessed cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAllComments" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get_all", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseIDsByAlertID" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_ids_by_alert_id_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseIDsByAlertID" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_ids_by_alert_id_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseIDsByAlertID" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_ids_by_alert_id_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User has accessed cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseIDsByAlertID" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_ids_by_alert_id_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseStatuses" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_find_statuses", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseStatuses" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_find_statuses", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseStatuses" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_find_statuses", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseStatuses" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_find_statuses", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getComment" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getComment" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getComment" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User has accessed cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getComment" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getReporters" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_reporters_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getReporters" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_reporters_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getReporters" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_reporters_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getReporters" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_reporters_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getTags" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_tags_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getTags" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_tags_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getTags" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_tags_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getTags" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_tags_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_user_actions_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-user-actions", + }, + }, + "message": "Failed attempt to access cases-user-actions [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_user_actions_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a user actions as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_user_actions_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-user-actions", + }, + }, + "message": "User has accessed cases-user-actions [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_user_actions_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a user actions as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "pushCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_push", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to update cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "pushCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_push", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "message": "Failed attempt to update a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "pushCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_push", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User is updating cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "pushCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_push", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "message": "User is updating a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to update cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "message": "Failed attempt to update a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User is updating cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "message": "User is updating a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateComment" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to update cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateComment" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "message": "Failed attempt to update a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateComment" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User is updating cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateComment" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "message": "User is updating a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateConfiguration" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-configure", + }, + }, + "message": "Failed attempt to update cases-configure [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateConfiguration" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "message": "Failed attempt to update a case configuration as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateConfiguration" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-configure", + }, + }, + "message": "User is updating cases-configure [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateConfiguration" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "message": "User is updating a case configuration as any owners", +} +`; diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.test.ts b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts new file mode 100644 index 0000000000000..d54b5164b10b9 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AuditLogger } from '../../../../plugins/security/server'; +import { Operations } from '.'; +import { AuthorizationAuditLogger } from './audit_logger'; +import { ReadOperations } from './types'; + +describe('audit_logger', () => { + it('creates a failure message without any owners', () => { + expect( + AuthorizationAuditLogger.createFailureMessage({ + owners: [], + operation: Operations.createCase, + }) + ).toBe('Unauthorized to create case of any owner'); + }); + + it('creates a failure message with owners', () => { + expect( + AuthorizationAuditLogger.createFailureMessage({ + owners: ['a', 'b'], + operation: Operations.createCase, + }) + ).toBe('Unauthorized to create case with owners: "a, b"'); + }); + + describe('log function', () => { + const mockLogger: jest.Mocked = { + log: jest.fn(), + }; + + let logger: AuthorizationAuditLogger; + + beforeEach(() => { + mockLogger.log.mockReset(); + logger = new AuthorizationAuditLogger(mockLogger); + }); + + it('does not throw an error when the underlying audit logger is undefined', () => { + const authLogger = new AuthorizationAuditLogger(); + jest.spyOn(authLogger, 'log'); + + expect(() => { + authLogger.log({ + operation: Operations.createCase, + entity: { + owner: 'a', + id: '1', + }, + }); + }).not.toThrow(); + + expect(authLogger.log).toHaveBeenCalledTimes(1); + }); + + it('logs a message with a saved object ID in the message field', () => { + logger.log({ + operation: Operations.createCase, + entity: { + owner: 'a', + id: '1', + }, + }); + expect(mockLogger.log.mock.calls[0][0]?.message).toContain('[id=1]'); + }); + + it('creates the owner part of the message when no owners are specified', () => { + logger.log({ + operation: Operations.createCase, + }); + + expect(mockLogger.log.mock.calls[0][0]?.message).toContain('as any owners'); + }); + + it('creates the owner part of the message when an owner is specified', () => { + logger.log({ + operation: Operations.createCase, + entity: { + owner: 'a', + id: '1', + }, + }); + + expect(mockLogger.log.mock.calls[0][0]?.message).toContain('as owner "a"'); + }); + + it('creates a failure message when passed an error', () => { + logger.log({ + operation: Operations.createCase, + entity: { + owner: 'a', + id: '1', + }, + error: new Error('error occurred'), + }); + + expect(mockLogger.log.mock.calls[0][0]?.message).toBe( + 'Failed attempt to create cases [id=1] as owner "a"' + ); + + expect(mockLogger.log.mock.calls[0][0]?.event?.outcome).toBe('failure'); + }); + + it('creates a write operation message', () => { + logger.log({ + operation: Operations.createCase, + entity: { + owner: 'a', + id: '1', + }, + }); + + expect(mockLogger.log.mock.calls[0][0]?.message).toBe( + 'User is creating cases [id=1] as owner "a"' + ); + + expect(mockLogger.log.mock.calls[0][0]?.event?.outcome).toBe('unknown'); + }); + + it('creates a read operation message', () => { + logger.log({ + operation: Operations.getCase, + entity: { + owner: 'a', + id: '1', + }, + }); + + expect(mockLogger.log.mock.calls[0][0]?.message).toBe( + 'User has accessed cases [id=1] as owner "a"' + ); + + expect(mockLogger.log.mock.calls[0][0]?.event?.outcome).toBe('success'); + }); + + describe('event structure', () => { + // I would have preferred to do these as match inline but that isn't supported because this is essentially a for loop + // for reference: https://github.com/facebook/jest/issues/9409#issuecomment-629272237 + + // This loops through all operation keys + it.each(Array.from(Object.keys(Operations)))( + `creates the correct audit event for operation: "%s" without an error or entity`, + (operationKey) => { + // forcing the cast here because using a string throws a type error + const key = operationKey as ReadOperations; + logger.log({ + operation: Operations[key], + }); + expect(mockLogger.log.mock.calls[0][0]).toMatchSnapshot(); + } + ); + + // This loops through all operation keys + it.each(Array.from(Object.keys(Operations)))( + `creates the correct audit event for operation: "%s" with an error but no entity`, + (operationKey) => { + // forcing the cast here because using a string throws a type error + const key = operationKey as ReadOperations; + logger.log({ + operation: Operations[key], + error: new Error('an error'), + }); + expect(mockLogger.log.mock.calls[0][0]).toMatchSnapshot(); + } + ); + + // This loops through all operation keys + it.each(Array.from(Object.keys(Operations)))( + `creates the correct audit event for operation: "%s" with an error and entity`, + (operationKey) => { + // forcing the cast here because using a string throws a type error + const key = operationKey as ReadOperations; + logger.log({ + operation: Operations[key], + entity: { + owner: 'awesome', + id: '1', + }, + error: new Error('an error'), + }); + expect(mockLogger.log.mock.calls[0][0]).toMatchSnapshot(); + } + ); + + // This loops through all operation keys + it.each(Array.from(Object.keys(Operations)))( + `creates the correct audit event for operation: "%s" without an error but with an entity`, + (operationKey) => { + // forcing the cast here because using a string throws a type error + const key = operationKey as ReadOperations; + logger.log({ + operation: Operations[key], + entity: { + owner: 'super', + id: '5', + }, + }); + expect(mockLogger.log.mock.calls[0][0]).toMatchSnapshot(); + } + ); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts index 82f9f6efdc11e..a59dfaaa4dabe 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts @@ -5,12 +5,15 @@ * 2.0. */ -import { DATABASE_CATEGORY, ECS_OUTCOMES, OperationDetails } from '.'; -import { AuditLogger } from '../../../security/server'; +import { EcsEventOutcome } from 'kibana/server'; +import { DATABASE_CATEGORY, ECS_OUTCOMES, isWriteOperation, OperationDetails } from '.'; +import { AuditEvent, AuditLogger } from '../../../security/server'; +import { OwnerEntity } from './types'; -enum AuthorizationResult { - Unauthorized = 'Unauthorized', - Authorized = 'Authorized', +interface CreateAuditMsgParams { + operation: OperationDetails; + entity?: OwnerEntity; + error?: Error; } /** @@ -19,106 +22,80 @@ enum AuthorizationResult { export class AuthorizationAuditLogger { private readonly auditLogger?: AuditLogger; - constructor(logger: AuditLogger | undefined) { + constructor(logger?: AuditLogger) { this.auditLogger = logger; } - private static createMessage({ - result, - owners, - operation, - }: { - result: AuthorizationResult; - owners?: string[]; - operation: OperationDetails; - }): string { - const ownerMsg = owners == null ? 'of any owner' : `with owners: "${owners.join(', ')}"`; - /** - * This will take the form: - * `Unauthorized to create case with owners: "securitySolution, observability"` - * `Unauthorized to find cases of any owner`. - */ - return `${result} to ${operation.verbs.present} ${operation.docType} ${ownerMsg}`; - } + /** + * Creates an AuditEvent describing the state of a request. + */ + private static createAuditMsg({ operation, error, entity }: CreateAuditMsgParams): AuditEvent { + const doc = + entity !== undefined + ? `${operation.savedObjectType} [id=${entity.id}]` + : `a ${operation.docType}`; - private logSuccessEvent({ - message, - operation, - username, - }: { - message: string; - operation: OperationDetails; - username?: string; - }) { - this.auditLogger?.log({ - message: `${username ?? 'unknown user'} ${message}`, + const ownerText = entity === undefined ? 'as any owners' : `as owner "${entity.owner}"`; + + let message: string; + let outcome: EcsEventOutcome; + + if (error) { + message = `Failed attempt to ${operation.verbs.present} ${doc} ${ownerText}`; + outcome = ECS_OUTCOMES.failure; + } else if (isWriteOperation(operation)) { + message = `User is ${operation.verbs.progressive} ${doc} ${ownerText}`; + outcome = ECS_OUTCOMES.unknown; + } else { + message = `User has ${operation.verbs.past} ${doc} ${ownerText}`; + outcome = ECS_OUTCOMES.success; + } + + return { + message, event: { action: operation.action, category: DATABASE_CATEGORY, - type: [operation.type], - outcome: ECS_OUTCOMES.success, + type: [operation.ecsType], + outcome, }, - ...(username != null && { - user: { - name: username, + ...(entity !== undefined && { + kibana: { + saved_object: { type: operation.savedObjectType, id: entity.id }, }, }), - }); + ...(error !== undefined && { + error: { + code: error.name, + message: error.message, + }, + }), + }; } /** - * Creates a audit message describing a failure to authorize + * Creates a message to be passed to an Error or Boom. */ - public failure({ - username, + public static createFailureMessage({ owners, operation, }: { - username?: string; - owners?: string[]; + owners: string[]; operation: OperationDetails; - }): string { - const message = AuthorizationAuditLogger.createMessage({ - result: AuthorizationResult.Unauthorized, - owners, - operation, - }); - this.auditLogger?.log({ - message: `${username ?? 'unknown user'} ${message}`, - event: { - action: operation.action, - category: DATABASE_CATEGORY, - type: [operation.type], - outcome: ECS_OUTCOMES.failure, - }, - // add the user information if we have it - ...(username != null && { - user: { - name: username, - }, - }), - }); - return message; + }) { + const ownerMsg = owners.length <= 0 ? 'of any owner' : `with owners: "${owners.join(', ')}"`; + /** + * This will take the form: + * `Unauthorized to create case with owners: "securitySolution, observability"` + * `Unauthorized to access cases of any owner` + */ + return `Unauthorized to ${operation.verbs.present} ${operation.docType} ${ownerMsg}`; } /** - * Creates a audit message describing a successful authorization + * Logs an audit event based on the status of an operation. */ - public success({ - username, - operation, - owners, - }: { - username?: string; - owners: string[]; - operation: OperationDetails; - }): string { - const message = AuthorizationAuditLogger.createMessage({ - result: AuthorizationResult.Authorized, - owners, - operation, - }); - this.logSuccessEvent({ message, operation, username }); - return message; + public log(auditMsgParams: CreateAuditMsgParams) { + this.auditLogger?.log(AuthorizationAuditLogger.createAuditMsg(auditMsgParams)); } } diff --git a/x-pack/plugins/cases/server/authorization/authorization.test.ts b/x-pack/plugins/cases/server/authorization/authorization.test.ts new file mode 100644 index 0000000000000..e602de565f294 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/authorization.test.ts @@ -0,0 +1,977 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { securityMock } from '../../../../plugins/security/server/mocks'; +import { httpServerMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { featuresPluginMock } from '../../../../plugins/features/server/mocks'; +import { Authorization, Operations } from '.'; +import { Space } from '../../../spaces/server'; +import { AuthorizationAuditLogger } from './audit_logger'; +import { KibanaRequest } from 'kibana/server'; +import { KibanaFeature } from '../../../../plugins/features/common'; +import { AuditLogger, SecurityPluginStart } from '../../../security/server'; +import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; + +describe('authorization', () => { + let request: KibanaRequest; + let mockLogger: jest.Mocked; + + beforeEach(() => { + request = httpServerMock.createKibanaRequest(); + mockLogger = { + log: jest.fn(), + }; + }); + + describe('create', () => { + let securityStart: jest.Mocked; + let featuresStart: jest.Mocked; + + beforeEach(() => { + securityStart = securityMock.createStart(); + featuresStart = featuresPluginMock.createStart(); + featuresStart.getKibanaFeatures.mockReturnValue(([ + { id: '1', cases: ['a'] }, + ] as unknown) as KibanaFeature[]); + }); + + it('creates an Authorization object', async () => { + expect.assertions(2); + + const getSpace = jest.fn(); + const authPromise = Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace, + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + await expect(authPromise).resolves.toBeDefined(); + await expect(authPromise).resolves.not.toThrow(); + }); + + it('throws and error when a failure occurs', async () => { + expect.assertions(1); + + const getSpace = jest.fn(async () => { + throw new Error('space error'); + }); + + const authPromise = Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace, + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + await expect(authPromise).rejects.toThrow(); + }); + }); + + describe('ensureAuthorized', () => { + const feature = { id: '1', cases: ['a'] }; + + let securityStart: ReturnType; + let featuresStart: jest.Mocked; + let auth: Authorization; + + beforeEach(async () => { + securityStart = securityMock.createStart(); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(true); + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ hasAllRequested: true })) + ); + + featuresStart = featuresPluginMock.createStart(); + featuresStart.getKibanaFeatures.mockReturnValue(([feature] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + }); + + it('throws an error when the owner passed in is not included in the features when security is disabled', async () => { + expect.assertions(1); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(false); + + try { + await auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'b' }], + operation: Operations.createCase, + }); + } catch (error) { + expect(error.message).toBe('Unauthorized to create case with owners: "b"'); + } + }); + + it('throws an error when the owner passed in is not included in the features when security undefined', async () => { + expect.assertions(1); + + auth = await Authorization.create({ + request, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + try { + await auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'b' }], + operation: Operations.createCase, + }); + } catch (error) { + expect(error.message).toBe('Unauthorized to create case with owners: "b"'); + } + }); + + it('throws an error when the owner passed in is not included in the features when security is enabled', async () => { + expect.assertions(1); + + try { + await auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'b' }], + operation: Operations.createCase, + }); + } catch (error) { + expect(error.message).toBe('Unauthorized to create case with owners: "b"'); + } + }); + + it('logs the error thrown when the passed in owner is not one of the features', async () => { + expect.assertions(2); + + try { + await auth.ensureAuthorized({ + entities: [ + { id: '1', owner: 'b' }, + { id: '5', owner: 'z' }, + ], + operation: Operations.createCase, + }); + } catch (error) { + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to create case with owners: \\"b, z\\"", + }, + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to create cases [id=1] as owner \\"b\\"", + }, + ], + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to create case with owners: \\"b, z\\"", + }, + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "Failed attempt to create cases [id=5] as owner \\"z\\"", + }, + ], + ] + `); + expect(error.message).toBe('Unauthorized to create case with owners: "b, z"'); + } + }); + + it('throws an error when the user does not have all the requested privileges', async () => { + expect.assertions(1); + + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ hasAllRequested: false })) + ); + + try { + await auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'a' }], + operation: Operations.createCase, + }); + } catch (error) { + expect(error.message).toBe('Unauthorized to create case with owners: "a"'); + } + }); + + it('throws an error when owner does not exist because it was from a disabled plugin', async () => { + expect.assertions(1); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(async () => ({ disabledFeatures: [feature.id] } as Space)), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + try { + await auth.ensureAuthorized({ + entities: [{ id: '100', owner: feature.cases[0] }], + operation: Operations.createCase, + }); + } catch (error) { + expect(error.message).toBe( + `Unauthorized to create case with owners: "${feature.cases[0]}"` + ); + } + }); + + it('does not throw an error when the user has the privileges needed', async () => { + expect.assertions(1); + + featuresStart.getKibanaFeatures.mockReturnValue(([ + feature, + { id: '2', cases: ['other-owner'] }, + ] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + await expect( + auth.ensureAuthorized({ + entities: [ + { id: '100', owner: feature.cases[0] }, + { id: '3', owner: 'other-owner' }, + ], + operation: Operations.createCase, + }) + ).resolves.not.toThrow(); + }); + + it('does not throw an error when the user has the privileges needed with a feature specifying multiple owners', async () => { + expect.assertions(1); + + featuresStart.getKibanaFeatures.mockReturnValue(([ + { id: '2', cases: ['a', 'other-owner'] }, + ] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + await expect( + auth.ensureAuthorized({ + entities: [ + { id: '100', owner: 'a' }, + { id: '3', owner: 'other-owner' }, + ], + operation: Operations.createCase, + }) + ).resolves.not.toThrow(); + }); + + it('logs a successful authorization when the user has the privileges needed with a feature specifying multiple owners', async () => { + expect.assertions(2); + + featuresStart.getKibanaFeatures.mockReturnValue(([ + { id: '2', cases: ['a', 'other-owner'] }, + ] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + + await expect( + auth.ensureAuthorized({ + entities: [ + { id: '100', owner: 'a' }, + { id: '3', owner: 'other-owner' }, + ], + operation: Operations.createCase, + }) + ).resolves.not.toThrow(); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "100", + "type": "cases", + }, + }, + "message": "User is creating cases [id=100] as owner \\"a\\"", + }, + ], + Array [ + Object { + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "3", + "type": "cases", + }, + }, + "message": "User is creating cases [id=3] as owner \\"other-owner\\"", + }, + ], + ] + `); + }); + }); + + describe('getAuthorizationFilter', () => { + const feature = { id: '1', cases: ['a', 'b'] }; + + let securityStart: ReturnType; + let featuresStart: jest.Mocked; + let auth: Authorization; + + beforeEach(async () => { + securityStart = securityMock.createStart(); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(true); + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ + hasAllRequested: true, + username: 'super', + privileges: { kibana: [] }, + })) + ); + + featuresStart = featuresPluginMock.createStart(); + featuresStart.getKibanaFeatures.mockReturnValue(([feature] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + }); + + it('throws and logs an error when there are no registered owners from plugins and security is enabled', async () => { + expect.assertions(2); + + featuresStart.getKibanaFeatures.mockReturnValue([]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + + try { + await auth.getAuthorizationFilter(Operations.findCases); + } catch (error) { + expect(error.message).toBe('Unauthorized to access cases of any owner'); + } + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to access cases of any owner", + }, + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a cases as any owners", + }, + ], + ] + `); + }); + + it('does not throw an error or log when a feature owner exists and security is disabled', async () => { + expect.assertions(3); + + auth = await Authorization.create({ + request, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + + const helpersPromise = auth.getAuthorizationFilter(Operations.findCases); + await expect(helpersPromise).resolves.not.toThrow(); + const helpers = await Promise.resolve(helpersPromise); + helpers.ensureSavedObjectsAreAuthorized([ + { id: '1', owner: 'blah' }, + { id: '2', owner: 'something-else' }, + ]); + + expect(helpers.filter).toBeUndefined(); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(`Array []`); + }); + + describe('hasAllRequested: true', () => { + it('logs and does not throw an error when passed the matching owners', async () => { + expect.assertions(3); + + const helpersPromise = auth.getAuthorizationFilter(Operations.findCases); + await expect(helpersPromise).resolves.not.toThrow(); + const helpers = await Promise.resolve(helpersPromise); + helpers.ensureSavedObjectsAreAuthorized([ + { id: '1', owner: 'a' }, + { id: '2', owner: 'b' }, + ]); + + expect(helpers.filter).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "or", + "type": "function", + } + `); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "2", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=2] as owner \\"b\\"", + }, + ], + ] + `); + }); + + it('logs and throws an error when passed an invalid owner', async () => { + expect.assertions(4); + + const helpersPromise = auth.getAuthorizationFilter(Operations.findCases); + await expect(helpersPromise).resolves.not.toThrow(); + const helpers = await Promise.resolve(helpersPromise); + try { + helpers.ensureSavedObjectsAreAuthorized([ + { id: '1', owner: 'a' }, + // c is an invalid owner, because it was not registered by a feature + { id: '2', owner: 'c' }, + ]); + } catch (error) { + expect(error.message).toBe('Unauthorized to access cases with owners: "c"'); + } + + expect(helpers.filter).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "or", + "type": "function", + } + `); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to access cases with owners: \\"c\\"", + }, + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "2", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=2] as owner \\"c\\"", + }, + ], + ] + `); + }); + }); + + describe('hasAllRequested: false', () => { + beforeEach(async () => { + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ + hasAllRequested: false, + username: 'super', + privileges: { + kibana: [ + { + authorized: true, + privilege: 'a:getCase', + }, + { + authorized: true, + privilege: 'b:getCase', + }, + { + authorized: false, + privilege: 'c:getCase', + }, + ], + }, + })) + ); + + (securityStart.authz.actions.cases.get as jest.MockedFunction< + typeof securityStart.authz.actions.cases.get + >).mockImplementation((owner, opName) => { + return `${owner}:${opName}`; + }); + + featuresStart.getKibanaFeatures.mockReturnValue(([ + { id: 'a', cases: ['a', 'b', 'c'] }, + ] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + }); + + it('logs and does not throw an error when passed the matching owners', async () => { + expect.assertions(3); + + const helpersPromise = auth.getAuthorizationFilter(Operations.findCases); + await expect(helpersPromise).resolves.not.toThrow(); + const helpers = await Promise.resolve(helpersPromise); + helpers.ensureSavedObjectsAreAuthorized([ + { id: '1', owner: 'a' }, + { id: '2', owner: 'b' }, + ]); + + expect(helpers.filter).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "or", + "type": "function", + } + `); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "2", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=2] as owner \\"b\\"", + }, + ], + ] + `); + }); + + it('logs and throws an error when passed an invalid owner', async () => { + expect.assertions(4); + + const helpersPromise = auth.getAuthorizationFilter(Operations.findCases); + await expect(helpersPromise).resolves.not.toThrow(); + const helpers = await Promise.resolve(helpersPromise); + try { + helpers.ensureSavedObjectsAreAuthorized([ + { id: '1', owner: 'a' }, + // c is an invalid owner, because it was not registered by a feature + { id: '2', owner: 'c' }, + ]); + } catch (error) { + expect(error.message).toBe('Unauthorized to access cases with owners: "c"'); + } + + expect(helpers.filter).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "or", + "type": "function", + } + `); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to access cases with owners: \\"c\\"", + }, + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "2", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=2] as owner \\"c\\"", + }, + ], + ] + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 296a125418023..a363874857d56 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -9,10 +9,11 @@ import { KibanaRequest, Logger } from 'kibana/server'; import Boom from '@hapi/boom'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; -import { AuthorizationFilter, GetSpaceFn } from './types'; +import { AuthFilterHelpers, GetSpaceFn } from './types'; import { getOwnersFilter } from './utils'; import { AuthorizationAuditLogger, OperationDetails } from '.'; import { createCaseError } from '../common'; +import { OwnerEntity } from './types'; /** * This class handles ensuring that the user making a request has the correct permissions @@ -90,10 +91,49 @@ export class Authorization { * Checks that the user making the request for the passed in owners and operation has the correct authorization. This * function will throw if the user is not authorized for the requested operation and owners. * - * @param owners an array of strings describing the case owners attempting to be authorized + * @param entities an array of entities describing the case owners in conjunction with the saved object ID attempting + * to be authorized * @param operation information describing the operation attempting to be authorized */ - public async ensureAuthorized(owners: string[], operation: OperationDetails) { + public async ensureAuthorized({ + entities, + operation, + }: { + entities: OwnerEntity[]; + operation: OperationDetails; + }) { + const logSavedObjects = (error?: Error) => { + for (const entity of entities) { + this.auditLogger.log({ operation, error, entity }); + } + }; + + try { + await this._ensureAuthorized( + entities.map((entity) => entity.owner), + operation + ); + } catch (error) { + logSavedObjects(error); + throw error; + } + + logSavedObjects(); + } + + /** + * Returns an object to filter the saved object find request to the authorized owners of an entity. + */ + public async getAuthorizationFilter(operation: OperationDetails): Promise { + try { + return await this._getAuthorizationFilter(operation); + } catch (error) { + this.auditLogger.log({ error, operation }); + throw error; + } + } + + private async _ensureAuthorized(owners: string[], operation: OperationDetails) { const { securityAuth } = this; const areAllOwnersAvailable = owners.every((owner) => this.featureCaseOwners.has(owner)); @@ -103,7 +143,7 @@ export class Authorization { ); const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username } = await checkPrivileges({ + const { hasAllRequested } = await checkPrivileges({ kibana: requiredPrivileges, }); @@ -115,55 +155,53 @@ export class Authorization { * as Privileged. * This check will ensure we don't accidentally let these through */ - throw Boom.forbidden(this.auditLogger.failure({ username, owners, operation })); + throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation })); } - if (hasAllRequested) { - this.auditLogger.success({ username, operation, owners }); - } else { - throw Boom.forbidden(this.auditLogger.failure({ owners, operation, username })); + if (!hasAllRequested) { + throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation })); } } else if (!areAllOwnersAvailable) { - throw Boom.forbidden(this.auditLogger.failure({ owners, operation })); + throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation })); } // else security is disabled so let the operation proceed } - /** - * Returns an object to filter the saved object find request to the authorized owners of an entity. - */ - public async getFindAuthorizationFilter( - operation: OperationDetails - ): Promise { + private async _getAuthorizationFilter(operation: OperationDetails): Promise { const { securityAuth } = this; if (securityAuth && this.shouldCheckAuthorization()) { - const { username, authorizedOwners } = await this.getAuthorizedOwners([operation]); + const { authorizedOwners } = await this.getAuthorizedOwners([operation]); if (!authorizedOwners.length) { - throw Boom.forbidden(this.auditLogger.failure({ username, operation })); + throw Boom.forbidden( + AuthorizationAuditLogger.createFailureMessage({ owners: authorizedOwners, operation }) + ); } return { filter: getOwnersFilter(operation.savedObjectType, authorizedOwners), - ensureSavedObjectIsAuthorized: (owner: string) => { - if (!authorizedOwners.includes(owner)) { - throw Boom.forbidden( - this.auditLogger.failure({ username, operation, owners: [owner] }) - ); - } - }, - logSuccessfulAuthorization: () => { - if (authorizedOwners.length) { - this.auditLogger.success({ username, owners: authorizedOwners, operation }); + ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => { + for (const entity of entities) { + if (!authorizedOwners.includes(entity.owner)) { + const error = Boom.forbidden( + AuthorizationAuditLogger.createFailureMessage({ + operation, + owners: [entity.owner], + }) + ); + this.auditLogger.log({ error, operation, entity }); + throw error; + } + + this.auditLogger.log({ operation, entity }); } }, }; } return { - ensureSavedObjectIsAuthorized: (owner: string) => {}, - logSuccessfulAuthorization: () => {}, + ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => {}, }; } diff --git a/x-pack/plugins/cases/server/authorization/index.test.ts b/x-pack/plugins/cases/server/authorization/index.test.ts new file mode 100644 index 0000000000000..ef2a5eed09eaa --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/index.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isWriteOperation, Operations } from '.'; +import { OperationDetails } from './types'; + +describe('index tests', () => { + it('should identify a write operation', () => { + expect(isWriteOperation(Operations.createCase)).toBeTruthy(); + }); + + it('should return false when the operation is not a write operation', () => { + expect(isWriteOperation(Operations.getCase)).toBeFalsy(); + }); + + it('should not identify an invalid operation as a write operation', () => { + expect(isWriteOperation({ name: 'blah' } as OperationDetails)).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 1356111ff1664..9a8b44a4a4f5d 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -73,13 +73,23 @@ export const ECS_OUTCOMES: Record = { unknown: 'unknown', }; +/** + * Determines if the passed in operation was a write operation. + * + * @param operation an OperationDetails object describing the operation that occurred + * @returns true if the passed in operation was a write operation + */ +export function isWriteOperation(operation: OperationDetails): boolean { + return Object.values(WriteOperations).includes(operation.name as WriteOperations); +} + /** * Definition of all APIs within the cases backend. */ export const Operations: Record = { // case operations [WriteOperations.CreateCase]: { - type: EVENT_TYPES.creation, + ecsType: EVENT_TYPES.creation, name: WriteOperations.CreateCase, action: 'case_create', verbs: createVerbs, @@ -87,7 +97,7 @@ export const Operations: Record; export const createAuthorizationMock = () => { const mocked: AuthorizationMock = { ensureAuthorized: jest.fn(), - getFindAuthorizationFilter: jest.fn(), + getAuthorizationFilter: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/cases/server/authorization/types.ts b/x-pack/plugins/cases/server/authorization/types.ts index 8d0ec93b33b03..4651d45ab3b5f 100644 --- a/x-pack/plugins/cases/server/authorization/types.ts +++ b/x-pack/plugins/cases/server/authorization/types.ts @@ -66,14 +66,14 @@ export interface OperationDetails { /** * The ECS event type that this operation should be audit logged as (creation, deletion, access, etc) */ - type: EcsEventType; + ecsType: EcsEventType; /** * The name of the operation to authorize against for the privilege check. * These values need to match one of the operation strings defined here: x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts */ name: string; /** - * The ECS `event.action` field, should be in the form of - e.g get-comment, find-cases + * The ECS `event.action` field, should be in the form of _ e.g comment_get, case_fined */ action: string; /** @@ -90,10 +90,24 @@ export interface OperationDetails { savedObjectType: string; } +/** + * Describes an entity with the necessary fields to identify if the user is authorized to interact with the saved object + * returned from some find query. + */ +export interface OwnerEntity { + owner: string; + id: string; +} + +/** + * Function callback for making sure the found saved objects are of the authorized owner + */ +export type EnsureSOAuthCallback = (entities: OwnerEntity[]) => void; + /** * Defines the helper methods and necessary information for authorizing the find API's request. */ -export interface AuthorizationFilter { +export interface AuthFilterHelpers { /** * The owner filter to pass to the saved object client's find operation that is scoped to the authorized owners */ @@ -101,9 +115,5 @@ export interface AuthorizationFilter { /** * Utility function for checking that the returned entities are in fact authorized for the user making the request */ - ensureSavedObjectIsAuthorized: (owner: string) => void; - /** - * Logs a successful audit message for the request - */ - logSuccessfulAuthorization: () => void; + ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; } diff --git a/x-pack/plugins/cases/server/authorization/utils.test.ts b/x-pack/plugins/cases/server/authorization/utils.test.ts new file mode 100644 index 0000000000000..3ebf6ee398e38 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/utils.test.ts @@ -0,0 +1,297 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { nodeBuilder } from '../../../../../src/plugins/data/common'; +import { OWNER_FIELD } from '../../common'; +import { + combineFilterWithAuthorizationFilter, + ensureFieldIsSafeForQuery, + getOwnersFilter, + includeFieldsRequiredForAuthentication, +} from './utils'; + +describe('utils', () => { + describe('combineFilterWithAuthorizationFilter', () => { + it('returns undefined if neither a filter or authorizationFilter are passed', () => { + expect(combineFilterWithAuthorizationFilter()).toBeUndefined(); + }); + + it('returns a single KueryNode when only a filter is passed in', () => { + const node = nodeBuilder.is('a', 'hello'); + expect(combineFilterWithAuthorizationFilter(node)).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + + it('returns a single KueryNode when only an authorizationFilter is passed in', () => { + const node = nodeBuilder.is('a', 'hello'); + expect(combineFilterWithAuthorizationFilter(undefined, node)).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + + it("returns a single KueryNode and'ing together the passed in parameters", () => { + const node = nodeBuilder.is('a', 'hello'); + const node2 = nodeBuilder.is('b', 'hi'); + + expect(combineFilterWithAuthorizationFilter(node, node2)).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": "hi", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + } + `); + }); + + it("returns a single KueryNode and'ing together the passed in parameters in opposite order", () => { + const node = nodeBuilder.is('a', 'hello'); + const node2 = nodeBuilder.is('b', 'hi'); + + expect(combineFilterWithAuthorizationFilter(node2, node)).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": "hi", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + } + `); + }); + }); + + describe('includeFieldsRequiredForAuthentication', () => { + it('returns undefined when the fields parameter is not specified', () => { + expect(includeFieldsRequiredForAuthentication()).toBeUndefined(); + }); + + it('returns an array with a single entry containing the owner field', () => { + expect(includeFieldsRequiredForAuthentication([])).toStrictEqual([OWNER_FIELD]); + }); + + it('returns an array without duplicates and including the owner field', () => { + expect(includeFieldsRequiredForAuthentication(['a', 'b', 'a'])).toStrictEqual([ + 'a', + 'b', + OWNER_FIELD, + ]); + }); + }); + + describe('ensureFieldIsSafeForQuery', () => { + it("throws an error if field contains character that aren't safe in a KQL query", () => { + expect(() => ensureFieldIsSafeForQuery('id', 'cases-*')).toThrowError( + `expected id not to include invalid character: *` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '<=""')).toThrowError( + `expected id not to include invalid character: <=` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '>=""')).toThrowError( + `expected id not to include invalid character: >=` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '1 or caseid:123')).toThrowError( + `expected id not to include whitespace and invalid character: :` + ); + + expect(() => ensureFieldIsSafeForQuery('id', ') or caseid:123')).toThrowError( + `expected id not to include whitespace and invalid characters: ), :` + ); + + expect(() => ensureFieldIsSafeForQuery('id', 'some space')).toThrowError( + `expected id not to include whitespace` + ); + }); + + it("doesn't throw an error if field is safe as part of a KQL query", () => { + expect(() => ensureFieldIsSafeForQuery('id', '123-0456-678')).not.toThrow(); + }); + }); + + describe('getOwnersFilter', () => { + it('returns undefined when the owners parameter is an empty array', () => { + expect(getOwnersFilter('a', [])).toBeUndefined(); + }); + + it('constructs a KueryNode with only a single node', () => { + expect(getOwnersFilter('a', ['hello'])).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a.attributes.owner", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + + it("constructs a KueryNode or'ing together two filters", () => { + expect(getOwnersFilter('a', ['hello', 'hi'])).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a.attributes.owner", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a.attributes.owner", + }, + Object { + "type": "literal", + "value": "hi", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "or", + "type": "function", + } + `); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index eb2dcc1a0f2e4..19dc37d0c3fdf 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -9,7 +9,14 @@ import { remove, uniq } from 'lodash'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; import { OWNER_FIELD } from '../../common/api'; -export const getOwnersFilter = (savedObjectType: string, owners: string[]): KueryNode => { +export const getOwnersFilter = ( + savedObjectType: string, + owners: string[] +): KueryNode | undefined => { + if (owners.length <= 0) { + return; + } + return nodeBuilder.or( owners.reduce((query, owner) => { ensureFieldIsSafeForQuery(OWNER_FIELD, owner); diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index b453e1feb5d63..9008e0fc28dee 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -49,7 +49,7 @@ import { CASE_COMMENT_SAVED_OBJECT, } from '../../../common'; -import { decodeCommentRequest, ensureAuthorized } from '../utils'; +import { decodeCommentRequest } from '../utils'; import { Operations } from '../../authorization'; async function getSubCase({ @@ -126,7 +126,6 @@ const addGeneratedAlerts = async ( caseService, userActionService, logger, - auditLogger, authorization, } = clientArgs; @@ -146,11 +145,8 @@ const addGeneratedAlerts = async ( const createdDate = new Date().toISOString(); const savedObjectID = SavedObjectsUtils.generateId(); - await ensureAuthorized({ - authorization, - auditLogger, - owners: [comment.owner], - savedObjectIDs: [savedObjectID], + await authorization.ensureAuthorized({ + entities: [{ owner: comment.owner, id: savedObjectID }], operation: Operations.createComment, }); @@ -339,7 +335,6 @@ export const addComment = async ( user, logger, authorization, - auditLogger, } = clientArgs; if (isCommentRequestTypeGenAlert(comment)) { @@ -356,12 +351,9 @@ export const addComment = async ( try { const savedObjectID = SavedObjectsUtils.generateId(); - await ensureAuthorized({ - authorization, - auditLogger, + await authorization.ensureAuthorized({ operation: Operations.createComment, - owners: [comment.owner], - savedObjectIDs: [savedObjectID], + entities: [{ owner: comment.owner, id: savedObjectID }], }); const createdDate = new Date().toISOString(); diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index d9a2b00ec50ae..d935a0c8f09db 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -13,7 +13,6 @@ import { CasesClientArgs } from '../types'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; import { createCaseError } from '../../common/error'; import { checkEnabledCaseConnectorOrThrow } from '../../common'; -import { ensureAuthorized } from '../utils'; import { Operations } from '../../authorization'; /** @@ -65,7 +64,6 @@ export async function deleteAll( userActionService, logger, authorization, - auditLogger, } = clientArgs; try { @@ -82,12 +80,12 @@ export async function deleteAll( throw Boom.notFound(`No comments found for ${id}.`); } - await ensureAuthorized({ - authorization, - auditLogger, + await authorization.ensureAuthorized({ operation: Operations.deleteAllComments, - savedObjectIDs: comments.saved_objects.map((comment) => comment.id), - owners: comments.saved_objects.map((comment) => comment.attributes.owner), + entities: comments.saved_objects.map((comment) => ({ + owner: comment.attributes.owner, + id: comment.id, + })), }); await Promise.all( @@ -141,7 +139,6 @@ export async function deleteComment( userActionService, logger, authorization, - auditLogger, } = clientArgs; try { @@ -158,11 +155,8 @@ export async function deleteComment( throw Boom.notFound(`This comment ${attachmentID} does not exist anymore.`); } - await ensureAuthorized({ - authorization, - auditLogger, - owners: [myComment.attributes.owner], - savedObjectIDs: [myComment.id], + await authorization.ensureAuthorized({ + entities: [{ owner: myComment.attributes.owner, id: myComment.id }], operation: Operations.deleteComment, }); diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index 9d85a90324a6c..e15bdcc7c8c2b 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -29,12 +29,7 @@ import { import { createCaseError } from '../../common/error'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { CasesClientArgs } from '../types'; -import { - combineFilters, - ensureAuthorized, - getAuthorizationFilter, - stringToKueryNode, -} from '../utils'; +import { combineFilters, stringToKueryNode } from '../utils'; import { Operations } from '../../authorization'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; @@ -90,13 +85,7 @@ export async function find( { caseID, queryParams }: FindArgs, clientArgs: CasesClientArgs ): Promise { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { checkEnabledCaseConnectorOrThrow(queryParams?.subCaseId); @@ -104,12 +93,7 @@ export async function find( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, - auditLogger, - operation: Operations.findComments, - }); + } = await authorization.getAuthorizationFilter(Operations.findComments); const id = queryParams?.subCaseId ?? caseID; const associationType = queryParams?.subCaseId ? AssociationType.subCase : AssociationType.case; @@ -161,8 +145,6 @@ export async function find( })) ); - logSuccessfulAuthorization(); - return CommentsResponseRt.encode(transformComments(theComments)); } catch (error) { throw createCaseError({ @@ -182,13 +164,7 @@ export async function get( { attachmentID, caseID }: GetArgs, clientArgs: CasesClientArgs ): Promise { - const { - attachmentService, - unsecuredSavedObjectsClient, - logger, - authorization, - auditLogger, - } = clientArgs; + const { attachmentService, unsecuredSavedObjectsClient, logger, authorization } = clientArgs; try { const comment = await attachmentService.get({ @@ -196,11 +172,8 @@ export async function get( attachmentId: attachmentID, }); - await ensureAuthorized({ - authorization, - auditLogger, - owners: [comment.attributes.owner], - savedObjectIDs: [comment.id], + await authorization.ensureAuthorized({ + entities: [{ owner: comment.attributes.owner, id: comment.id }], operation: Operations.getComment, }); @@ -224,13 +197,7 @@ export async function getAll( { caseID, includeSubCaseComments, subCaseID }: GetAllArgs, clientArgs: CasesClientArgs ): Promise { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { let comments: SavedObjectsFindResponse; @@ -244,15 +211,9 @@ export async function getAll( ); } - const { - filter, - ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, - auditLogger, - operation: Operations.getAllComments, - }); + const { filter, ensureSavedObjectsAreAuthorized } = await authorization.getAuthorizationFilter( + Operations.getAllComments + ); if (subCaseID) { comments = await caseService.getAllSubCaseComments({ @@ -279,8 +240,6 @@ export async function getAll( comments.saved_objects.map((comment) => ({ id: comment.id, owner: comment.attributes.owner })) ); - logSuccessfulAuthorization(); - return AllCommentsResponseRt.encode(flattenCommentSavedObjects(comments.saved_objects)); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 3310f9e8f6aa6..c0566ff646814 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -15,7 +15,7 @@ import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/consta import { AttachmentService, CasesService } from '../../services'; import { CaseResponse, CommentPatchRequest } from '../../../common/api'; import { CasesClientArgs } from '..'; -import { decodeCommentRequest, ensureAuthorized } from '../utils'; +import { decodeCommentRequest } from '../utils'; import { createCaseError } from '../../common/error'; import { Operations } from '../../authorization'; @@ -105,7 +105,6 @@ export async function update( user, userActionService, authorization, - auditLogger, } = clientArgs; try { @@ -137,12 +136,9 @@ export async function update( throw Boom.notFound(`This comment ${queryCommentId} does not exist anymore.`); } - await ensureAuthorized({ - authorization, - auditLogger, + await authorization.ensureAuthorized({ + entities: [{ owner: myComment.attributes.owner, id: myComment.id }], operation: Operations.updateComment, - savedObjectIDs: [myComment.id], - owners: [myComment.attributes.owner], }); if (myComment.attributes.type !== queryRestAttributes.type) { diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index e1edcfdda0423..879edd5eb1b5c 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -23,7 +23,7 @@ import { OWNER_FIELD, } from '../../../common'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { ensureAuthorized, getConnectorFromConfiguration } from '../utils'; +import { getConnectorFromConfiguration } from '../utils'; import { createCaseError } from '../../common/error'; import { Operations } from '../../authorization'; @@ -52,7 +52,6 @@ export const create = async ( user, logger, authorization: auth, - auditLogger, } = clientArgs; // default to an individual case if the type is not defined. @@ -76,12 +75,9 @@ export const create = async ( try { const savedObjectID = SavedObjectsUtils.generateId(); - await ensureAuthorized({ + await auth.ensureAuthorized({ operation: Operations.createCase, - owners: [query.owner], - authorization: auth, - auditLogger, - savedObjectIDs: [savedObjectID], + entities: [{ owner: query.owner, id: savedObjectID }], }); // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 8ad48bde7f971..b66abc6cc7be4 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -12,8 +12,7 @@ import { CasesClientArgs } from '..'; import { createCaseError } from '../../common/error'; import { AttachmentService, CasesService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { Operations } from '../../authorization'; -import { ensureAuthorized } from '../utils'; +import { Operations, OwnerEntity } from '../../authorization'; import { OWNER_FIELD } from '../../../common/api'; async function deleteSubCases({ @@ -66,13 +65,11 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P user, userActionService, logger, - authorization: auth, - auditLogger, + authorization, } = clientArgs; try { const cases = await caseService.getCases({ unsecuredSavedObjectsClient, caseIds: ids }); - const soIds = new Set(); - const owners = new Set(); + const entities = new Map(); for (const theCase of cases.saved_objects) { // bulkGet can return an error. @@ -83,17 +80,12 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P logger, }); } - - soIds.add(theCase.id); - owners.add(theCase.attributes.owner); + entities.set(theCase.id, { id: theCase.id, owner: theCase.attributes.owner }); } - await ensureAuthorized({ + await authorization.ensureAuthorized({ operation: Operations.deleteCase, - owners: [...owners.values()], - authorization: auth, - auditLogger, - savedObjectIDs: [...soIds.values()], + entities: Array.from(entities.values()), }); await Promise.all( diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 633261100ddea..3b4efe78f642b 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -21,7 +21,7 @@ import { } from '../../../common/api'; import { createCaseError } from '../../common/error'; -import { constructQueryOptions, getAuthorizationFilter } from '../utils'; +import { constructQueryOptions } from '../utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { Operations } from '../../authorization'; import { transformCases } from '../../common'; @@ -36,13 +36,7 @@ export const find = async ( params: CasesFindRequest, clientArgs: CasesClientArgs ): Promise => { - const { - unsecuredSavedObjectsClient, - caseService, - authorization: auth, - auditLogger, - logger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, authorization, logger } = clientArgs; try { const queryParams = pipe( @@ -53,12 +47,7 @@ export const find = async ( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization: auth, - operation: Operations.findCases, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.findCases); const queryArgs = { tags: queryParams.tags, @@ -100,8 +89,6 @@ export const find = async ( }), ]); - logSuccessfulAuthorization(); - return CasesFindResponseRt.encode( transformCases({ casesMap: cases.casesMap, diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index cf6d12ceae0a0..7a8100ad60ff3 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -30,11 +30,7 @@ import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CasesClientArgs } from '..'; import { Operations } from '../../authorization'; -import { - combineAuthorizedAndOwnerFilter, - ensureAuthorized, - getAuthorizationFilter, -} from '../utils'; +import { combineAuthorizedAndOwnerFilter } from '../utils'; import { CasesService } from '../../services'; /** @@ -61,13 +57,7 @@ export const getCaseIDsByAlertID = async ( { alertID, options }: CaseIDsByAlertIDParams, clientArgs: CasesClientArgs ): Promise => { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { const queryParams = pipe( @@ -78,12 +68,7 @@ export const getCaseIDsByAlertID = async ( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, - operation: Operations.getCaseIDsByAlertID, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.getCaseIDsByAlertID); const filter = combineAuthorizedAndOwnerFilter( queryParams.owner, @@ -104,8 +89,6 @@ export const getCaseIDsByAlertID = async ( })) ); - logSuccessfulAuthorization(); - return CasesService.getCaseIDsFromAlertAggs(commentsWithAlert); } catch (error) { throw createCaseError({ @@ -145,13 +128,7 @@ export const get = async ( { id, includeComments, includeSubCaseComments }: GetParams, clientArgs: CasesClientArgs ): Promise => { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization: auth, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) { @@ -184,12 +161,9 @@ export const get = async ( }); } - await ensureAuthorized({ + await authorization.ensureAuthorized({ operation: Operations.getCase, - owners: [theCase.attributes.owner], - authorization: auth, - auditLogger, - savedObjectIDs: [theCase.id], + entities: [{ owner: theCase.attributes.owner, id: theCase.id }], }); if (!includeComments) { @@ -233,13 +207,7 @@ export async function getTags( params: AllTagsFindRequest, clientArgs: CasesClientArgs ): Promise { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization: auth, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { const queryParams = pipe( @@ -250,12 +218,7 @@ export async function getTags( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization: auth, - operation: Operations.findCases, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.findCases); const filter = combineAuthorizedAndOwnerFilter(queryParams.owner, authorizationFilter); @@ -280,7 +243,6 @@ export async function getTags( }); ensureSavedObjectsAreAuthorized(mappedCases); - logSuccessfulAuthorization(); return [...tags.values()]; } catch (error) { @@ -295,13 +257,7 @@ export async function getReporters( params: AllReportersFindRequest, clientArgs: CasesClientArgs ): Promise { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization: auth, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { const queryParams = pipe( @@ -312,12 +268,7 @@ export async function getReporters( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization: auth, - operation: Operations.getReporters, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.getReporters); const filter = combineAuthorizedAndOwnerFilter(queryParams.owner, authorizationFilter); @@ -346,7 +297,6 @@ export async function getReporters( }); ensureSavedObjectsAreAuthorized(mappedCases); - logSuccessfulAuthorization(); return UsersRt.encode([...reporters.values()]); } catch (error) { diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 74d3fb1373fd7..dd527122d0616 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -24,7 +24,6 @@ import { createIncident, getCommentContextFromAttributes } from './utils'; import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; -import { ensureAuthorized } from '../utils'; import { Operations } from '../../authorization'; /** @@ -77,7 +76,6 @@ export const push = async ( actionsClient, user, logger, - auditLogger, authorization, } = clientArgs; @@ -93,12 +91,9 @@ export const push = async ( casesClient.userActions.getAll({ caseId }), ]); - await ensureAuthorized({ - authorization, - auditLogger, + await authorization.ensureAuthorized({ + entities: [{ owner: theCase.owner, id: caseId }], operation: Operations.pushCase, - savedObjectIDs: [caseId], - owners: [theCase.owner], }); // We need to change the logic when we support subcases diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 1dabca40146f8..db20ba8318447 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -36,7 +36,7 @@ import { CommentAttributes, } from '../../../common/api'; import { buildCaseUserActions } from '../../services/user_actions/helpers'; -import { ensureAuthorized, getCaseToUpdate } from '../utils'; +import { getCaseToUpdate } from '../utils'; import { CasesService } from '../../services'; import { @@ -55,8 +55,7 @@ import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { UpdateAlertRequest } from '../alerts/client'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '..'; -import { Operations } from '../../authorization'; -import { OwnerEntity } from '../types'; +import { Operations, OwnerEntity } from '../../authorization'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -406,7 +405,6 @@ export const update = async ( user, logger, authorization, - auditLogger, } = clientArgs; const query = pipe( excess(CasesPatchRequestRt).decode(cases), @@ -429,12 +427,9 @@ export const update = async ( query.cases ); - await ensureAuthorized({ - authorization, - auditLogger, - owners: casesToAuthorize.map((caseInfo) => caseInfo.owner), + await authorization.ensureAuthorized({ + entities: casesToAuthorize, operation: Operations.updateCase, - savedObjectIDs: casesToAuthorize.map((caseInfo) => caseInfo.id), }); if (nonExistingCases.length > 0) { diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index e0bf8c7d82308..14348e03f99cc 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -41,11 +41,7 @@ import { getMappings } from './get_mappings'; import { FindActionResult } from '../../../../actions/server/types'; import { ActionType } from '../../../../actions/common'; import { Operations } from '../../authorization'; -import { - combineAuthorizedAndOwnerFilter, - ensureAuthorized, - getAuthorizationFilter, -} from '../utils'; +import { combineAuthorizedAndOwnerFilter } from '../utils'; import { ConfigurationGetFields, MappingsArgs, @@ -148,13 +144,7 @@ async function get( clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise { - const { - unsecuredSavedObjectsClient, - caseConfigureService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseConfigureService, logger, authorization } = clientArgs; try { const queryParams = pipe( excess(GetConfigureFindRequestRt).decode(params), @@ -164,12 +154,7 @@ async function get( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, - operation: Operations.findConfigurations, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.findConfigurations); const filter = combineAuthorizedAndOwnerFilter( queryParams.owner, @@ -190,8 +175,6 @@ async function get( })) ); - logSuccessfulAuthorization(); - const configurations = await Promise.all( myCaseConfigure.saved_objects.map(async (configuration) => { const { connector, ...caseConfigureWithoutConnector } = configuration?.attributes ?? { @@ -267,7 +250,6 @@ async function update( unsecuredSavedObjectsClient, user, authorization, - auditLogger, } = clientArgs; try { @@ -295,12 +277,9 @@ async function update( configurationId, }); - await ensureAuthorized({ + await authorization.ensureAuthorized({ operation: Operations.updateConfiguration, - owners: [configuration.attributes.owner], - authorization, - auditLogger, - savedObjectIDs: [configuration.id], + entities: [{ owner: configuration.attributes.owner, id: configuration.id }], }); if (version !== configuration.version) { @@ -386,7 +365,6 @@ async function create( logger, user, authorization, - auditLogger, } = clientArgs; try { let error = null; @@ -394,17 +372,14 @@ async function create( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, + } = await authorization.getAuthorizationFilter( /** * The operation is createConfiguration because the procedure is part of * the create route. The user should have all * permissions to delete the results. */ - operation: Operations.createConfiguration, - auditLogger, - }); + Operations.createConfiguration + ); const filter = combineAuthorizedAndOwnerFilter( configuration.owner, @@ -424,8 +399,6 @@ async function create( })) ); - logSuccessfulAuthorization(); - if (myCaseConfigure.saved_objects.length > 0) { await Promise.all( myCaseConfigure.saved_objects.map((cc) => @@ -436,12 +409,9 @@ async function create( const savedObjectID = SavedObjectsUtils.generateId(); - await ensureAuthorized({ + await authorization.ensureAuthorized({ operation: Operations.createConfiguration, - owners: [configuration.owner], - authorization, - auditLogger, - savedObjectIDs: [savedObjectID], + entities: [{ owner: configuration.owner, id: savedObjectID }], }); const creationDate = new Date().toISOString(); diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 7110e7e9e1d92..4644efb61916f 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -109,7 +109,6 @@ export class CasesClientFactory { attachmentService: new AttachmentService(this.logger), logger: this.logger, authorization: auth, - auditLogger, actionsClient: await this.options.actionsPluginStart.getActionsClientWithRequest(request), }); } diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index 9816bfe1fd7cf..0e222d54ab218 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -22,7 +22,7 @@ import { } from '../../../common/api'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; -import { constructQueryOptions, getAuthorizationFilter } from '../utils'; +import { constructQueryOptions } from '../utils'; /** * Statistics API contract. @@ -50,13 +50,7 @@ async function getStatusTotalsByType( params: CasesStatusRequest, clientArgs: CasesClientArgs ): Promise { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { const queryParams = pipe( @@ -67,12 +61,7 @@ async function getStatusTotalsByType( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, - operation: Operations.getCaseStatuses, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.getCaseStatuses); const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { @@ -90,8 +79,6 @@ async function getStatusTotalsByType( }), ]); - logSuccessfulAuthorization(); - return CasesStatusResponseRt.encode({ count_open_cases: openCases, count_in_progress_cases: inProgressCases, diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 7d1c0855061c2..f6b229b94800d 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -8,7 +8,6 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { User } from '../../common/api'; -import { AuditLogger } from '../../../security/server'; import { Authorization } from '../authorization/authorization'; import { AlertServiceContract, @@ -35,15 +34,5 @@ export interface CasesClientArgs { readonly attachmentService: AttachmentService; readonly logger: Logger; readonly authorization: PublicMethodsOf; - readonly auditLogger?: AuditLogger; readonly actionsClient: PublicMethodsOf; } - -/** - * Describes an entity with the necessary fields to identify if the user is authorized to interact with the saved object - * returned from some find query. - */ -export interface OwnerEntity { - owner: string; - id: string; -} diff --git a/x-pack/plugins/cases/server/client/user_actions/get.ts b/x-pack/plugins/cases/server/client/user_actions/get.ts index 7cc1dc7d27dfe..a0dddc79ef4b4 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -14,7 +14,6 @@ import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../com import { createCaseError } from '../../common/error'; import { checkEnabledCaseConnectorOrThrow } from '../../common'; import { CasesClientArgs } from '..'; -import { ensureAuthorized } from '../utils'; import { Operations } from '../../authorization'; import { UserActionGet } from './client'; @@ -22,13 +21,7 @@ export const get = async ( { caseId, subCaseId }: UserActionGet, clientArgs: CasesClientArgs ): Promise => { - const { - unsecuredSavedObjectsClient, - userActionService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, userActionService, logger, authorization } = clientArgs; try { checkEnabledCaseConnectorOrThrow(subCaseId); @@ -39,11 +32,11 @@ export const get = async ( subCaseId, }); - await ensureAuthorized({ - authorization, - auditLogger, - owners: userActions.saved_objects.map((userAction) => userAction.attributes.owner), - savedObjectIDs: userActions.saved_objects.map((userAction) => userAction.id), + await authorization.ensureAuthorized({ + entities: userActions.saved_objects.map((userAction) => ({ + owner: userAction.attributes.owner, + id: userAction.id, + })), operation: Operations.getUserActions, }); diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index d42947ad17edd..7ceb9cec60c39 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -12,8 +12,7 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { EcsEventOutcome, SavedObjectsFindResponse } from 'kibana/server'; -import { PublicMethodsOf } from '@kbn/utility-types'; +import { SavedObjectsFindResponse } from 'kibana/server'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; import { esKuery } from '../../../../../src/plugins/data/server'; import { @@ -30,7 +29,6 @@ import { OWNER_FIELD, } from '../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants'; -import { AuditEvent } from '../../../security/server'; import { combineFilterWithAuthorizationFilter } from '../authorization/utils'; import { getIDsAndIndicesAsArrays, @@ -38,9 +36,6 @@ import { isCommentRequestTypeUser, SavedObjectFindOptionsKueryNode, } from '../common'; -import { Authorization, DATABASE_CATEGORY, ECS_OUTCOMES, OperationDetails } from '../authorization'; -import { AuditLogger } from '../../../security/server'; -import { OwnerEntity } from './types'; export const decodeCommentRequest = (comment: CommentRequest) => { if (isCommentRequestTypeUser(comment)) { @@ -482,133 +477,3 @@ export const sortToSnake = (sortField: string | undefined): SortFieldCase => { return SortFieldCase.createdAt; } }; - -/** - * Creates an AuditEvent describing the state of a request. - */ -function createAuditMsg({ - operation, - outcome, - error, - savedObjectID, -}: { - operation: OperationDetails; - savedObjectID?: string; - outcome?: EcsEventOutcome; - error?: Error; -}): AuditEvent { - const doc = - savedObjectID != null - ? `${operation.savedObjectType} [id=${savedObjectID}]` - : `a ${operation.docType}`; - const message = error - ? `Failed attempt to ${operation.verbs.present} ${doc}` - : outcome === ECS_OUTCOMES.unknown - ? `User is ${operation.verbs.progressive} ${doc}` - : `User has ${operation.verbs.past} ${doc}`; - - return { - message, - event: { - action: operation.action, - category: DATABASE_CATEGORY, - type: [operation.type], - outcome: outcome ?? (error ? ECS_OUTCOMES.failure : ECS_OUTCOMES.success), - }, - ...(savedObjectID != null && { - kibana: { - saved_object: { type: operation.savedObjectType, id: savedObjectID }, - }, - }), - ...(error != null && { - error: { - code: error.name, - message: error.message, - }, - }), - }; -} - -/** - * Wraps the Authorization class' ensureAuthorized call in a try/catch to handle the audit logging - * on a failure. - */ -export async function ensureAuthorized({ - owners, - operation, - savedObjectIDs, - authorization, - auditLogger, -}: { - owners: string[]; - operation: OperationDetails; - savedObjectIDs: string[]; - authorization: PublicMethodsOf; - auditLogger?: AuditLogger; -}) { - const logSavedObjects = ({ outcome, error }: { outcome?: EcsEventOutcome; error?: Error }) => { - for (const savedObjectID of savedObjectIDs) { - auditLogger?.log(createAuditMsg({ operation, outcome, error, savedObjectID })); - } - }; - - try { - await authorization.ensureAuthorized(owners, operation); - - // log that we're attempting an operation - logSavedObjects({ outcome: ECS_OUTCOMES.unknown }); - } catch (error) { - logSavedObjects({ error }); - throw error; - } -} - -/** - * Function callback for making sure the found saved objects are of the authorized owner - */ -export type EnsureSOAuthCallback = (entities: OwnerEntity[]) => void; - -interface AuthFilterHelpers { - filter?: KueryNode; - ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; - logSuccessfulAuthorization: () => void; -} - -/** - * Wraps the Authorization class' method for determining which found saved objects the user making the request - * is authorized to interact with. - */ -export async function getAuthorizationFilter({ - operation, - authorization, - auditLogger, -}: { - operation: OperationDetails; - authorization: PublicMethodsOf; - auditLogger?: AuditLogger; -}): Promise { - try { - const { - filter, - ensureSavedObjectIsAuthorized, - logSuccessfulAuthorization, - } = await authorization.getFindAuthorizationFilter(operation); - return { - filter, - ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => { - for (const entity of entities) { - try { - ensureSavedObjectIsAuthorized(entity.owner); - auditLogger?.log(createAuditMsg({ operation, savedObjectID: entity.id })); - } catch (error) { - auditLogger?.log(createAuditMsg({ error, operation, savedObjectID: entity.id })); - } - } - }, - logSuccessfulAuthorization, - }; - } catch (error) { - auditLogger?.log(createAuditMsg({ error, operation })); - throw error; - } -} diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 1cd5ded87d76b..196314a0ecbfb 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -51,8 +51,9 @@ import { SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; import { ClientArgs } from '..'; -import { combineFilters, EnsureSOAuthCallback } from '../../client/utils'; +import { combineFilters } from '../../client/utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; +import { EnsureSOAuthCallback } from '../../authorization'; interface GetCaseIdsByAlertIdArgs extends ClientArgs { alertId: string; diff --git a/x-pack/plugins/security/server/authorization/actions/actions.mock.ts b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts index 97890e21c0eb7..ba627f08c00ca 100644 --- a/x-pack/plugins/security/server/authorization/actions/actions.mock.ts +++ b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts @@ -9,6 +9,7 @@ import type { Actions } from './actions'; import { AlertingActions } from './alerting'; import { ApiActions } from './api'; import { AppActions } from './app'; +import { CasesActions } from './cases'; import { SavedObjectActions } from './saved_object'; import { SpaceActions } from './space'; import { UIActions } from './ui'; @@ -19,6 +20,7 @@ jest.mock('./saved_object'); jest.mock('./space'); jest.mock('./ui'); jest.mock('./alerting'); +jest.mock('./cases'); const create = (versionNumber: string) => { const t = ({ @@ -27,6 +29,7 @@ const create = (versionNumber: string) => { login: 'login:', savedObject: new SavedObjectActions(versionNumber), alerting: new AlertingActions(versionNumber), + cases: new CasesActions(versionNumber), space: new SpaceActions(versionNumber), ui: new UIActions(versionNumber), version: `version:${versionNumber}`,