new: [UI] Event locks for background jobs and automatic tools

pull/6925/head
Jakub Onderka 2021-01-28 21:15:33 +01:00
parent 2faaa30578
commit c1399b36f9
6 changed files with 194 additions and 68 deletions

View File

@ -262,7 +262,9 @@ class ACLComponent extends Component
'viewEventAttributes' => array('*'),
'viewGraph' => array('*'),
'viewGalaxyMatrix' => array('*'),
'xml' => array('*')
'xml' => array('*'),
'addEventLock' => ['perm_auth'],
'removeEventLock' => ['perm_auth'],
),
'favouriteTags' => array(
'toggle' => array('*'),

View File

@ -5280,9 +5280,43 @@ class EventsController extends AppController
}
}
public function addEventLock($id)
{
if (!$this->request->is('post')) {
throw new MethodNotAllowedException('This endpoint requires a POST request.');
}
$event = $this->Event->fetchSimpleEvent($this->Auth->User(), $id);
if (empty($event)) {
throw new MethodNotAllowedException(__('Invalid Event'));
}
if (!$this->__canModifyEvent($event)) {
throw new UnauthorizedException(__('You do not have permission to do that.'));
}
$this->loadModel('EventLock');
$lockId = $this->EventLock->insertLockApi($event['Event']['id'], $this->Auth->user());
return $this->RestResponse->viewData(['lock_id' => $lockId], $this->response->type());
}
public function removeEventLock($id, $lockId)
{
if (!$this->request->is('post')) {
throw new MethodNotAllowedException('This endpoint requires a POST request.');
}
$event = $this->Event->fetchSimpleEvent($this->Auth->User(), $id);
if (empty($event)) {
throw new MethodNotAllowedException(__('Invalid Event'));
}
$this->loadModel('EventLock');
$deleted = $this->EventLock->deleteApiLock($event['Event']['id'], $lockId, $this->Auth->user());
return $this->RestResponse->viewData(['deleted' => $deleted], $this->response->type());
}
public function checkLocks($id)
{
$this->loadModel('EventLock');
$event = $this->Event->find('first', array(
'recursive' => -1,
'conditions' => array('Event.id' => $id),
@ -5290,29 +5324,34 @@ class EventsController extends AppController
));
$locks = array();
if (!empty($event) && ($event['Event']['orgc_id'] == $this->Auth->user('org_id') || $this->_isSiteAdmin())) {
$this->loadModel('EventLock');
$locks = $this->EventLock->checkLock($this->Auth->user(), $id);
}
if (!empty($locks)) {
$temp = $locks;
$locks = array();
foreach ($temp as $t) {
if ($t['User']['id'] !== $this->Auth->user('id')) {
if ($t['type'] === 'user' && $t['User']['id'] !== $this->Auth->user('id')) {
if (!$this->_isSiteAdmin() && $t['User']['org_id'] != $this->Auth->user('org_id')) {
continue;
$locks[] = __('another user');
} else {
$locks[] = $t['User']['email'];
}
$locks[] = $t['User']['email'];
} else if ($t['type'] === 'job') {
$locks[] = __('background job');
} else if ($t['type'] === 'api') {
$locks[] = __('external tool');
}
}
}
// TODO: i18n
if (!empty($locks)) {
$message = sprintf('Warning: Your view on this event might not be up to date as it is currently being edited by: %s', implode(', ', $locks));
$this->set('message', $message);
$this->layout = false;
$this->render('/Events/ajax/event_lock');
} else {
if (empty($locks)) {
return $this->RestResponse->viewData('', $this->response->type(), false, true);
}
$message = __('Warning: Your view on this event might not be up to date as it is currently being edited by: %s', implode(', ', $locks));
$this->set('message', $message);
$this->layout = false;
$this->render('/Events/ajax/event_lock');
}
public function getEditStrategy($id)

View File

@ -3619,9 +3619,6 @@ class Event extends AppModel
// Low level function to add an Event based on an Event $data array
public function _add(array &$data, $fromXml, array $user, $org_id = 0, $passAlong = null, $fromPull = false, $jobId = null, &$created_id = 0, &$validationErrors = array())
{
if ($jobId) {
App::uses('AuthComponent', 'Controller/Component');
}
if (Configure::read('MISP.enableEventBlocklisting') !== false && isset($data['Event']['uuid'])) {
$this->EventBlocklist = ClassRegistry::init('EventBlocklist');
if ($this->EventBlocklist->isBlocked($data['Event']['uuid'])) {
@ -3799,6 +3796,12 @@ class Event extends AppModel
);
$saveResult = $this->save(array('Event' => $data['Event']), array('fieldList' => $fieldList['Event']));
if ($saveResult) {
if ($jobId) {
/** @var EventLock $eventLock */
$eventLock = ClassRegistry::init('EventLock');
$eventLock->insertLockBackgroundJob($this->id, $jobId);
}
if ($passAlong) {
if ($server['Server']['publish_without_email'] == 0) {
$st = "enabled";
@ -3927,6 +3930,10 @@ class Event extends AppModel
}
}
}
if ($jobId) {
$eventLock->deleteBackgroundJobLock($this->id, $jobId);
}
return true;
} else {
$validationErrors['Event'] = $this->validationErrors;
@ -4051,6 +4058,11 @@ class Event extends AppModel
$saveResult = $this->save(array('Event' => $data['Event']), array('fieldList' => $fieldList));
$this->Log = ClassRegistry::init('Log');
if ($saveResult) {
if ($jobId) {
/** @var EventLock $eventLock */
$eventLock = ClassRegistry::init('EventLock');
$eventLock->insertLockBackgroundJob($data['Event']['id'], $jobId);
}
$validationErrors = array();
if (isset($data['Event']['Attribute'])) {
$data['Event']['Attribute'] = array_values($data['Event']['Attribute']);
@ -4177,6 +4189,9 @@ class Event extends AppModel
}
$this->publish($existingEvent['Event']['id']);
}
if ($jobId) {
$eventLock->deleteBackgroundJobLock($data['Event']['id'], $jobId);
}
return true;
}
return $this->validationErrors;
@ -6087,6 +6102,10 @@ class Event extends AppModel
$ontheflyattributes = array();
$i = 0;
if ($jobId) {
/** @var EventLock $eventLock */
$eventLock = ClassRegistry::init('EventLock');
$eventLock->insertLockBackgroundJob($event['Event']['id'], $jobId);
$this->Job = ClassRegistry::init('Job');
$total = count($attributeSources);
}
@ -6194,6 +6213,7 @@ class Event extends AppModel
$message = $saved . ' ' . $messageScopeSaved . ' created' . $emailResult . '.';
}
if ($jobId) {
$eventLock->deleteBackgroundJobLock($event['Event']['id'], $jobId);
$this->Job->saveStatus($jobId, true, __('Processing complete. %s', $message));
}
if (!empty($returnRawResults)) {

View File

@ -1,65 +1,132 @@
<?php
App::uses('AppModel', 'Model');
// Table `event_locks` is not used anymore
class EventLock extends AppModel
{
public $useTable = 'event_locks';
// In seconds
const DEFAULT_TTL = 900;
public $recursive = -1;
public $actsAs = array(
'Containable',
);
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
)
);
public $validate = array(
);
public function beforeValidate($options = array())
/**
* @param array $user
* @param int $eventId
* @return bool True if insert was successful.
*/
public function insertLock(array $user, $eventId)
{
parent::beforeValidate();
return true;
return $this->insertLockToRedis("misp:event_lock:$eventId:user:{$user['id']}", [
'type' => 'user',
'timestamp' => time(),
'User' => [
'id' => $user['id'],
'org_id' => $user['org_id'],
'email' => $user['email'],
]
]);
}
public function insertLock($user, $eventId)
/**
* @param int $eventId
* @param int $jobId
* @return bool True if insert was successful.
*/
public function insertLockBackgroundJob($eventId, $jobId)
{
$date = new DateTime();
$lock = array(
'timestamp' => $date->getTimestamp(),
return $this->insertLockToRedis("misp:event_lock:$eventId:job:$jobId", [
'type' => 'job',
'timestamp' => time(),
'job_id' => $jobId,
]);
}
/**
* @param int $eventId
* @return int|null Lock ID
*/
public function insertLockApi($eventId, array $user)
{
$rand = mt_rand();
if ($this->insertLockToRedis("misp:event_lock:$eventId:api:{$user['id']}:$rand", [
'type' => 'api',
'user_id' => $user['id'],
'event_id' => $eventId
);
$this->deleteAll(array('user_id' => $user['id']));
$this->create();
return $this->save($lock);
'timestamp' => time(),
])) {
return $rand;
}
return null;
}
public function checkLock($user, $eventId)
/**
* @param int $eventId
* @param int $jobId
* @return null
*/
public function deleteBackgroundJobLock($eventId, $jobId)
{
$this->cleanupLock($user, $eventId);
$locks = $this->find('all', array(
'recursive' => -1,
'contain' => array('User.email', 'User.org_id', 'User.id'),
'conditions' => array(
'event_id' => $eventId
)
));
return $locks;
try {
$redis = $this->setupRedisWithException();
} catch (Exception $e) {
return false;
}
$deleted = $redis->del("misp:event_lock:$eventId:job:$jobId");
return $deleted > 0;
}
// If a lock has been active for 15 minutes, delete it
public function cleanupLock()
/**
* @param string $key
* @param array $data
* @return bool
*/
private function insertLockToRedis($key, array $data)
{
$date = new DateTime();
$timestamp = $date->getTimestamp();
$timestamp -= 900;
$this->deleteAll(array('timestamp <' => $timestamp));
return true;
try {
$redis = $this->setupRedisWithException();
} catch (Exception $e) {
return false;
}
return $redis->setex($key, self::DEFAULT_TTL, json_encode($data));
}
/**
* @param int $eventId
* @param int $lockId
* @return bool
*/
public function deleteApiLock($eventId, $lockId, array $user)
{
try {
$redis = $this->setupRedisWithException();
} catch (Exception $e) {
return false;
}
$deleted = $redis->del("misp:event_lock:$eventId:api:{$user['id']}:$lockId");
return $deleted > 0;
}
/**
* @param array $user
* @param int $eventId
* @return array[]
* @throws JsonException
*/
public function checkLock(array $user, $eventId)
{
try {
$redis = $this->setupRedisWithException();
} catch (Exception $e) {
return [];
}
$keys = $redis->keys("misp:event_lock:$eventId:*");
if (empty($keys)) {
return [];
}
return array_map(function ($value) {
return $this->jsonDecode($value);
}, $redis->mget($keys));
}
}

View File

@ -540,7 +540,7 @@
<script type="text/javascript">
var showContext = false;
$(function () {
queryEventLock('<?php echo h($event['Event']['id']); ?>', '<?php echo h($me['org_id']); ?>');
queryEventLock('<?php echo h($event['Event']['id']); ?>');
popoverStartup();
$("th, td, dt, div, span, li").tooltip({

View File

@ -4998,16 +4998,14 @@ $(document.body).on('click', 'a[data-paginator]', function (e) {
});
});
function queryEventLock(event_id, user_org_id) {
function queryEventLock(event_id) {
if (tabIsActive) {
$.ajax({
url: baseurl + "/events/checkLocks/" + event_id,
type: "get",
success: function(data, statusText, xhr) {
if (xhr.status == 200) {
if ($('#event_lock_warning').length != 0) {
$('#event_lock_warning').remove();
}
$('#event_lock_warning').remove();
if (data != '') {
$('#main-view-container').append(data);
}
@ -5015,7 +5013,7 @@ function queryEventLock(event_id, user_org_id) {
}
});
}
setTimeout(function() { queryEventLock(event_id, user_org_id); }, 5000);
setTimeout(function() { queryEventLock(event_id); }, 5000);
}
function checkIfLoggedIn() {