diff --git a/app/Controller/Component/ACLComponent.php b/app/Controller/Component/ACLComponent.php index d31769222..e26f2d367 100644 --- a/app/Controller/Component/ACLComponent.php +++ b/app/Controller/Component/ACLComponent.php @@ -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('*'), diff --git a/app/Controller/EventsController.php b/app/Controller/EventsController.php index f7e871e3a..9c115dd91 100644 --- a/app/Controller/EventsController.php +++ b/app/Controller/EventsController.php @@ -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) diff --git a/app/Model/Event.php b/app/Model/Event.php index 95a962c3b..30fc9088a 100755 --- a/app/Model/Event.php +++ b/app/Model/Event.php @@ -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)) { diff --git a/app/Model/EventLock.php b/app/Model/EventLock.php index a0fd2807a..790c2b801 100644 --- a/app/Model/EventLock.php +++ b/app/Model/EventLock.php @@ -1,65 +1,132 @@ 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)); } } diff --git a/app/View/Events/view.ctp b/app/View/Events/view.ctp index 38dd83e15..f4f00b60b 100644 --- a/app/View/Events/view.ctp +++ b/app/View/Events/view.ctp @@ -540,7 +540,7 @@