CVE-2019-16202: Improper Privilege Management (CWE-269)
value: MISP before 2.4.115 allows privilege escalation in certain situations. After updating to 2.4.115, escalation attempts are blocked by the __checkLoggedActions function with a "This could be an indication of an attempted privilege escalation on older vulnerable versions of MISP (<2.4.115)" message.
CWE description: The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.
CVSS v3 base score: 6.5
Published: 2019-09-10T14:15Z
Repository: https://github.com/MISP/MISP
Language: PHP
Files changed: app/Console/Command/AdminShell.php, app/Controller/Component/ACLComponent.php, app/Controller/ServersController.php, app/Controller/UsersController.php, app/Model/Log.php, app/Model/Server.php, app/Model/User.php, app/View/Servers/index.ctp, app/View/Users/admin_index.ctp
Commit date: 2019-09-09 13:00:21 +0200
Commit message: fix: [security] Fix to a vulnerability related to the server index

- along with various support tools
- more information coming soon

Diff: diff --git a/app/Console/Command/AdminShell.php b/app/Console/Command/AdminShell.php
index 07817f0589e..0ee3544faab 100644
--- a/app/Console/Command/AdminShell.php
+++ b/app/Console/Command/AdminShell.php
@@ -504,6 +504,32 @@ class AdminShell extends AppShell
     {
         echo 'Cleaning caches...' . PHP_EOL;
         $this->Server->cleanCacheFiles();
         echo '...caches lost in time, like tears in rain.' . PHP_EOL;
     }
+
+    public function resetSyncAuthkeys()
+    {
+        if (empty($this->args[0])) {
+            echo sprintf(
+                __("MISP mass sync authkey reset command line tool.\n\nUsage: %sConsole/cake resetSyncAuthkeys [user_id]") . "\n\n",
+                APP
+            );
+            die();
+        } else {
+            $userId = $this->args[0];
+            $user = $this->User->getAuthUser($userId);
+            if (empty($user)) {
+                echo __('Invalid user.') . "\n\n";
+            }
+            if (!$user['Role']['perm_site_admin']) {
+                echo __('User has to be a site admin.') . "\n\n";
+            }
+            if (!empty($this->args[1])) {
+                $jobId = $this->args[1];
+            } else {
+                $jobId = false;
+            }
+            $this->User->resetAllSyncAuthKeys($user, $jobId);
+        }
+    }
 }
diff --git a/app/Controller/Component/ACLComponent.php b/app/Controller/Component/ACLComponent.php
index f796c3cede8..1c019daa9d8 100644
--- a/app/Controller/Component/ACLComponent.php
+++ b/app/Controller/Component/ACLComponent.php
@@ -333,11 +333,11 @@ class ACLComponent extends Component
                     'index' => array('*'),
                     'view' => array('*'),
             ),
             'servers' => array(
                     'add' => array(),
-                    'cache' => array('perm_site_admin'),
+                    'cache' => array(),
                     'checkout' => array(),
                     'createSync' => array('perm_sync'),
                     'delete' => array(),
                     'deleteFile' => array(),
                     'edit' => array(),
@@ -346,18 +346,18 @@ class ACLComponent extends Component
                     'getApiInfo' => array('*'),
                     'getGit' => array(),
                     'getInstanceUUID' => array('perm_sync'),
                     'getPyMISPVersion' => array('*'),
                     'getSetting' => array(),
-                    'getSubmodulesStatus' => array('perm_site_admin'),
-                    'getSubmoduleQuickUpdateForm' => array('perm_site_admin'),
+                    'getSubmodulesStatus' => array(),
+                    'getSubmoduleQuickUpdateForm' => array(),
                     'getWorkers' => array(),
                     'getVersion' => array('*'),
-                    'import' => ('perm_site_admin'),
-                    'index' => array('OR' => array('perm_sync', 'perm_admin')),
+                    'import' => array(),
+                    'index' => array(),
                     'ondemandAction' => array(),
-                    'postTest' => array('perm_sync'),
+                    'postTest' => array(),
                     'previewEvent' => array(),
                     'previewIndex' => array(),
                     'pull' => array(),
                     'purgeSessions' => array(),
                     'push' => array(),
@@ -369,11 +369,11 @@ class ACLComponent extends Component
                     'startWorker' => array(),
                     'startZeroMQServer' => array(),
                     'statusZeroMQServer' => array(),
                     'stopWorker' => array(),
                     'stopZeroMQServer' => array(),
-                    'testConnection' => array('perm_sync'),
+                    'testConnection' => array(),
                     'update' => array(),
                     'updateJSON' => array(),
                     'updateProgress' => array(),
                     'updateSubmodule' => array(),
                     'uploadFile' => array(),
@@ -516,10 +516,11 @@ class ACLComponent extends Component
                     'histogram' => array('*'),
                     'index' => array('*'),
                     'initiatePasswordReset' => array('perm_admin'),
                     'login' => array('*'),
                     'logout' => array('*'),
+                    'resetAllSyncAuthKeys' => array(),
                     'resetauthkey' => array('*'),
                     'request_API' => array('*'),
                     'routeafterlogin' => array('*'),
                     'statistics' => array('*'),
                     'tagStatisticsGraph' => array('*'),
@@ -551,10 +552,82 @@ class ACLComponent extends Component
                     'add' => array('perm_add'),
                     'delete' => array('perm_modify'),
             )
     );
 
+    private function __checkLoggedActions($user, $controller, $action)
+    {
+        $loggedActions = array(
+            'servers' => array(
+                'index' => array(
+                    'role' => array(
+                        'NOT' => array(
+                            'perm_site_admin'
+                        )
+                    ),
+                    'message' => __('This could be an indication of an attempted privilege escalation on older vulnerable versions of MISP (<2.4.115)')
+                )
+            )
+        );
+        foreach ($loggedActions as $k => $v) {
+            $loggedActions[$k] = array_change_key_case($v);
+        }
+        $message = '';
+        if (!empty($loggedActions[$controller])) {
+            if (!empty($loggedActions[$controller][$action])) {
+                $message = $loggedActions[$controller][$action]['message'];
+                $hit = false;
+                if (empty($loggedActions[$controller][$action]['role'])) {
+                    $hit = true;
+                } else {
+                    $role_req = $loggedActions[$controller][$action]['role'];
+                    if (empty($role_req['OR']) && empty($role_req['AND']) && empty($role_req['NOT'])) {
+                        $role_req = array('OR' => $role_req);
+                    }
+                    if (!empty($role_req['NOT'])) {
+                        foreach ($role_req['NOT'] as $k => $v) {
+                            if (!$user['Role'][$v]) {
+                                $hit = true;
+                                continue;
+                            }
+                        }
+                    }
+                    if (!$hit && !empty($role_req['AND'])) {
+                        $subhit = true;
+                        foreach ($role_req['AND'] as $k => $v) {
+                            $subhit = $subhit && $user['Role'][$v];
+                        }
+                        if ($subhit) {
+                            $hit = true;
+                        }
+                    }
+                    if (!$hit && !empty($role_req['OR'])) {
+                        foreach ($role_req['OR'] as $k => $v) {
+                            if ($user['Role'][$v]) {
+                                $hit = true;
+                                continue;
+                            }
+                        }
+                    }
+                    if ($hit) {
+                        $this->Log = ClassRegistry::init('Log');
+                        $this->Log->create();
+                        $this->Log->save(array(
+                                'org' => 'SYSTEM',
+                                'model' => 'User',
+                                'model_id' => $user['id'],
+                                'email' => $user['email'],
+                                'action' => 'security',
+                                'user_id' => $user['id'],
+                                'title' => __('User triggered security alert by attempting to access /%s/%s. Reason why this endpoint is of interest: %s', $controller, $action, $message),
+                        ));
+                    }
+                }
+            }
+        }
+    }
+
     // The check works like this:
     // If the user is a site admin, return true
     // If the requested action has an OR-d list, iterate through the list. If any of the permissions are set for the user, return true
     // If the requested action has an AND-ed list, iterate through the list. If any of the permissions for the user are not set, turn the check to false. Otherwise return true.
     // If the requested action has a permission, check if the user's role has it flagged. If yes, return true
@@ -565,10 +638,11 @@ class ACLComponent extends Component
         $action = strtolower($action);
         $aclList = $this->__aclList;
         foreach ($aclList as $k => $v) {
             $aclList[$k] = array_change_key_case($v);
         }
+        $this->__checkLoggedActions($user, $controller, $action);
         if ($user['Role']['perm_site_admin']) {
             return true;
         }
         if (!isset($aclList[$controller])) {
             return $this->__error(404, 'Invalid controller.', $soft);
diff --git a/app/Controller/ServersController.php b/app/Controller/ServersController.php
index b3190420e09..c59b17c00a7 100644
--- a/app/Controller/ServersController.php
+++ b/app/Controller/ServersController.php
@@ -42,16 +42,10 @@ class ServersController extends AppController
         }
     }
 
     public function index()
     {
-        if (!$this->_isSiteAdmin()) {
-            if (!$this->userRole['perm_sync'] && !$this->userRole['perm_admin']) {
-                $this->redirect(array('controller' => 'events', 'action' => 'index'));
-            }
-            $this->paginate['conditions'] = array('Server.org_id LIKE' => $this->Auth->user('org_id'));
-        }
         if ($this->_isRest()) {
             $params = array(
                 'recursive' => -1,
                 'contain' => array(
                         'User' => array(
@@ -2087,6 +2081,30 @@ misp.direct_call(relative_path, body)
                     $this->redirect(array('action' => 'index'));
                 }
             }
         }
     }
+
+    public function resetRemoteAuthKey($id)
+    {
+        if (!$this->request->is('post')) {
+            throw new MethodNotAllowedException(__('This endpoint expects POST requests.'));
+        }
+        $result = $this->Server->resetRemoteAuthkey($id);
+        if ($result !== true) {
+            if (!$this->_isRest()) {
+                $this->Flash->error($result);
+                $this->redirect(array('action' => 'index'));
+            } else {
+                return $this->RestResponse->saveFailResponse('Servers', 'resetRemoteAuthKey', $id, $message, $this->response->type());
+            }
+        } else {
+            $message = __('API key updated.');
+            if (!$this->_isRest()) {
+                $this->Flash->success($message);
+                $this->redirect(array('action' => 'index'));
+            } else {
+                return $this->RestResponse->saveSuccessResponse('Servers', 'resetRemoteAuthKey', $message, $this->response->type());
+            }
+        }
+    }
 }
diff --git a/app/Controller/UsersController.php b/app/Controller/UsersController.php
index 6495a081b2c..15137472670 100644
--- a/app/Controller/UsersController.php
+++ b/app/Controller/UsersController.php
@@ -209,11 +209,11 @@ class UsersController extends AppController
                 }
                 $temp = $user['User']['password'];
                 // Save the data
                 if ($this->User->save($user)) {
                     $message = __('Password Changed.');
-                    $this->__extralog("change_pw");
+                    $this->User->extralog($this->Auth->user(), "change_pw", null, null, $user);
                     if ($this->_isRest()) {
                         return $this->RestResponse->saveSuccessResponse('User', 'change_pw', false, $this->response->type(), $message);
                     }
                     $this->Flash->success($message);
                     $this->_refreshAuth();
@@ -867,11 +867,15 @@ class UsersController extends AppController
                             }
                         }
                         $c++;
                     }
                     $fieldsResultStr = substr($fieldsResultStr, 2);
-                    $this->__extralog("edit", "user", $fieldsResultStr);
+                    $user = $this->User->find('first', array(
+                        'recursive' => -1,
+                        'conditions' => array('User.id' => $this->id)
+                    ));
+                    $this->User->extralog($this->Auth->user(), "edit", "user", $fieldsResultStr, $user);
                     if ($this->_isRest()) {
                         $user = $this->User->find('first', array(
                                 'conditions' => array('User.id' => $this->User->id),
                                 'recursive' => -1
                         ));
@@ -952,11 +956,11 @@ class UsersController extends AppController
         if (empty($user)) {
             throw new NotFoundException(__('Invalid user'));
         }
         $fieldsDescrStr = 'User (' . $id . '): ' . $user['User']['email'];
         if ($this->User->delete($id)) {
-            $this->__extralog("delete", $fieldsDescrStr, '');
+            $this->User->extralog($this->Auth->user(), "delete", $fieldsDescrStr, '');
             if ($this->_isRest()) {
                 return $this->RestResponse->saveSuccessResponse('User', 'admin_delete', $id, $this->response->type(), 'User deleted.');
             } else {
                 $this->Flash->success(__('User deleted'));
                 $this->redirect(array('action' => 'index'));
@@ -1008,11 +1012,11 @@ class UsersController extends AppController
                 unset($this->Auth->authenticate['Form']['passwordHasher']);
                 $this->Auth->constructAuthenticate();
             }
         }
         if ($this->Auth->login()) {
-            $this->__extralog("login");
+            $this->User->extralog($this->Auth->user(), "login");
             $this->User->Behaviors->disable('SysLogLogable.SysLogLogable');
             $this->User->id = $this->Auth->user('id');
             $user = $this->User->find('first', array(
                 'conditions' => array(
                     'User.id' => $this->Auth->user('id')
@@ -1123,11 +1127,11 @@ class UsersController extends AppController
     }
 
     public function logout()
     {
         if ($this->Session->check('Auth.User')) {
-            $this->__extralog("logout");
+            $this->User->extralog($this->Auth->user(), "logout");
         }
         $this->Flash->info(__('Good-Bye'));
         $user = $this->User->find('first', array(
             'conditions' => array(
                 'User.id' => $this->Auth->user('id')
@@ -1138,46 +1142,53 @@ class UsersController extends AppController
         $user['User']['action'] = 'logout';
         $this->User->save($user['User'], true, array('id'));
         $this->redirect($this->Auth->logout());
     }
 
-    public function resetauthkey($id = null)
+    public function resetauthkey($id = null, $alert = false)
     {
         if (!$this->_isAdmin() && Configure::read('MISP.disableUserSelfManagement')) {
             throw new MethodNotAllowedException('User self-management has been disabled on this instance.');
         }
         if ($id == 'me') {
             $id = $this->Auth->user('id');
         }
         if (!$this->userRole['perm_auth']) {
-            throw new MethodNotAllowedException('Invalid action.');
+            throw new MethodNotAllowedException(__('Invalid action.'));
+        }
+        $newkey = $this->User->resetauthkey($this->Auth->user(), $id, $alert);
+        if ($newkey === false) {
+            throw new MethodNotAllowedException(__('Invalid user.'));
         }
-        $this->User->id = $id;
-        if (!$id || !$this->User->exists($id)) {
-            throw new MethodNotAllowedException('Invalid user.');
-        }
-        $user = $this->User->read();
-        $oldKey = $this->User->data['User']['authkey'];
-        if (!$this->_isSiteAdmin() && !($this->_isAdmin() && $this->Auth->user('org_id') == $this->User->data['User']['org_id']) && ($this->Auth->user('id') != $id)) {
-            throw new MethodNotAllowedException('Invalid user.');
-        }
-        $newkey = $this->User->generateAuthKey();
-        $this->User->saveField('authkey', $newkey);
-        $this->__extralog(
-                'reset_auth_key',
-                'Authentication key for user ' . $user['User']['id'] . ' (' . $user['User']['email'] . ')',
-                $fieldsResult = 'authkey(' . $oldKey . ') => (' . $newkey . ')'
-        );
         if (!$this->_isRest()) {
             $this->Flash->success(__('New authkey generated.', true));
             $this->_refreshAuth();
             $this->redirect($this->referer());
         } else {
             return $this->RestResponse->saveSuccessResponse('User', 'resetauthkey', $id, $this->response->type(), 'Authkey updated: ' . $newkey);
         }
     }
 
+    public function resetAllSyncAuthKeys()
+    {
+        if (!$this->request->is('post')) {
+            throw new MethodNotAllowedException(__('This functionality is only accessible via POST requests.'));
+        }
+        $results = $this->User->resetAllSyncAuthKeysRouter($this->Auth->user());
+        if ($results === true) {
+            $message = __('Job initiated.');
+        } else {
+            $message = __('%s authkeys reset, %s could not be reset.', $results['success'], $results['fails']);
+        }
+        if (!$this->_isRest()) {
+            $this->Flash->info($message);
+            $this->redirect($this->referer());
+        } else {
+            return $this->RestResponse->saveSuccessResponse('User', 'resetAllSyncAuthKeys', false, $this->response->type(), $message);
+        }
+    }
+
     public function histogram($selected = null)
     {
         //if (!$this->request->is('ajax') && !$this->_isRest()) throw new MethodNotAllowedException('This function can only be accessed via AJAX or the API.');
         if ($selected == '[]') {
             $selected = null;
@@ -1295,48 +1306,10 @@ class UsersController extends AppController
         }
         $this->response->file($termsFile, array('download' => true, 'name' => Configure::read('MISP.terms_file')));
         return $this->response;
     }
 
-    private function __extralog($action = null, $description = null, $fieldsResult = null)
-    {
-        // new data
-        $model = 'User';
-        $modelId = $this->Auth->user('id');
-        if ($action == 'login') {
-            $description = "User (" . $this->Auth->user('id') . "): " . $this->data['User']['email'];
-        } elseif ($action == 'logout') {
-            $description = "User (" . $this->Auth->user('id') . "): " . $this->Auth->user('email');
-        } elseif ($action == 'edit') {
-            $description = "User (" . $this->User->id . "): " . $this->data['User']['email'];
-        } elseif ($action == 'change_pw') {
-            $description = "User (" . $this->User->id . "): " . $this->Auth->user('email');
-            $fieldsResult = "Password changed.";
-        }
-
-        // query
-        $this->Log = ClassRegistry::init('Log');
-        $this->Log->create();
-        $this->Log->save(array(
-            'org' => $this->Auth->user('Organisation')['name'],
-            'model' => $model,
-            'model_id' => $modelId,
-            'email' => $this->Auth->user('email'),
-            'action' => $action,
-            'title' => $description,
-            'change' => isset($fieldsResult) ? $fieldsResult : ''));
-
-        // write to syslogd as well
-        App::import('Lib', 'SysLog.SysLog');
-        $syslog = new SysLog();
-        if (isset($fieldsResult) && $fieldsResult) {
-            $syslog->write('notice', $description . ' -- ' . $action . ' -- ' . $fieldsResult);
-        } else {
-            $syslog->write('notice', $description . ' -- ' . $action);
-        }
-    }
-
     // Used for fields_before and fields for audit
     public function arrayCopy(array $array)
     {
         $result = array();
         foreach ($array as $key => $val) {
diff --git a/app/Model/Log.php b/app/Model/Log.php
index a7c0e8b38ed..4748fcba8b2 100644
--- a/app/Model/Log.php
+++ b/app/Model/Log.php
@@ -46,10 +46,11 @@ class Log extends AppModel
                             'push',
                             'remove_dead_workers',
                             'request',
                             'request_delegation',
                             'reset_auth_key',
+                            'security',
                             'serverSettingsEdit',
                             'tag',
                             'undelete',
                             'update',
                             'update_database',
diff --git a/app/Model/Server.php b/app/Model/Server.php
index 2d1cd1e747e..7fdc93be8f3 100644
--- a/app/Model/Server.php
+++ b/app/Model/Server.php
@@ -2287,14 +2287,14 @@ class Server extends AppModel
         $existingEvent = $eventModel->find('first', array('conditions' => array('Event.uuid' => $event['Event']['uuid'])));
         $passAlong = $server['Server']['id'];
         if (!$existingEvent) {
             // add data for newly imported events
             $result = $eventModel->_add($event, true, $user, $server['Server']['org_id'], $passAlong, true, $jobId);
-            if ($result === true) {
+            if ($result) {
                 $successes[] = $eventId;
             } else {
-                $fails[$eventId] = __('Failed (partially?) because of errors: ') . $result;
+                $fails[$eventId] = __('Failed (partially?) because of validation errors: ') . json_encode($eventModel->validationErrors, true);
             }
         } else {
             if (!$existingEvent['Event']['locked'] && !$server['Server']['internal']) {
                 $fails[$eventId] = __('Blocked an edit to an event that was created locally. This can happen if a synchronised event that was created on this instance was modified by an administrator on the remote side.');
             } else {
@@ -2314,10 +2314,11 @@ class Server extends AppModel
     {
         $event = $eventModel->downloadEventFromServer(
                 $eventId,
                 $server
         );
+        ;
         if (!empty($event)) {
             if ($this->__checkIfEventIsBlockedBeforePull($event)) {
                 return false;
             }
             $event = $this->__updatePulledEventBeforeInsert($event, $server, $user);
@@ -2326,11 +2327,11 @@ class Server extends AppModel
             } else {
                 $this->__checkIfPulledEventExistsAndAddOrUpdate($event, $eventId, $successes, $fails, $eventModel, $server, $user, $jobId);
             }
         } else {
             // error
-            $fails[$eventId] = __('failed downloading the event') . ': ' . json_encode($event);
+            $fails[$eventId] = __('failed downloading the event');
         }
         return true;
     }
 
     private function __handlePulledProposals($proposals, $events, $job, $jobId, $eventModel, $user)
@@ -5185,6 +5186,67 @@ class Server extends AppModel
             $result = $this->$target->update();
             $results[$target] = $result === false ? false : true;
         }
         return $results;
     }
+
+    public function resetRemoteAuthKey($id)
+    {
+        $server = $this->find('first', array(
+            'recursive' => -1,
+            'conditions' => array('Server.id' => $id)
+        ));
+        if (empty($server)) {
+            return __('Invalid server');
+        }
+        $HttpSocket = $this->setupHttpSocket($server);
+        $request = $this->setupSyncRequest($server);
+        $uri = $server['Server']['url'] . '/users/resetauthkey/me';
+        try {
+            $response = $HttpSocket->post($uri, '{}', $request);
+        } catch (Exception $e) {
+            $this->Log = ClassRegistry::init('Log');
+            $this->Log->create();
+            $message = 'Could not reset the remote authentication key.';
+            $this->Log->save(array(
+                    'org' => 'SYSTEM',
+                    'model' => 'Server',
+                    'model_id' => $id,
+                    'email' => 'SYSTEM',
+                    'action' => 'error',
+                    'user_id' => 0,
+                    'title' => 'Error: ' . $message,
+            ));
+            return $message;
+        }
+        if ($response->isOk()) {
+            try {
+                $response = json_decode($response->body, true);
+            } catch (Exception $e) {
+                $this->Log = ClassRegistry::init('Log');
+                $this->Log->create();
+                $message = 'Invalid response received from the remote instance.';
+                $this->Log->save(array(
+                        'org' => 'SYSTEM',
+                        'model' => 'Server',
+                        'model_id' => $id,
+                        'email' => 'SYSTEM',
+                        'action' => 'error',
+                        'user_id' => 0,
+                        'title' => 'Error: ' . $message,
+                ));
+                return $message;
+            }
+            if (!empty($response['message'])) {
+                $authkey = $response['message'];
+            }
+            if (substr($authkey, 0, 17) === 'Authkey updated: ') {
+                $authkey = substr($authkey, 17, 57);
+            }
+            $server['Server']['authkey'] = $authkey;
+            $this->save($server);
+            return true;
+        } else {
+            return __('Could not reset the remote authentication key.');
+        }
+    }
 }
diff --git a/app/Model/User.php b/app/Model/User.php
index d6e6cd6df1f..9b1e3aa0ed0 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -1251,6 +1251,176 @@ class User extends AppModel
         ));
         $this->validator()->remove('password'); // password is too simple, remove validation
         $this->save($admin);
         return $authKey;
     }
+
+    public function resetAllSyncAuthKeysRouter($user, $jobId = false)
+    {
+        if (Configure::read('MISP.background_jobs')) {
+            $job = ClassRegistry::init('Job');
+            $job->create();
+            $eventModel = ClassRegistry::init('Event');
+            $data = array(
+                    'worker' => $eventModel->__getPrioWorkerIfPossible(),
+                    'job_type' => __('reset_all_sync_api_keys'),
+                    'job_input' => __('Reseting all API keys'),
+                    'status' => 0,
+                    'retries' => 0,
+                    'org_id' => $user['org_id'],
+                    'org' => $user['Organisation']['name'],
+                    'message' => 'Issuing new API keys to all sync users.',
+            );
+            $job->save($data);
+            $jobId = $job->id;
+            $process_id = CakeResque::enqueue(
+                    'prio',
+                    'AdminShell',
+                    array('resetSyncAuthkeys', $user['id'], $jobId),
+                    true
+            );
+            $job->saveField('process_id', $process_id);
+            return true;
+        } else {
+            return $this->resetAllSyncAuthKeys($user);
+        }
+    }
+
+    public function resetAllSyncAuthKeys($user, $jobId = false)
+    {
+        $affected_users = $this->find('all', array(
+            'recursive' => -1,
+            'contain' => array('Role'),
+            'conditions' => array(
+                'OR' => array(
+                    'Role.perm_sync' => 1,
+                    'Role.perm_admin' => 1
+                ),
+                'Role.perm_site_admin' => 0
+            )
+        ));
+        $results = array('success' => 0, 'fails' => 0);
+        $user_count = count($affected_users);
+        if ($jobId) {
+            $job = ClassRegistry::init('Job');
+            $existingJob = $job->find('first', array(
+                'conditions' => array('Job.id' => $jobId),
+                'recursive' => -1
+            ));
+            if (empty($existingJob)) {
+                $jobId = false;
+            }
+        }
+        foreach ($affected_users as $k => $affected_user) {
+            try {
+                $reset_result = $this->resetauthkey($user, $affected_user['User']['id'], true);
+                if ($reset_result) {
+                    $results['success'] += 1;
+                } else {
+                    $results['fails'] += 1;
+                }
+            } catch (Exception $e) {
+                $results['fails'] += 1;
+            }
+            if ($jobId) {
+                if ($k % 100 == 0) {
+                    $job->id =  $jobId;
+                    $job->saveField('progress', 100 * (($k + 1) / count($user_count)));
+                    $job->saveField('message', __('Reset in progress - %s/%s.', $k, $user_count));
+                }
+            }
+        }
+        if ($jobId) {
+            $message = __('%s authkeys reset, %s could not be reset', $results['success'], $results['fails']);
+            $job->saveField('progress', 100);
+            $job->saveField('message', $message);
+            $job->saveField('status', 4);
+        }
+        return $results;
+    }
+
+    public function resetauthkey($user, $id, $alert = false)
+    {
+        $this->id = $id;
+        if (!$id || !$this->exists($id)) {
+            return false;
+        }
+        $updatedUser = $this->read();
+        $oldKey = $this->data['User']['authkey'];
+        if (empty($user['Role']['perm_site_admin']) && !($user['Role']['perm_admin'] && $user['org_id'] == $updatedUser['User']['org_id']) && ($user['id'] != $id)) {
+            return false;
+        }
+        $newkey = $this->generateAuthKey();
+        $this->saveField('authkey', $newkey);
+        $this->extralog(
+                $user,
+                'reset_auth_key',
+                sprintf(
+                    __('Authentication key for user %s (%s) updated.'),
+                    $updatedUser['User']['id'],
+                    $updatedUser['User']['email']
+                ),
+                $fieldsResult = 'authkey(' . $oldKey . ') => (' . $newkey . ')',
+                $updatedUser
+        );
+        if ($alert) {
+            $baseurl = Configure::read('MISP.external_baseurl');
+            if (empty($baseurl)) {
+                $baseurl = Configure::read('MISP.baseurl');
+            }
+            $body = __(
+                "Dear user,\n\nan API key reset has been triggered by an administrator for your user account on %s.\n\nYour new API key is: %s\n\nPlease update your server's sync setup to reflect this change.\n\nWe apologise for the inconvenience.",
+                $baseurl,
+                $newkey
+            );
+            $bodyNoEnc = __(
+                "Dear user,\n\nan API key reset has been triggered by an administrator for your user account on %s.\n\nYour new API key can be retrieved by logging in using this sync user's account.\n\nPlease update your server's sync setup to reflect this change.\n\nWe apologise for the inconvenience.",
+                $baseurl,
+                $newkey
+            );
+            $this->sendEmail(
+                $updatedUser,
+                $body,
+                $bodyNoEnc,
+                __('API key reset by administrator')
+            );
+        }
+        return $newkey;
+    }
+
+    public function extralog($user, $action = null, $description = null, $fieldsResult = null, $modifiedUser = null)
+    {
+        // new data
+        $model = 'User';
+        $modelId = $user['id'];
+        if (!empty($modifiedUser)) {
+            $modelId = $modifiedUser['User']['id'];
+        }
+        if ($action == 'login') {
+            $description = "User (" . $user['id'] . "): " . $user['email'];
+        } elseif ($action == 'logout') {
+            $description = "User (" . $user['id'] . "): " . $user['email'];
+        } elseif ($action == 'edit') {
+            $description = "User (" . $modifiedUser['User']['id'] . "): " . $modifiedUser['User']['email'];
+        } elseif ($action == 'change_pw') {
+            $description = "User (" . $modifiedUser['User']['id'] . "): " . $modifiedUser['User']['email'];
+            $fieldsResult = "Password changed.";
+        }
+
+        // query
+        $this->Log = ClassRegistry::init('Log');
+        $this->Log->create();
+        $this->Log->save(array(
+            'org' => $user['Organisation']['name'],
+            'model' => $model,
+            'model_id' => $modelId,
+            'email' => $user['email'],
+            'action' => $action,
+            'title' => $description,
+            'change' => isset($fieldsResult) ? $fieldsResult : ''));
+
+        // write to syslogd as well
+        App::import('Lib', 'SysLog.SysLog');
+        $syslog = new SysLog();
+        $syslog->write('notice', $description . ' -- ' . $action . (empty($fieldResult) ? '' : '-- ' . $fieldResult));
+    }
 }
diff --git a/app/View/Servers/index.ctp b/app/View/Servers/index.ctp
index 1d2c31da215..0dba82bd933 100644
--- a/app/View/Servers/index.ctp
+++ b/app/View/Servers/index.ctp
@@ -19,10 +19,11 @@
     <table class="table table-striped table-hover table-condensed">
     <tr>
             <th><?php echo $this->Paginator->sort('id');?></th>
             <th><?php echo $this->Paginator->sort('name');?></th>
             <th><?php echo __('Connection test');?></th>
+            <th><?php echo __('Reset API key');?></th>
             <th><?php echo $this->Paginator->sort('internal');?></th>
             <th><?php echo $this->Paginator->sort('push');?></th>
             <th><?php echo $this->Paginator->sort('pull');?></th>
             <th><?php echo $this->Paginator->sort('caching_enabled', 'Cache');?></th>
             <th><?php echo $this->Paginator->sort('unpublish_event (push event)');?></th>
@@ -68,10 +69,24 @@ foreach ($servers as $server):
                 if (!empty($server['Server']['name'])) echo h($server['Server']['name']);
                 else echo h($server['Server']['url']);
             ?>
         </td>
         <td id="connection_test_<?php echo $server['Server']['id'];?>"><span role="button" tabindex="0" aria-label="<?php echo __('Test the connection to the remote instance');?>" title="<?php echo __('Test the connection to the remote instance');?>" class="btn btn-primary" style="line-height:10px; padding: 4px 4px;" onClick="testConnection('<?php echo $server['Server']['id'];?>');"><?php echo __('Run');?></span></td>
+        <td id="reset_api_key_<?php echo $server['Server']['id'];?>">
+            <?php
+                echo $this->Form->postLink(
+                    __('Reset'),
+                    $baseurl . '/servers/resetRemoteAuthKey/' . $server['Server']['id'],
+                    array(
+                        'style' => 'line-height:10px; padding: 4px 4px;',
+                        'title' => __('Remotely reset API key'),
+                        'aria-label' => __('Remotely reset API key'),
+                        'class' => 'btn btn-primary'
+                    )
+                );
+            ?>
+        </td>
 
 <td><span class="<?php echo ($server['Server']['internal']? 'icon-ok' : 'icon-remove'); ?>" role="img" aria-label="<?php echo ($server['Server']['internal']? __('Yes') : __('No')); ?>" title="<?php echo ($server['Server']['internal']? __('Internal instance that ignores distribution level degradation *WARNING: Only use this setting if you have several internal instances and the sync link is to an internal extension of the current MISP community*') : __('Normal sync link to an external MISP instance. Distribution degradation will follow the normal rules.')); ?>"></span></td>
         <td><span class="<?php echo ($server['Server']['push']? 'icon-ok' : 'icon-remove'); ?>" role="img" aria-label="<?php echo ($server['Server']['push']? __('Yes') : __('No')); ?>"></span><span class="short <?php if (!$server['Server']['push'] || empty($ruleDescription['push'])) echo "hidden"; ?>" data-toggle="popover" title="Distribution List" data-content="<?php echo $ruleDescription['push']; ?>"> (<?php echo __('Rules');?>)</span></td>
         <td><span class="<?php echo ($server['Server']['pull']? 'icon-ok' : 'icon-remove'); ?>" role="img" aria-label="<?php echo ($server['Server']['pull']? __('Yes') : __('No')); ?>"></span><span class="short <?php if (!$server['Server']['pull'] || empty($ruleDescription['pull'])) echo "hidden"; ?>" data-toggle="popover" title="Distribution List" data-content="<?php echo $ruleDescription['pull']; ?>"> (<?php echo __('Rules');?>)</span></td>
         <td>
diff --git a/app/View/Users/admin_index.ctp b/app/View/Users/admin_index.ctp
index c889161dcdc..7732ea2b0c4 100755
--- a/app/View/Users/admin_index.ctp
+++ b/app/View/Users/admin_index.ctp
@@ -1,7 +1,27 @@
 <div class="users index">
     <h2><?php echo __('Users');?></h2>
+    <?php
+        if ($isSiteAdmin) {
+            echo sprintf(
+                '<span>%s</span>',
+                __(
+                    'Click %s to reset the API keys of all sync and org admin users in one shot. This will also automatically inform them of their new API keys.',
+                    $this->Form->postLink(
+                        __('here'),
+                        $baseurl . '/users/resetAllSyncAuthKeys',
+                        array(
+                            'title' => __('Reset all sync user API keys'),
+                            'aria-label' => __('Reset all sync user API keys'),
+                            'class' => 'bold'
+                        ),
+                        __('Are you sure you wish to reset the API keys of all users with sync privileges?')
+                    )
+                )
+            );
+        }
+    ?>
     <div class="pagination">
         <ul>
         <?php
             $this->Paginator->options(array(
                 'update' => '.span12',

Vulnerable code:                     'cache' => array('perm_site_admin'),
                    'getSubmodulesStatus' => array('perm_site_admin'),
                    'getSubmoduleQuickUpdateForm' => array('perm_site_admin'),
                    'import' => ('perm_site_admin'),
                    'index' => array('OR' => array('perm_sync', 'perm_admin')),
                    'postTest' => array('perm_sync'),
                    'testConnection' => array('perm_sync'),
        if (!$this->_isSiteAdmin()) {
            if (!$this->userRole['perm_sync'] && !$this->userRole['perm_admin']) {
                $this->redirect(array('controller' => 'events', 'action' => 'index'));
            }
            $this->paginate['conditions'] = array('Server.org_id LIKE' => $this->Auth->user('org_id'));
        }
                    $this->__extralog("change_pw");
                    $this->__extralog("edit", "user", $fieldsResultStr);
            $this->__extralog("delete", $fieldsDescrStr, '');
            $this->__extralog("login");
            $this->__extralog("logout");
    public function resetauthkey($id = null)
            throw new MethodNotAllowedException('Invalid action.');
        $this->User->id = $id;
        if (!$id || !$this->User->exists($id)) {
            throw new MethodNotAllowedException('Invalid user.');
        }
        $user = $this->User->read();
        $oldKey = $this->User->data['User']['authkey'];
        if (!$this->_isSiteAdmin() && !($this->_isAdmin() && $this->Auth->user('org_id') == $this->User->data['User']['org_id']) && ($this->Auth->user('id') != $id)) {
            throw new MethodNotAllowedException('Invalid user.');
        }
        $newkey = $this->User->generateAuthKey();
        $this->User->saveField('authkey', $newkey);
        $this->__extralog(
                'reset_auth_key',
                'Authentication key for user ' . $user['User']['id'] . ' (' . $user['User']['email'] . ')',
                $fieldsResult = 'authkey(' . $oldKey . ') => (' . $newkey . ')'
        );
    private function __extralog($action = null, $description = null, $fieldsResult = null)
    {
        // new data
        $model = 'User';
        $modelId = $this->Auth->user('id');
        if ($action == 'login') {
            $description = "User (" . $this->Auth->user('id') . "): " . $this->data['User']['email'];
        } elseif ($action == 'logout') {
            $description = "User (" . $this->Auth->user('id') . "): " . $this->Auth->user('email');
        } elseif ($action == 'edit') {
            $description = "User (" . $this->User->id . "): " . $this->data['User']['email'];
        } elseif ($action == 'change_pw') {
            $description = "User (" . $this->User->id . "): " . $this->Auth->user('email');
            $fieldsResult = "Password changed.";
        }

        // query
        $this->Log = ClassRegistry::init('Log');
        $this->Log->create();
        $this->Log->save(array(
            'org' => $this->Auth->user('Organisation')['name'],
            'model' => $model,
            'model_id' => $modelId,
            'email' => $this->Auth->user('email'),
            'action' => $action,
            'title' => $description,
            'change' => isset($fieldsResult) ? $fieldsResult : ''));

        // write to syslogd as well
        App::import('Lib', 'SysLog.SysLog');
        $syslog = new SysLog();
        if (isset($fieldsResult) && $fieldsResult) {
            $syslog->write('notice', $description . ' -- ' . $action . ' -- ' . $fieldsResult);
        } else {
            $syslog->write('notice', $description . ' -- ' . $action);
        }
    }

            if ($result === true) {
                $fails[$eventId] = __('Failed (partially?) because of errors: ') . $result;
            $fails[$eventId] = __('failed downloading the event') . ': ' . json_encode($event);
Fixed code: 
    public function resetSyncAuthkeys()
    {
        if (empty($this->args[0])) {
            echo sprintf(
                __("MISP mass sync authkey reset command line tool.\n\nUsage: %sConsole/cake resetSyncAuthkeys [user_id]") . "\n\n",
                APP
            );
            die();
        } else {
            $userId = $this->args[0];
            $user = $this->User->getAuthUser($userId);
            if (empty($user)) {
                echo __('Invalid user.') . "\n\n";
            }
            if (!$user['Role']['perm_site_admin']) {
                echo __('User has to be a site admin.') . "\n\n";
            }
            if (!empty($this->args[1])) {
                $jobId = $this->args[1];
            } else {
                $jobId = false;
            }
            $this->User->resetAllSyncAuthKeys($user, $jobId);
        }
    }
                    'cache' => array(),
                    'getSubmodulesStatus' => array(),
                    'getSubmoduleQuickUpdateForm' => array(),
                    'import' => array(),
                    'index' => array(),
                    'postTest' => array(),
                    'testConnection' => array(),
                    'resetAllSyncAuthKeys' => array(),
    private function __checkLoggedActions($user, $controller, $action)
    {
        $loggedActions = array(
            'servers' => array(
                'index' => array(
                    'role' => array(
                        'NOT' => array(
                            'perm_site_admin'
                        )
                    ),
                    'message' => __('This could be an indication of an attempted privilege escalation on older vulnerable versions of MISP (<2.4.115)')
                )
            )
        );
        foreach ($loggedActions as $k => $v) {
            $loggedActions[$k] = array_change_key_case($v);
        }
        $message = '';
        if (!empty($loggedActions[$controller])) {
            if (!empty($loggedActions[$controller][$action])) {
                $message = $loggedActions[$controller][$action]['message'];
                $hit = false;
                if (empty($loggedActions[$controller][$action]['role'])) {
                    $hit = true;
                } else {
                    $role_req = $loggedActions[$controller][$action]['role'];
                    if (empty($role_req['OR']) && empty($role_req['AND']) && empty($role_req['NOT'])) {
                        $role_req = array('OR' => $role_req);
                    }
                    if (!empty($role_req['NOT'])) {
                        foreach ($role_req['NOT'] as $k => $v) {
                            if (!$user['Role'][$v]) {
                                $hit = true;
                                continue;
                            }
                        }
                    }
                    if (!$hit && !empty($role_req['AND'])) {
                        $subhit = true;
                        foreach ($role_req['AND'] as $k => $v) {
                            $subhit = $subhit && $user['Role'][$v];
                        }
                        if ($subhit) {
                            $hit = true;
                        }
                    }
                    if (!$hit && !empty($role_req['OR'])) {
                        foreach ($role_req['OR'] as $k => $v) {
                            if ($user['Role'][$v]) {
                                $hit = true;
                                continue;
                            }
                        }
                    }
                    if ($hit) {
                        $this->Log = ClassRegistry::init('Log');
                        $this->Log->create();
                        $this->Log->save(array(
                                'org' => 'SYSTEM',
                                'model' => 'User',
                                'model_id' => $user['id'],
                                'email' => $user['email'],
                                'action' => 'security',
                                'user_id' => $user['id'],
                                'title' => __('User triggered security alert by attempting to access /%s/%s. Reason why this endpoint is of interest: %s', $controller, $action, $message),
                        ));
                    }
                }
            }
        }
    }

        $this->__checkLoggedActions($user, $controller, $action);

    public function resetRemoteAuthKey($id)
    {
        if (!$this->request->is('post')) {
            throw new MethodNotAllowedException(__('This endpoint expects POST requests.'));
        }
        $result = $this->Server->resetRemoteAuthkey($id);
        if ($result !== true) {
            if (!$this->_isRest()) {
                $this->Flash->error($result);
                $this->redirect(array('action' => 'index'));
            } else {
                return $this->RestResponse->saveFailResponse('Servers', 'resetRemoteAuthKey', $id, $message, $this->response->type());
            }
        } else {
            $message = __('API key updated.');
            if (!$this->_isRest()) {
                $this->Flash->success($message);
                $this->redirect(array('action' => 'index'));
            } else {
                return $this->RestResponse->saveSuccessResponse('Servers', 'resetRemoteAuthKey', $message, $this->response->type());
            }
        }
    }
                    $this->User->extralog($this->Auth->user(), "change_pw", null, null, $user);
                    $user = $this->User->find('first', array(
                        'recursive' => -1,
                        'conditions' => array('User.id' => $this->id)
                    ));
                    $this->User->extralog($this->Auth->user(), "edit", "user", $fieldsResultStr, $user);
            $this->User->extralog($this->Auth->user(), "delete", $fieldsDescrStr, '');
            $this->User->extralog($this->Auth->user(), "login");
            $this->User->extralog($this->Auth->user(), "logout");
    public function resetauthkey($id = null, $alert = false)
            throw new MethodNotAllowedException(__('Invalid action.'));
        }
        $newkey = $this->User->resetauthkey($this->Auth->user(), $id, $alert);
        if ($newkey === false) {
            throw new MethodNotAllowedException(__('Invalid user.'));
    public function resetAllSyncAuthKeys()
    {
        if (!$this->request->is('post')) {
            throw new MethodNotAllowedException(__('This functionality is only accessible via POST requests.'));
        }
        $results = $this->User->resetAllSyncAuthKeysRouter($this->Auth->user());
        if ($results === true) {
            $message = __('Job initiated.');
        } else {
            $message = __('%s authkeys reset, %s could not be reset.', $results['success'], $results['fails']);
        }
        if (!$this->_isRest()) {
            $this->Flash->info($message);
            $this->redirect($this->referer());
        } else {
            return $this->RestResponse->saveSuccessResponse('User', 'resetAllSyncAuthKeys', false, $this->response->type(), $message);
        }
    }

                            'security',
            if ($result) {
                $fails[$eventId] = __('Failed (partially?) because of validation errors: ') . json_encode($eventModel->validationErrors, true);
        ;
            $fails[$eventId] = __('failed downloading the event');

    public function resetRemoteAuthKey($id)
    {
        $server = $this->find('first', array(
            'recursive' => -1,
            'conditions' => array('Server.id' => $id)
        ));
        if (empty($server)) {
            return __('Invalid server');
        }
        $HttpSocket = $this->setupHttpSocket($server);
        $request = $this->setupSyncRequest($server);
        $uri = $server['Server']['url'] . '/users/resetauthkey/me';
        try {
            $response = $HttpSocket->post($uri, '{}', $request);
        } catch (Exception $e) {
            $this->Log = ClassRegistry::init('Log');
            $this->Log->create();
            $message = 'Could not reset the remote authentication key.';
            $this->Log->save(array(
                    'org' => 'SYSTEM',
                    'model' => 'Server',
                    'model_id' => $id,
                    'email' => 'SYSTEM',
                    'action' => 'error',
                    'user_id' => 0,
                    'title' => 'Error: ' . $message,
            ));
            return $message;
        }
        if ($response->isOk()) {
            try {
                $response = json_decode($response->body, true);
            } catch (Exception $e) {
                $this->Log = ClassRegistry::init('Log');
                $this->Log->create();
                $message = 'Invalid response received from the remote instance.';
                $this->Log->save(array(
                        'org' => 'SYSTEM',
                        'model' => 'Server',
                        'model_id' => $id,
                        'email' => 'SYSTEM',
                        'action' => 'error',
                        'user_id' => 0,
                        'title' => 'Error: ' . $message,
                ));
                return $message;
            }
            if (!empty($response['message'])) {
                $authkey = $response['message'];
            }
            if (substr($authkey, 0, 17) === 'Authkey updated: ') {
                $authkey = substr($authkey, 17, 57);
            }
            $server['Server']['authkey'] = $authkey;
            $this->save($server);
            return true;
        } else {
            return __('Could not reset the remote authentication key.');
        }
    }

    public function resetAllSyncAuthKeysRouter($user, $jobId = false)
    {
        if (Configure::read('MISP.background_jobs')) {
            $job = ClassRegistry::init('Job');
            $job->create();
            $eventModel = ClassRegistry::init('Event');
            $data = array(
                    'worker' => $eventModel->__getPrioWorkerIfPossible(),
                    'job_type' => __('reset_all_sync_api_keys'),
                    'job_input' => __('Reseting all API keys'),
                    'status' => 0,
                    'retries' => 0,
                    'org_id' => $user['org_id'],
                    'org' => $user['Organisation']['name'],
                    'message' => 'Issuing new API keys to all sync users.',
            );
            $job->save($data);
            $jobId = $job->id;
            $process_id = CakeResque::enqueue(
                    'prio',
                    'AdminShell',
                    array('resetSyncAuthkeys', $user['id'], $jobId),
                    true
            );
            $job->saveField('process_id', $process_id);
            return true;
        } else {
            return $this->resetAllSyncAuthKeys($user);
        }
    }

    public function resetAllSyncAuthKeys($user, $jobId = false)
    {
        $affected_users = $this->find('all', array(
            'recursive' => -1,
            'contain' => array('Role'),
            'conditions' => array(
                'OR' => array(
                    'Role.perm_sync' => 1,
                    'Role.perm_admin' => 1
                ),
                'Role.perm_site_admin' => 0
            )
        ));
        $results = array('success' => 0, 'fails' => 0);
        $user_count = count($affected_users);
        if ($jobId) {
            $job = ClassRegistry::init('Job');
            $existingJob = $job->find('first', array(
                'conditions' => array('Job.id' => $jobId),
                'recursive' => -1
            ));
            if (empty($existingJob)) {
                $jobId = false;
            }
        }
        foreach ($affected_users as $k => $affected_user) {
            try {
                $reset_result = $this->resetauthkey($user, $affected_user['User']['id'], true);
                if ($reset_result) {
                    $results['success'] += 1;
                } else {
                    $results['fails'] += 1;
                }
            } catch (Exception $e) {
                $results['fails'] += 1;
            }
            if ($jobId) {
                if ($k % 100 == 0) {
                    $job->id =  $jobId;
                    $job->saveField('progress', 100 * (($k + 1) / count($user_count)));
                    $job->saveField('message', __('Reset in progress - %s/%s.', $k, $user_count));
                }
            }
        }
        if ($jobId) {
            $message = __('%s authkeys reset, %s could not be reset', $results['success'], $results['fails']);
            $job->saveField('progress', 100);
            $job->saveField('message', $message);
            $job->saveField('status', 4);
        }
        return $results;
    }

    public function resetauthkey($user, $id, $alert = false)
    {
        $this->id = $id;
        if (!$id || !$this->exists($id)) {
            return false;
        }
        $updatedUser = $this->read();
        $oldKey = $this->data['User']['authkey'];
        if (empty($user['Role']['perm_site_admin']) && !($user['Role']['perm_admin'] && $user['org_id'] == $updatedUser['User']['org_id']) && ($user['id'] != $id)) {
            return false;
        }
        $newkey = $this->generateAuthKey();
        $this->saveField('authkey', $newkey);
        $this->extralog(
                $user,
                'reset_auth_key',
                sprintf(
                    __('Authentication key for user %s (%s) updated.'),
                    $updatedUser['User']['id'],
                    $updatedUser['User']['email']
                ),
                $fieldsResult = 'authkey(' . $oldKey . ') => (' . $newkey . ')',
                $updatedUser
        );
        if ($alert) {
            $baseurl = Configure::read('MISP.external_baseurl');
            if (empty($baseurl)) {
                $baseurl = Configure::read('MISP.baseurl');
            }
            $body = __(
                "Dear user,\n\nan API key reset has been triggered by an administrator for your user account on %s.\n\nYour new API key is: %s\n\nPlease update your server's sync setup to reflect this change.\n\nWe apologise for the inconvenience.",
                $baseurl,
                $newkey
            );
            $bodyNoEnc = __(
                "Dear user,\n\nan API key reset has been triggered by an administrator for your user account on %s.\n\nYour new API key can be retrieved by logging in using this sync user's account.\n\nPlease update your server's sync setup to reflect this change.\n\nWe apologise for the inconvenience.",
                $baseurl,
                $newkey
            );
            $this->sendEmail(
                $updatedUser,
                $body,
                $bodyNoEnc,
                __('API key reset by administrator')
            );
        }
        return $newkey;
    }

    public function extralog($user, $action = null, $description = null, $fieldsResult = null, $modifiedUser = null)
    {
        // new data
        $model = 'User';
        $modelId = $user['id'];
        if (!empty($modifiedUser)) {
            $modelId = $modifiedUser['User']['id'];
        }
        if ($action == 'login') {
            $description = "User (" . $user['id'] . "): " . $user['email'];
        } elseif ($action == 'logout') {
            $description = "User (" . $user['id'] . "): " . $user['email'];
        } elseif ($action == 'edit') {
            $description = "User (" . $modifiedUser['User']['id'] . "): " . $modifiedUser['User']['email'];
        } elseif ($action == 'change_pw') {
            $description = "User (" . $modifiedUser['User']['id'] . "): " . $modifiedUser['User']['email'];
            $fieldsResult = "Password changed.";
        }

        // query
        $this->Log = ClassRegistry::init('Log');
        $this->Log->create();
        $this->Log->save(array(
            'org' => $user['Organisation']['name'],
            'model' => $model,
            'model_id' => $modelId,
            'email' => $user['email'],
            'action' => $action,
            'title' => $description,
            'change' => isset($fieldsResult) ? $fieldsResult : ''));

        // write to syslogd as well
        App::import('Lib', 'SysLog.SysLog');
        $syslog = new SysLog();
        $syslog->write('notice', $description . ' -- ' . $action . (empty($fieldResult) ? '' : '-- ' . $fieldResult));
    }
            <th><?php echo __('Reset API key');?></th>
        <td id="reset_api_key_<?php echo $server['Server']['id'];?>">
            <?php
                echo $this->Form->postLink(
                    __('Reset'),
                    $baseurl . '/servers/resetRemoteAuthKey/' . $server['Server']['id'],
                    array(
                        'style' => 'line-height:10px; padding: 4px 4px;',
                        'title' => __('Remotely reset API key'),
                        'aria-label' => __('Remotely reset API key'),
                        'class' => 'btn btn-primary'
                    )
                );
            ?>
        </td>
    <?php
        if ($isSiteAdmin) {
            echo sprintf(
                '<span>%s</span>',
                __(
                    'Click %s to reset the API keys of all sync and org admin users in one shot. This will also automatically inform them of their new API keys.',
                    $this->Form->postLink(
                        __('here'),
                        $baseurl . '/users/resetAllSyncAuthKeys',
                        array(
                            'title' => __('Reset all sync user API keys'),
                            'aria-label' => __('Reset all sync user API keys'),
                            'class' => 'bold'
                        ),
                        __('Are you sure you wish to reset the API keys of all users with sync privileges?')
                    )
                )
            );
        }
    ?>