Merge branch 'develop' of github.com:MISP/MISP into develop

pull/6772/head
Alexandre Dulaunoy 2020-12-22 12:53:07 +01:00
commit 1c12de00fa
No known key found for this signature in database
GPG Key ID: 09E2CD4944E6CBCD
38 changed files with 1258 additions and 666 deletions

View File

@ -1,27 +1,4 @@
<?php <?php
/**
* Application level Controller
*
* This file is application-wide controller file. You can put all
* application-wide controller-related methods here.
*
* PHP 5
*
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @package app.Controller
* @since CakePHP(tm) v 0.2.9
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
// TODO GnuPG encryption has issues when keys are expired
App::uses('ConnectionManager', 'Model'); App::uses('ConnectionManager', 'Model');
App::uses('Controller', 'Controller'); App::uses('Controller', 'Controller');
App::uses('File', 'Utility'); App::uses('File', 'Utility');
@ -36,18 +13,17 @@ App::uses('RequestRearrangeTool', 'Tools');
* @package app.Controller * @package app.Controller
* @link http://book.cakephp.org/2.0/en/controllers.html#the-app-controller * @link http://book.cakephp.org/2.0/en/controllers.html#the-app-controller
* *
* @throws ForbiddenException // TODO Exception
* @property ACLComponent $ACL * @property ACLComponent $ACL
* @property RestResponseComponent $RestResponse * @property RestResponseComponent $RestResponse
* @property CRUDComponent $CRUD * @property CRUDComponent $CRUD
* @property IndexFilterComponent $IndexFilter
* @property RateLimitComponent $RateLimit
*/ */
class AppController extends Controller class AppController extends Controller
{ {
public $defaultModel = ''; public $defaultModel = '';
public $debugMode = false; public $helpers = array('OrgImg', 'FontAwesome', 'UserName', 'DataPathCollector');
public $helpers = array('Utility', 'OrgImg', 'FontAwesome', 'UserName', 'DataPathCollector');
private $__queryVersion = '119'; private $__queryVersion = '119';
public $pyMispVersion = '2.4.135'; public $pyMispVersion = '2.4.135';
@ -55,12 +31,11 @@ class AppController extends Controller
public $phprec = '7.4'; public $phprec = '7.4';
public $pythonmin = '3.6'; public $pythonmin = '3.6';
public $pythonrec = '3.7'; public $pythonrec = '3.7';
public $isApiAuthed = false; private $isApiAuthed = false;
public $baseurl = ''; public $baseurl = '';
public $sql_dump = false; public $sql_dump = false;
private $isRest = null;
public $restResponsePayload = null; public $restResponsePayload = null;
// Used for _isAutomation(), a check that returns true if the controller & action combo matches an action that is a non-xml and non-json automation method // Used for _isAutomation(), a check that returns true if the controller & action combo matches an action that is a non-xml and non-json automation method
@ -72,6 +47,8 @@ class AppController extends Controller
); );
protected $_legacyParams = array(); protected $_legacyParams = array();
/** @var array */
public $userRole;
/** @var User */ /** @var User */
public $User; public $User;
@ -114,14 +91,12 @@ class AppController extends Controller
public function beforeFilter() public function beforeFilter()
{ {
$this->Auth->loginRedirect = Configure::read('MISP.baseurl') . '/users/routeafterlogin'; $this->_setupBaseurl();
$this->Auth->loginRedirect = $this->baseurl. '/users/routeafterlogin';
$customLogout = Configure::read('Plugin.CustomAuth_custom_logout'); $customLogout = Configure::read('Plugin.CustomAuth_custom_logout');
if ($customLogout) { $this->Auth->logoutRedirect = $customLogout ?: ($this->baseurl . '/users/login');
$this->Auth->logoutRedirect = $customLogout;
} else {
$this->Auth->logoutRedirect = Configure::read('MISP.baseurl') . '/users/login';
}
$this->__sessionMassage(); $this->__sessionMassage();
if (Configure::read('Security.allow_cors')) { if (Configure::read('Security.allow_cors')) {
// Add CORS headers // Add CORS headers
@ -152,8 +127,8 @@ class AppController extends Controller
$this->sql_dump = intval($this->params['named']['sql']); $this->sql_dump = intval($this->params['named']['sql']);
} }
$this->_setupDatabaseConnection();
$this->_setupDebugMode(); $this->_setupDebugMode();
$this->_setupDatabaseConnection();
$this->set('ajax', $this->request->is('ajax')); $this->set('ajax', $this->request->is('ajax'));
$this->set('queryVersion', $this->__queryVersion); $this->set('queryVersion', $this->__queryVersion);
@ -166,17 +141,19 @@ class AppController extends Controller
Configure::write('Config.language', 'eng'); Configure::write('Config.language', 'eng');
} }
//if fresh installation (salt empty) generate a new salt // For fresh installation (salt empty) generate a new salt
if (!Configure::read('Security.salt')) { if (!Configure::read('Security.salt')) {
$this->loadModel('Server'); $this->loadModel('Server');
$this->Server->serverSettingsSaveValue('Security.salt', $this->User->generateRandomPassword(32)); $this->Server->serverSettingsSaveValue('Security.salt', $this->User->generateRandomPassword(32));
} }
// Check if the instance has a UUID, if not assign one. // Check if the instance has a UUID, if not assign one.
if (!Configure::read('MISP.uuid')) { if (!Configure::read('MISP.uuid')) {
$this->loadModel('Server'); $this->loadModel('Server');
$this->Server->serverSettingsSaveValue('MISP.uuid', CakeText::uuid()); $this->Server->serverSettingsSaveValue('MISP.uuid', CakeText::uuid());
} }
// check if Apache provides kerberos authentication data
// Check if Apache provides kerberos authentication data
$authUserFields = $this->User->describeAuthFields(); $authUserFields = $this->User->describeAuthFields();
$envvar = Configure::read('ApacheSecureAuth.apacheEnv'); $envvar = Configure::read('ApacheSecureAuth.apacheEnv');
if ($envvar && isset($_SERVER[$envvar])) { if ($envvar && isset($_SERVER[$envvar])) {
@ -196,10 +173,9 @@ class AppController extends Controller
} }
Configure::write('CurrentController', $this->params['controller']); Configure::write('CurrentController', $this->params['controller']);
Configure::write('CurrentAction', $this->params['action']); Configure::write('CurrentAction', $this->params['action']);
$versionArray = $this->{$this->modelClass}->checkMISPVersion(); $versionArray = $this->User->checkMISPVersion();
$this->mispVersion = implode('.', array_values($versionArray)); $this->mispVersion = implode('.', array_values($versionArray));
$this->Security->blackHoleCallback = 'blackHole'; $this->Security->blackHoleCallback = 'blackHole';
$this->_setupBaseurl();
// send users away that are using ancient versions of IE // send users away that are using ancient versions of IE
// Make sure to update this if IE 20 comes out :) // Make sure to update this if IE 20 comes out :)
@ -227,232 +203,68 @@ class AppController extends Controller
// REST authentication // REST authentication
if ($this->_isRest() || $this->_isAutomation()) { if ($this->_isRest() || $this->_isAutomation()) {
// disable CSRF for REST access // disable CSRF for REST access
if (array_key_exists('Security', $this->components)) { if (isset($this->components['Security'])) {
$this->Security->csrfCheck = false; $this->Security->csrfCheck = false;
} }
// If enabled, allow passing the API key via a named parameter (for crappy legacy systems only) if ($this->__loginByAuthKey() === false || $this->Auth->user() === null) {
$namedParamAuthkey = false;
if (Configure::read('Security.allow_unsafe_apikey_named_param') && !empty($this->params['named']['apikey'])) {
$namedParamAuthkey = $this->params['named']['apikey'];
}
// Authenticate user with authkey in Authorization HTTP header
if (!empty($_SERVER['HTTP_AUTHORIZATION']) || !empty($namedParamAuthkey)) {
$found_misp_auth_key = false;
$authentication = explode(',', $_SERVER['HTTP_AUTHORIZATION']);
if (!empty($namedParamAuthkey)) {
$authentication[] = $namedParamAuthkey;
}
$user = false;
foreach ($authentication as $auth_key) {
if (preg_match('/^[a-zA-Z0-9]{40}$/', trim($auth_key))) {
$found_misp_auth_key = true;
$temp = $this->checkAuthUser(trim($auth_key));
if ($temp) {
$user['User'] = $temp;
}
}
}
if ($found_misp_auth_key) {
if ($user) {
unset($user['User']['gpgkey']);
unset($user['User']['certif_public']);
// User found in the db, add the user info to the session
if (Configure::read('MISP.log_auth')) {
$this->Log = ClassRegistry::init('Log');
$this->Log->create();
$log = array(
'org' => $user['User']['Organisation']['name'],
'model' => 'User',
'model_id' => $user['User']['id'],
'email' => $user['User']['email'],
'action' => 'auth',
'title' => 'Successful authentication using API key',
'change' => 'HTTP method: ' . $_SERVER['REQUEST_METHOD'] . PHP_EOL . 'Target: ' . $this->here,
);
$this->Log->save($log);
}
$this->Session->renew();
$this->Session->write(AuthComponent::$sessionKey, $user['User']);
$this->isApiAuthed = true;
} else {
// User not authenticated correctly
// reset the session information
$redis = $this->{$this->modelClass}->setupRedis();
if ($redis && !$redis->exists('misp:auth_fail_throttling:' . trim($auth_key))) {
$redis->set('misp:auth_fail_throttling:' . trim($auth_key), 1);
$redis->expire('misp:auth_fail_throttling:' . trim($auth_key), 3600);
$this->Session->destroy();
$this->Log = ClassRegistry::init('Log');
$this->Log->create();
$log = array(
'org' => 'SYSTEM',
'model' => 'User',
'model_id' => 0,
'email' => 'SYSTEM',
'action' => 'auth_fail',
'title' => 'Failed authentication using API key (' . trim($auth_key) . ')',
'change' => null,
);
$this->Log->save($log);
}
throw new ForbiddenException('Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header.');
}
unset($user);
}
}
if ($this->Auth->user() == null) {
throw new ForbiddenException('Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header.'); throw new ForbiddenException('Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header.');
} }
} elseif (!$this->Session->read(AuthComponent::$sessionKey)) { } elseif (!$this->Session->read(AuthComponent::$sessionKey)) {
$this->_loadAuthenticationPlugins(); $this->_loadAuthenticationPlugins();
} }
} }
$this->set('externalAuthUser', $userLoggedIn);
// user must accept terms
//
// grab the base path from our base url for use in the following checks
$base_dir = parse_url($this->baseurl, PHP_URL_PATH);
// if MISP is running out of the web root already, just set this variable to blank so we don't wind up with '//' in the following if statements $user = $this->Auth->user();
if ($base_dir == '/') { if ($user) {
$base_dir = ''; Configure::write('CurrentUserId', $user['id']);
} $this->__logAccess($user);
if ($this->Auth->user()) { // Try to run updates
Configure::write('CurrentUserId', $this->Auth->user('id')); if ($user['Role']['perm_site_admin'] || (Configure::read('MISP.live') && !$this->_isRest())) {
$this->User->setMonitoring($this->Auth->user()); $this->User->runUpdates();
if (Configure::read('MISP.log_user_ips')) { }
$redis = $this->{$this->modelClass}->setupRedis();
if ($redis) { // Put username to response header for webserver or proxy logging
$redis->set('misp:ip_user:' . trim($_SERVER['REMOTE_ADDR']), $this->Auth->user('id')); if (Configure::read('Security.username_in_response_header')) {
$redis->expire('misp:ip_user:' . trim($_SERVER['REMOTE_ADDR']), 60*60*24*30); $headerValue = $user['email'];
$redis->sadd('misp:user_ip:' . $this->Auth->user('id'), trim($_SERVER['REMOTE_ADDR'])); if (isset($user['logged_by_authkey']) && $user['logged_by_authkey']) {
$headerValue .= isset($user['authkey_id']) ? "/API/{$user['authkey_id']}" : '/API/default';
} }
$this->response->header('X-Username', $headerValue);
$this->RestResponse->setHeader('X-Username', $headerValue);
} }
// update script
if ($this->Auth->user('Role')['perm_site_admin'] || (Configure::read('MISP.live') && !$this->_isRest())) { if (!$this->__verifyUser($user)) {
$this->{$this->modelClass}->runUpdates(); $this->_stop(); // just for sure
} }
$user = $this->Auth->user();
if (!isset($user['force_logout']) || $user['force_logout']) { if (isset($user['logged_by_authkey']) && $user['logged_by_authkey'] && !($this->_isRest() || $this->_isAutomation())) {
$this->loadModel('User'); throw new ForbiddenException("When user is authenticated by authkey, just REST request can be processed");
$this->User->id = $this->Auth->user('id');
$this->User->saveField('force_logout', false);
} }
if ($this->Auth->user('disabled')) {
$this->Log = ClassRegistry::init('Log'); // Put token expiration time to response header that can be processed by automation tool
$this->Log->create(); if (isset($user['authkey_expiration']) && $user['authkey_expiration']) {
$log = array( $expiration = date('c', $user['authkey_expiration']);
'org' => $this->Auth->user('Organisation')['name'], $this->response->header('X-Auth-Key-Expiration', $expiration);
'model' => 'User', $this->RestResponse->setHeader('X-Auth-Key-Expiration', $expiration);
'model_id' => $this->Auth->user('id'),
'email' => $this->Auth->user('email'),
'action' => 'auth_fail',
'title' => 'Login attempt by disabled user.',
'change' => null,
);
$this->Log->save($log);
$this->Auth->logout();
if ($this->_isRest()) {
throw new ForbiddenException('Authentication failed. Your user account has been disabled.');
} else {
$this->Flash->error('Your user account has been disabled.', array('key' => 'error'));
$this->_redirectToLogin();
}
} }
$this->set('default_memory_limit', ini_get('memory_limit')); $this->set('default_memory_limit', ini_get('memory_limit'));
if (isset($this->Auth->user('Role')['memory_limit'])) { if (isset($user['Role']['memory_limit']) && $user['Role']['memory_limit'] !== '') {
if ($this->Auth->user('Role')['memory_limit'] !== '') { ini_set('memory_limit', $user['Role']['memory_limit']);
ini_set('memory_limit', $this->Auth->user('Role')['memory_limit']);
}
} }
$this->set('default_max_execution_time', ini_get('max_execution_time')); $this->set('default_max_execution_time', ini_get('max_execution_time'));
if (isset($this->Auth->user('Role')['max_execution_time'])) { if (isset($user['Role']['max_execution_time']) && $user['Role']['max_execution_time'] !== '') {
if ($this->Auth->user('Role')['max_execution_time'] !== '') { ini_set('max_execution_time', $user['Role']['max_execution_time']);
ini_set('max_execution_time', $this->Auth->user('Role')['max_execution_time']);
}
} }
} else {
$pre_auth_actions = array('login', 'register', 'getGpgPublicKey');
if (!empty(Configure::read('Security.email_otp_enabled'))) {
$pre_auth_actions[] = 'email_otp';
}
if ($this->params['controller'] !== 'users' || !in_array($this->params['action'], $pre_auth_actions)) {
if (!$this->request->is('ajax')) {
$this->Session->write('pre_login_requested_url', $this->here);
}
$this->_redirectToLogin();
}
}
// check if MISP is live $this->set('mispVersion', "{$versionArray['major']}.{$versionArray['minor']}.0");
if ($this->Auth->user() && !Configure::read('MISP.live')) {
$role = $this->getActions();
if (!$role['perm_site_admin']) {
$message = Configure::read('MISP.maintenance_message');
if (empty($message)) {
$this->loadModel('Server');
$message = $this->Server->serverSettings['MISP']['maintenance_message']['value'];
}
if (strpos($message, '$email') && Configure::read('MISP.email')) {
$email = Configure::read('MISP.email');
$message = str_replace('$email', $email, $message);
}
$this->Flash->info($message);
$this->Auth->logout();
throw new MethodNotAllowedException($message);//todo this should pb be removed?
} else {
$this->Flash->error(__('Warning: MISP is currently disabled for all users. Enable it in Server Settings (Administration -> Server Settings -> MISP tab -> live). An update might also be in progress, you can see the progress in ') , array('params' => array('url' => $this->baseurl . '/servers/updateProgress/', 'urlName' => __('Update Progress')), 'clear' => 1));
}
}
if ($this->Session->check(AuthComponent::$sessionKey)) {
if ($this->action !== 'checkIfLoggedIn' || $this->request->params['controller'] !== 'users') {
$this->User->id = $this->Auth->user('id');
if (!$this->User->exists()) {
$message = __('Something went wrong. Your user account that you are authenticated with doesn\'t exist anymore.');
if ($this->_isRest) {
echo $this->RestResponse->throwException(
401,
$message
);
} else {
$this->Flash->info($message);
}
$this->Auth->logout();
$this->_redirectToLogin();
}
if (!empty(Configure::read('MISP.terms_file')) && !$this->Auth->user('termsaccepted') && (!in_array($this->request->here, array($base_dir.'/users/terms', $base_dir.'/users/logout', $base_dir.'/users/login', $base_dir.'/users/downloadTerms')))) {
//if ($this->_isRest()) throw new MethodNotAllowedException('You have not accepted the terms of use yet, please log in via the web interface and accept them.');
if (!$this->_isRest()) {
$this->redirect(array('controller' => 'users', 'action' => 'terms', 'admin' => false));
}
} elseif ($this->Auth->user('change_pw') && (!in_array($this->request->here, array($base_dir.'/users/terms', $base_dir.'/users/change_pw', $base_dir.'/users/logout', $base_dir.'/users/login')))) {
//if ($this->_isRest()) throw new MethodNotAllowedException('Your user account is expecting a password change, please log in via the web interface and change it before proceeding.');
if (!$this->_isRest()) {
$this->redirect(array('controller' => 'users', 'action' => 'change_pw', 'admin' => false));
}
} elseif (!$this->_isRest() && !($this->params['controller'] == 'news' && $this->params['action'] == 'index') && (!in_array($this->request->here, array($base_dir.'/users/terms', $base_dir.'/users/change_pw', $base_dir.'/users/logout', $base_dir.'/users/login')))) {
$newsread = $this->User->field('newsread', array('User.id' => $this->Auth->user('id')));
$this->loadModel('News');
$latest_news = $this->News->field('date_created', array(), 'date_created DESC');
if ($latest_news && $newsread < $latest_news) {
$this->redirect(array('controller' => 'news', 'action' => 'index', 'admin' => false));
}
}
}
}
unset($base_dir);
// We don't want to run these role checks before the user is logged in, but we want them available for every view once the user is logged on
// instead of using checkAction(), like we normally do from controllers when trying to find out about a permission flag, we can use getActions()
// getActions returns all the flags in a single SQL query
if ($this->Auth->user()) {
$this->set('mispVersion', implode('.', array($versionArray['major'], $versionArray['minor'], 0)));
$this->set('mispVersionFull', $this->mispVersion); $this->set('mispVersionFull', $this->mispVersion);
$role = $this->getActions(); $this->set('me', $user);
$this->set('me', $this->Auth->user()); $role = $user['Role'];
$this->set('isAdmin', $role['perm_admin']); $this->set('isAdmin', $role['perm_admin']);
$this->set('isSiteAdmin', $role['perm_site_admin']); $this->set('isSiteAdmin', $role['perm_site_admin']);
$this->set('hostOrgUser', $this->Auth->user('org_id') == Configure::read('MISP.host_org_id')); $this->set('hostOrgUser', $user['org_id'] == Configure::read('MISP.host_org_id'));
$this->set('isAclAdd', $role['perm_add']); $this->set('isAclAdd', $role['perm_add']);
$this->set('isAclModify', $role['perm_modify']); $this->set('isAclModify', $role['perm_modify']);
$this->set('isAclModifyOrg', $role['perm_modify_org']); $this->set('isAclModifyOrg', $role['perm_modify_org']);
@ -475,48 +287,26 @@ class AppController extends Controller
$this->set('aclComponent', $this->ACL); $this->set('aclComponent', $this->ACL);
$this->userRole = $role; $this->userRole = $role;
$this->set('loggedInUserName', $this->__convertEmailToName($this->Auth->user('email'))); $this->set('loggedInUserName', $this->__convertEmailToName($user['email']));
$this->__accessMonitor($user);
if (
Configure::read('MISP.log_paranoid') ||
!empty(Configure::read('Security.monitored'))
) {
$this->Log = ClassRegistry::init('Log');
$this->Log->create();
$change = 'HTTP method: ' . $_SERVER['REQUEST_METHOD'] . PHP_EOL . 'Target: ' . $this->here;
if (
(
$this->request->is('post') ||
$this->request->is('put')
) &&
(
!empty(Configure::read('MISP.log_paranoid_include_post_body')) ||
!empty(Configure::read('Security.monitored'))
)
) {
$payload = $this->request->input();
if (!empty($payload['_Token'])) {
unset($payload['_Token']);
}
$change .= PHP_EOL . 'Request body: ' . json_encode($payload);
}
$log = array(
'org' => $this->Auth->user('Organisation')['name'],
'model' => 'User',
'model_id' => $this->Auth->user('id'),
'email' => $this->Auth->user('email'),
'action' => 'request',
'title' => 'Paranoid log entry',
'change' => $change,
);
$this->Log->save($log);
}
} else { } else {
$pre_auth_actions = array('login', 'register', 'getGpgPublicKey');
if (!empty(Configure::read('Security.email_otp_enabled'))) {
$pre_auth_actions[] = 'email_otp';
}
if (!$this->_isControllerAction(['users' => $pre_auth_actions])) {
if (!$this->request->is('ajax')) {
$this->Session->write('pre_login_requested_url', $this->here);
}
$this->_redirectToLogin();
}
$this->set('me', false); $this->set('me', false);
} }
if ($this->Auth->user() && $this->_isSiteAdmin()) { if ($this->Auth->user() && $this->_isSiteAdmin()) {
if (Configure::read('Session.defaults') == 'database') { if (Configure::read('Session.defaults') === 'database') {
$db = ConnectionManager::getDataSource('default'); $db = ConnectionManager::getDataSource('default');
$sqlResult = $db->query('SELECT COUNT(id) AS session_count FROM cake_sessions WHERE expires < ' . time() . ';'); $sqlResult = $db->query('SELECT COUNT(id) AS session_count FROM cake_sessions WHERE expires < ' . time() . ';');
if (isset($sqlResult[0][0]['session_count']) && $sqlResult[0][0]['session_count'] > 1000) { if (isset($sqlResult[0][0]['session_count']) && $sqlResult[0][0]['session_count'] > 1000) {
@ -545,28 +335,326 @@ class AppController extends Controller
} }
} }
} }
$this->components['RestResponse']['sql_dump'] = $this->sql_dump;
// Notifications and homepage is not necessary for AJAX or REST requests // Notifications and homepage is not necessary for AJAX or REST requests
if ($this->Auth->user() && !$this->_isRest() && !$this->request->is('ajax')) { if ($this->Auth->user() && !$this->_isRest() && !$this->request->is('ajax')) {
if ($this->request->params['controller'] === 'users' && $this->request->params['action'] === 'dashboard') { if ($this->request->params['controller'] === 'users' && $this->request->params['action'] === 'dashboard') {
$notifications = $this->{$this->modelClass}->populateNotifications($this->Auth->user()); $notifications = $this->User->populateNotifications($this->Auth->user());
} else { } else {
$notifications = $this->{$this->modelClass}->populateNotifications($this->Auth->user(), 'fast'); $notifications = $this->User->populateNotifications($this->Auth->user(), 'fast');
} }
$this->set('notifications', $notifications); $this->set('notifications', $notifications);
$this->loadModel('UserSetting'); $homepage = $this->User->UserSetting->getValueForUser($this->Auth->user('id'), 'homepage');
$homepage = $this->UserSetting->find('first', array(
'recursive' => -1,
'conditions' => array(
'UserSetting.user_id' => $this->Auth->user('id'),
'UserSetting.setting' => 'homepage'
),
'contain' => array('User.id', 'User.org_id')
));
if (!empty($homepage)) { if (!empty($homepage)) {
$this->set('homepage', $homepage['UserSetting']['value']); $this->set('homepage', $homepage);
}
}
}
/**
* @return null|bool True if authkey was correct, False if incorrect and Null if not provided
* @throws Exception
*/
private function __loginByAuthKey()
{
if (Configure::read('Security.authkey_keep_session') && $this->Auth->user()) {
// Do not check authkey if session is establish and correct, just close session to allow multiple requests
session_write_close();
return true;
}
// If enabled, allow passing the API key via a named parameter (for crappy legacy systems only)
$namedParamAuthkey = false;
if (Configure::read('Security.allow_unsafe_apikey_named_param') && !empty($this->params['named']['apikey'])) {
$namedParamAuthkey = $this->params['named']['apikey'];
}
// Authenticate user with authkey in Authorization HTTP header
if (!empty($_SERVER['HTTP_AUTHORIZATION']) || !empty($namedParamAuthkey)) {
$foundMispAuthKey = false;
$authentication = explode(',', $_SERVER['HTTP_AUTHORIZATION']);
if (!empty($namedParamAuthkey)) {
$authentication[] = $namedParamAuthkey;
}
$user = false;
foreach ($authentication as $authKey) {
$authKey = trim($authKey);
if (preg_match('/^[a-zA-Z0-9]{40}$/', $authKey)) {
$foundMispAuthKey = true;
$temp = $this->checkAuthUser($authKey);
if ($temp) {
$user = $temp;
break;
}
}
}
if ($foundMispAuthKey) {
$authKeyToStore = substr($authKey, 0, 4)
. str_repeat('*', 32)
. substr($authKey, -4);
if ($user) {
unset($user['gpgkey']);
unset($user['certif_public']);
// User found in the db, add the user info to the session
if (Configure::read('MISP.log_auth')) {
$this->loadModel('Log');
$this->Log->create();
$log = array(
'org' => $user['Organisation']['name'],
'model' => 'User',
'model_id' => $user['id'],
'email' => $user['email'],
'action' => 'auth',
'title' => "Successful authentication using API key ($authKeyToStore)",
'change' => 'HTTP method: ' . $_SERVER['REQUEST_METHOD'] . PHP_EOL . 'Target: ' . $this->here,
);
$this->Log->save($log);
}
$this->Session->renew();
$this->Session->write(AuthComponent::$sessionKey, $user);
$this->isApiAuthed = true;
return true;
} else {
// User not authenticated correctly
// reset the session information
$redis = $this->User->setupRedis();
// Do not log every fail, but just once per hour
if ($redis && !$redis->exists('misp:auth_fail_throttling:' . $authKeyToStore)) {
$redis->setex('misp:auth_fail_throttling:' . $authKeyToStore, 3600, 1);
$this->loadModel('Log');
$this->Log->create();
$log = array(
'org' => 'SYSTEM',
'model' => 'User',
'model_id' => 0,
'email' => 'SYSTEM',
'action' => 'auth_fail',
'title' => "Failed authentication using API key ($authKeyToStore)",
'change' => null,
);
$this->Log->save($log);
}
$this->Session->destroy();
}
}
return false;
}
return null;
}
/**
* Check if:
* - user exists in database
* - is not disabled
* - need to force logout
* - accepted terms and conditions
* - must change password
* - reads latest news
*
* @param array $user
* @return bool
*/
private function __verifyUser(array $user)
{
// Skip these checks for 'checkIfLoggedIn' action to make that call fast
if ($this->_isControllerAction(['users' => ['checkIfLoggedIn']])) {
return true;
}
// Load last user profile modification from database
$userFromDb = $this->User->find('first', [
'conditions' => ['id' => $user['id']],
'recursive' => -1,
'fields' => ['date_modified'],
]);
// Check if user with given ID exists
if (!$userFromDb) {
$message = __('Something went wrong. Your user account that you are authenticated with doesn\'t exist anymore.');
if ($this->_isRest()) {
// TODO: Why not exception?
$response = $this->RestResponse->throwException(401, $message);
$response->send();
$this->_stop();
} else {
$this->Flash->info($message);
$this->Auth->logout();
$this->_redirectToLogin();
}
return false;
}
// Check if session data contain latest changes from db
if ((int)$user['date_modified'] < (int)$userFromDb['User']['date_modified']) {
$user = $this->_refreshAuth(); // session data are old, reload from database
}
// Check if MISP access is enabled
if (!Configure::read('MISP.live')) {
if (!$user['Role']['perm_site_admin']) {
$message = Configure::read('MISP.maintenance_message');
if (empty($message)) {
$this->loadModel('Server');
$message = $this->Server->serverSettings['MISP']['maintenance_message']['value'];
}
if (strpos($message, '$email') && Configure::read('MISP.email')) {
$email = Configure::read('MISP.email');
$message = str_replace('$email', $email, $message);
}
$this->Flash->info($message);
$this->Auth->logout();
throw new MethodNotAllowedException($message);//todo this should pb be removed?
} else {
$this->Flash->error(__('Warning: MISP is currently disabled for all users. Enable it in Server Settings (Administration -> Server Settings -> MISP tab -> live). An update might also be in progress, you can see the progress in ') , array('params' => array('url' => $this->baseurl . '/servers/updateProgress/', 'urlName' => __('Update Progress')), 'clear' => 1));
}
}
// Force logout doesn't make sense for API key authentication
if (!$this->isApiAuthed && $user['force_logout']) {
$this->User->id = $user['id'];
$this->User->saveField('force_logout', false);
$this->Auth->logout();
$this->_redirectToLogin();
return false;
}
if ($user['disabled']) {
$this->Log = ClassRegistry::init('Log');
$this->Log->createLogEntry($user, 'auth_fail', 'User', $user['id'], 'Login attempt by disabled user.');
$this->Auth->logout();
if ($this->_isRest()) {
throw new ForbiddenException('Authentication failed. Your user account has been disabled.');
} else {
$this->Flash->error(__('Your user account has been disabled.'));
$this->_redirectToLogin();
}
return false;
}
// Check if auth key is not expired. Make sense when Security.authkey_keep_session is enabled.
if (isset($user['authkey_expiration']) && $user['authkey_expiration']) {
$time = isset($_SERVER['REQUEST_TIME']) ? $_SERVER['REQUEST_TIME'] : time();
if ($user['authkey_expiration'] < $time) {
$this->Auth->logout();
throw new ForbiddenException('Auth key is expired');
}
}
$isUserRequest = !$this->_isRest() && !$this->request->is('ajax') && !$this->_isAutomation();
// Next checks makes sense just for user direct HTTP request, so skip REST and AJAX calls
if (!$isUserRequest) {
return true;
}
// Check if user accepted terms and conditions
if (!$user['termsaccepted'] && !empty(Configure::read('MISP.terms_file')) && !$this->_isControllerAction(['users' => ['terms', 'logout', 'login', 'downloadTerms']])) {
//if ($this->_isRest()) throw new MethodNotAllowedException('You have not accepted the terms of use yet, please log in via the web interface and accept them.');
$this->redirect(array('controller' => 'users', 'action' => 'terms', 'admin' => false));
return false;
}
// Check if user must change password
if ($user['change_pw'] && !$this->_isControllerAction(['users' => ['terms', 'change_pw', 'logout', 'login']])) {
//if ($this->_isRest()) throw new MethodNotAllowedException('Your user account is expecting a password change, please log in via the web interface and change it before proceeding.');
$this->redirect(array('controller' => 'users', 'action' => 'change_pw', 'admin' => false));
return false;
}
// Check if user must read news
if (!$this->_isControllerAction(['news' => ['index'], 'users' => ['terms', 'change_pw', 'login', 'logout']])) {
$this->loadModel('News');
$latestNewsCreated = $this->News->field('date_created', array(), 'date_created DESC');
if ($latestNewsCreated && $user['newsread'] < $latestNewsCreated) {
$this->redirect(array('controller' => 'news', 'action' => 'index', 'admin' => false));
return false;
}
}
return true;
}
/**
* @param array $actionsToCheck
* @return bool
*/
private function _isControllerAction($actionsToCheck = [])
{
$controller = Inflector::variable($this->request->params['controller']);
if (!isset($actionsToCheck[$controller])) {
return false;
}
return in_array($this->action, $actionsToCheck[$controller], true);
}
/**
* User access monitoring
* @param array $user
*/
private function __logAccess(array $user)
{
$logUserIps = Configure::read('MISP.log_user_ips');
if (!$logUserIps) {
return;
}
$redis = $this->User->setupRedis();
if (!$redis) {
return;
}
$remoteAddress = trim($_SERVER['REMOTE_ADDR']);
$pipe = $redis->multi(Redis::PIPELINE);
// keep for 30 days
$pipe->setex('misp:ip_user:' . $remoteAddress, 60 * 60 * 24 * 30, $user['id']);
$pipe->sadd('misp:user_ip:' . $user['id'], $remoteAddress);
// Log key usage if enabled
if (isset($user['authkey_id']) && Configure::read('MISP.log_user_ips_authkeys')) {
// Use request time if defined
$time = isset($_SERVER['REQUEST_TIME']) ? $_SERVER['REQUEST_TIME'] : time();
$hashKey = date("Y-m-d", $time) . ":$remoteAddress";
$pipe->hIncrBy("misp:authkey_usage:{$user['authkey_id']}", $hashKey, 1);
// delete after one year of inactivity
$pipe->expire("misp:authkey_usage:{$user['authkey_id']}", 3600 * 24 * 365);
$pipe->set("misp:authkey_last_usage:{$user['authkey_id']}", $time);
}
$pipe->exec();
}
/**
* @param array $user
* @throws Exception
*/
private function __accessMonitor(array $user)
{
$userMonitoringEnabled = Configure::read('Security.user_monitoring_enabled');
if ($userMonitoringEnabled) {
$redis = $this->User->setupRedis();
$userMonitoringEnabled = $redis && $redis->sismember('misp:monitored_users', $user['id']);
}
if (Configure::read('MISP.log_paranoid') || $userMonitoringEnabled) {
$change = 'HTTP method: ' . $_SERVER['REQUEST_METHOD'] . PHP_EOL . 'Target: ' . $this->here;
if (
(
$this->request->is('post') ||
$this->request->is('put')
) &&
(
!empty(Configure::read('MISP.log_paranoid_include_post_body')) ||
$userMonitoringEnabled
)
) {
$payload = $this->request->input();
$change .= PHP_EOL . 'Request body: ' . $payload;
}
$this->Log = ClassRegistry::init('Log');
try {
$this->Log->createLogEntry($user, 'request', 'User', $user['id'], 'Paranoid log entry', $change);
} catch (Exception $e) {
// When `MISP.log_skip_db_logs_completely` is enabled, Log::createLogEntry method throws exception
} }
} }
} }
@ -601,7 +689,7 @@ class AppController extends Controller
public function afterFilter() public function afterFilter()
{ {
if ($this->isApiAuthed && $this->_isRest() && $this->Session->started()) { if ($this->isApiAuthed && $this->_isRest() && !Configure::read('Security.authkey_keep_session')) {
$this->Session->destroy(); $this->Session->destroy();
} }
} }
@ -648,16 +736,17 @@ class AppController extends Controller
/* /*
* Sanitize the configured `MISP.baseurl` and expose it to the view as `baseurl`. * Sanitize the configured `MISP.baseurl` and expose it to the view as `baseurl`.
*/ */
protected function _setupBaseurl() { protected function _setupBaseurl()
{
// Let us access $baseurl from all views // Let us access $baseurl from all views
$baseurl = Configure::read('MISP.baseurl'); $baseurl = Configure::read('MISP.baseurl');
if (substr($baseurl, -1) == '/') { if (substr($baseurl, -1) === '/') {
// if the baseurl has a trailing slash, remove it. It can lead to issues with the CSRF protection // if the baseurl has a trailing slash, remove it. It can lead to issues with the CSRF protection
$baseurl = rtrim($baseurl, '/'); $baseurl = rtrim($baseurl, '/');
$this->loadModel('Server'); $this->loadModel('Server');
$this->Server->serverSettingsSaveValue('MISP.baseurl', $baseurl); $this->Server->serverSettingsSaveValue('MISP.baseurl', $baseurl);
} }
if (trim($baseurl) == 'http://') { if (trim($baseurl) === 'http://') {
$this->Server->serverSettingsSaveValue('MISP.baseurl', ''); $this->Server->serverSettingsSaveValue('MISP.baseurl', '');
} }
$this->baseurl = $baseurl; $this->baseurl = $baseurl;
@ -683,8 +772,6 @@ class AppController extends Controller
throw new BadRequestException('The request has been black-holed'); throw new BadRequestException('The request has been black-holed');
} }
public $userRole = null;
protected function _isRest() protected function _isRest()
{ {
return $this->IndexFilter->isRest(); return $this->IndexFilter->isRest();
@ -692,12 +779,7 @@ class AppController extends Controller
protected function _isAutomation() protected function _isAutomation()
{ {
foreach ($this->automationArray as $controllerName => $controllerActions) { return $this->IndexFilter->isApiFunction($this->params['controller'], $this->params['action']);
if ($this->params['controller'] == $controllerName && in_array($this->params['action'], $controllerActions)) {
return true;
}
}
return false;
} }
/** /**
@ -829,34 +911,12 @@ class AppController extends Controller
return $data; return $data;
} }
// pass an action to this method for it to check the active user's access to the action
public function checkAction($action = 'perm_sync')
{
$this->loadModel('Role');
$this->Role->recursive = -1;
$role = $this->Role->findById($this->Auth->user('role_id'));
if ($role['Role'][$action]) {
return true;
}
return false;
}
// returns the role of the currently authenticated user as an array, used to set the permission variables for views in the AppController's beforeFilter() method
public function getActions()
{
$this->loadModel('Role');
$this->Role->recursive = -1;
$role = $this->Role->findById($this->Auth->user('role_id'));
return $role['Role'];
}
public function checkAuthUser($authkey) public function checkAuthUser($authkey)
{ {
if (Configure::read('Security.advanced_authkeys')) { if (Configure::read('Security.advanced_authkeys')) {
$this->loadModel('AuthKey'); $this->loadModel('AuthKey');
$user = $this->AuthKey->getAuthUserByAuthKey($authkey); $user = $this->AuthKey->getAuthUserByAuthKey($authkey);
} else { } else {
$this->loadModel('User');
$user = $this->User->getAuthUserByAuthKey($authkey); $user = $this->User->getAuthUserByAuthKey($authkey);
} }
@ -866,22 +926,16 @@ class AppController extends Controller
if (!$user['Role']['perm_auth']) { if (!$user['Role']['perm_auth']) {
return false; return false;
} }
if ($user['Role']['perm_site_admin']) { $user['logged_by_authkey'] = true;
$user['siteadmin'] = true;
}
return $user; return $user;
} }
public function checkExternalAuthUser($authkey) public function checkExternalAuthUser($authkey)
{ {
$this->loadModel('User');
$user = $this->User->getAuthUserByExternalAuth($authkey); $user = $this->User->getAuthUserByExternalAuth($authkey);
if (empty($user)) { if (empty($user)) {
return false; return false;
} }
if ($user['Role']['perm_site_admin']) {
$user['siteadmin'] = true;
}
return $user; return $user;
} }
@ -1270,7 +1324,7 @@ class AppController extends Controller
$final = $this->$scope->restSearch($user, $returnFormat, $filters, false, false, $elementCounter, $renderView); $final = $this->$scope->restSearch($user, $returnFormat, $filters, false, false, $elementCounter, $renderView);
if (!empty($renderView) && !empty($final)) { if (!empty($renderView) && !empty($final)) {
$this->layout = false; $this->layout = false;
$final = json_decode($final, true); $final = json_decode($final->intoString(), true);
foreach ($final as $key => $data) { foreach ($final as $key => $data) {
$this->set($key, $data); $this->set($key, $data);
} }
@ -1331,4 +1385,30 @@ class AppController extends Controller
} }
return false; return false;
} }
/**
* Refresh user data in session, but keep information about authkey.
* @return array User data in auth format
*/
protected function _refreshAuth()
{
$sessionUser = $this->Auth->user();
$user = $this->User->getAuthUser($sessionUser['id']);
if (!$user) {
throw new RuntimeException("User with ID {$sessionUser['id']} not exists.");
}
if (isset($sessionUser['authkey_id'])) {
$this->loadModel('AuthKey');
if (!$this->AuthKey->exists($sessionUser['authkey_id'])) {
throw new RuntimeException("Auth key with ID {$sessionUser['authkey_id']} not exists.");
}
}
foreach (['authkey_id', 'authkey_expiration', 'logged_by_authkey'] as $copy) {
if (isset($sessionUser[$copy])) {
$user[$copy] = $sessionUser[$copy];
}
}
$this->Auth->login($user);
return $user;
}
} }

View File

@ -1,6 +1,9 @@
<?php <?php
App::uses('AppController', 'Controller'); App::uses('AppController', 'Controller');
/**
* @property AuthKey $AuthKey
*/
class AuthKeysController extends AppController class AuthKeysController extends AppController
{ {
public $components = array( public $components = array(
@ -23,13 +26,22 @@ class AuthKeysController extends AppController
$this->set('user_id', $id); $this->set('user_id', $id);
$conditions['AND'][] = ['AuthKey.user_id' => $id]; $conditions['AND'][] = ['AuthKey.user_id' => $id];
} }
$keyUsageEnabled = Configure::read('MISP.log_user_ips') && Configure::read('MISP.log_user_ips_authkeys');
$this->CRUD->index([ $this->CRUD->index([
'filters' => ['User.username', 'authkey', 'comment', 'User.id'], 'filters' => ['User.email', 'authkey_start', 'authkey_end', 'comment', 'User.id'],
'quickFilters' => ['authkey', 'comment'], 'quickFilters' => ['comment', 'authkey_start', 'authkey_end', 'User.email'],
'contain' => ['User'], 'contain' => ['User.id', 'User.email'],
'conditions' => $conditions, 'conditions' => $conditions,
'afterFind' => function (array $authKeys) { 'afterFind' => function (array $authKeys) use ($keyUsageEnabled) {
if ($keyUsageEnabled) {
$keyIds = Hash::extract($authKeys, "{n}.AuthKey.id");
$lastUsedById = $this->AuthKey->getLastUsageForKeys($keyIds);
}
foreach ($authKeys as &$authKey) { foreach ($authKeys as &$authKey) {
if ($keyUsageEnabled) {
$lastUsed = $lastUsedById[$authKey['AuthKey']['id']];
$authKey['AuthKey']['last_used'] = $lastUsed;
}
unset($authKey['AuthKey']['authkey']); unset($authKey['AuthKey']['authkey']);
} }
return $authKeys; return $authKeys;
@ -38,8 +50,12 @@ class AuthKeysController extends AppController
if ($this->IndexFilter->isRest()) { if ($this->IndexFilter->isRest()) {
return $this->restResponsePayload; return $this->restResponsePayload;
} }
$this->set('metaGroup', $this->_isAdmin ? 'admin' : 'globalActions'); $this->set('title_for_layout', __('Auth Keys'));
$this->set('metaAction', 'authkeys_index'); $this->set('keyUsageEnabled', $keyUsageEnabled);
$this->set('menuData', [
'menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions',
'menuItem' => 'authkeys_index',
]);
} }
public function delete($id) public function delete($id)
@ -61,18 +77,22 @@ class AuthKeysController extends AppController
public function add($user_id = false) public function add($user_id = false)
{ {
$this->set('menuData', array('menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions', 'menuItem' => 'authKeyAdd'));
$params = [ $params = [
'displayOnSuccess' => 'authkey_display', 'displayOnSuccess' => 'authkey_display',
'saveModelVariable' => ['authkey_raw'] 'saveModelVariable' => ['authkey_raw'],
'override' => ['authkey' => null], // do not allow to use own key, always generate random one
'afterFind' => function ($authKey) { // remove hashed key from response
unset($authKey['AuthKey']['authkey']);
return $authKey;
}
]; ];
$selectConditions = []; $selectConditions = [];
if (!$this->_isSiteAdmin()) { if (!$this->_isSiteAdmin()) {
$selectConditions['AND'][] = ['User.id' => $this->Auth->user('id')]; $selectConditions['AND'][] = ['User.id' => $this->Auth->user('id')];
$params['override'] = ['user_id' => $this->Auth->user('id')]; $params['override']['user_id'] = $this->Auth->user('id');
} else if ($user_id) { } else if ($user_id) {
$selectConditions['AND'][] = ['User.id' => $user_id]; $selectConditions['AND'][] = ['User.id' => $user_id];
$params['override'] = ['user_id' => $user_id]; $params['override']['user_id'] = $user_id;
} }
$this->CRUD->add($params); $this->CRUD->add($params);
if ($this->IndexFilter->isRest()) { if ($this->IndexFilter->isRest()) {
@ -86,6 +106,11 @@ class AuthKeysController extends AppController
]) ])
]; ];
$this->set(compact('dropdownData')); $this->set(compact('dropdownData'));
$this->set('menuData', [
'menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions',
'menuItem' => 'authKeyAdd',
]);
$this->set('validity', Configure::read('Security.advanced_authkeys_validity'));
} }
public function view($id = false) public function view($id = false)
@ -101,6 +126,15 @@ class AuthKeysController extends AppController
if ($this->IndexFilter->isRest()) { if ($this->IndexFilter->isRest()) {
return $this->restResponsePayload; return $this->restResponsePayload;
} }
if (Configure::read('MISP.log_user_ips') && Configure::read('MISP.log_user_ips_authkeys')) {
list($keyUsage, $lastUsed, $uniqueIps) = $this->AuthKey->getKeyUsage($id);
$this->set('keyUsage', $keyUsage);
$this->set('lastUsed', $lastUsed);
$this->set('uniqueIps', $uniqueIps);
}
$this->set('title_for_layout', __('Auth Key'));
$this->set('menuData', [ $this->set('menuData', [
'menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions', 'menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions',
'menuItem' => 'authKeyView', 'menuItem' => 'authKeyView',

View File

@ -15,13 +15,11 @@ class ACLComponent extends Component
private $__aclList = array( private $__aclList = array(
'*' => array( '*' => array(
'blackhole' => array(), 'blackhole' => array(),
'checkAction' => array(),
'checkAuthUser' => array(), 'checkAuthUser' => array(),
'checkExternalAuthUser' => array(), 'checkExternalAuthUser' => array(),
'cleanModelCaches' => array(), 'cleanModelCaches' => array(),
'debugACL' => array(), 'debugACL' => array(),
'generateCount' => array(), 'generateCount' => array(),
'getActions' => array(),
'pruneDuplicateUUIDs' => array(), 'pruneDuplicateUUIDs' => array(),
'queryACL' => array(), 'queryACL' => array(),
'removeDuplicateEvents' => array(), 'removeDuplicateEvents' => array(),

View File

@ -15,7 +15,7 @@ class CRUDComponent extends Component
} }
} }
public function index($options) public function index(array $options)
{ {
$this->prepareResponse(); $this->prepareResponse();
if (!empty($options['quickFilters'])) { if (!empty($options['quickFilters'])) {
@ -75,8 +75,6 @@ class CRUDComponent extends Component
$input[$modelName][$field] = $value; $input[$modelName][$field] = $value;
} }
} }
if (isset($input[$modelName]['id'])) {
}
unset($input[$modelName]['id']); unset($input[$modelName]['id']);
if (!empty($params['fields'])) { if (!empty($params['fields'])) {
$data = []; $data = [];
@ -86,20 +84,25 @@ class CRUDComponent extends Component
} else { } else {
$data = $input; $data = $input;
} }
if ($this->Controller->{$modelName}->save($data)) { /** @var Model $model */
$data = $this->Controller->{$modelName}->find('first', [ $model = $this->Controller->{$modelName};
if ($model->save($data)) {
$data = $model->find('first', [
'recursive' => -1, 'recursive' => -1,
'conditions' => [ 'conditions' => [
'id' => $this->Controller->{$modelName}->id 'id' => $model->id
] ]
]); ]);
if (!empty($params['saveModelVariable'])) { if (!empty($params['saveModelVariable'])) {
foreach ($params['saveModelVariable'] as $var) { foreach ($params['saveModelVariable'] as $var) {
if (isset($this->Controller->{$modelName}->$var)) { if (isset($model->$var)) {
$data[$modelName][$var] = $this->Controller->{$modelName}->$var; $data[$modelName][$var] = $model->$var;
} }
} }
} }
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data);
}
$message = __('%s added.', $modelName); $message = __('%s added.', $modelName);
if ($this->Controller->IndexFilter->isRest()) { if ($this->Controller->IndexFilter->isRest()) {
$this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData($data, 'json'); $this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData($data, 'json');
@ -116,7 +119,9 @@ class CRUDComponent extends Component
} else { } else {
$message = __('%s could not be added.', $modelName); $message = __('%s could not be added.', $modelName);
if ($this->Controller->IndexFilter->isRest()) { if ($this->Controller->IndexFilter->isRest()) {
$controllerName = $this->Controller->params['controller'];
$actionName = $this->Controller->params['action'];
$this->Controller->restResponsePayload = $this->Controller->RestResponse->saveFailResponse($controllerName, $actionName, false, $model->validationErrors, 'json');
} else { } else {
$this->Controller->Flash->error($message); $this->Controller->Flash->error($message);
} }
@ -246,24 +251,25 @@ class CRUDComponent extends Component
$this->Controller->render('/genericTemplates/delete'); $this->Controller->render('/genericTemplates/delete');
} }
protected function setQuickFilters($params, array $query, $quickFilterFields)
protected function setQuickFilters($params, $query, $quickFilterFields)
{ {
$queryConditions = [];
if (!empty($params['quickFilter']) && !empty($quickFilterFields)) { if (!empty($params['quickFilter']) && !empty($quickFilterFields)) {
$queryConditions = [];
$filter = '%' . strtolower($params['quickFilter']) . '%';
foreach ($quickFilterFields as $filterField) { foreach ($quickFilterFields as $filterField) {
$queryConditions[$filterField] = $params['quickFilter']; $queryConditions["LOWER($filterField) LIKE"] = $filter;
} }
$query['conditions']['OR'][] = $queryConditions; $query['conditions']['OR'] = $queryConditions;
} }
return $query; return $query;
} }
protected function setFilters($params, $query) protected function setFilters(array $params, array $query)
{ {
$params = $this->massageFilters($params); // For CakePHP 2, we don't need to distinguish between simpleFilters and relatedFilters
if (!empty($params['simpleFilters'])) { //$params = $this->massageFilters($params);
foreach ($params['simpleFilters'] as $filter => $filterValue) { if (!empty($params)) {
foreach ($params as $filter => $filterValue) {
if ($filter === 'quickFilter') { if ($filter === 'quickFilter') {
continue; continue;
} }

View File

@ -6,7 +6,8 @@
class IndexFilterComponent extends Component class IndexFilterComponent extends Component
{ {
public $Controller = false; /** @var Controller */
public $Controller;
public $isRest = null; public $isRest = null;
public function initialize(Controller $controller) { public function initialize(Controller $controller) {
@ -74,7 +75,7 @@ class IndexFilterComponent extends Component
} }
} }
} }
$this->Controller->set('passedArgs', json_encode($this->Controller->passedArgs, true)); $this->Controller->set('passedArgs', json_encode($this->Controller->passedArgs));
return $data; return $data;
} }
@ -85,7 +86,7 @@ class IndexFilterComponent extends Component
return $this->isRest; return $this->isRest;
} }
$api = $this->isApiFunction($this->Controller->request->params['controller'], $this->Controller->request->params['action']); $api = $this->isApiFunction($this->Controller->request->params['controller'], $this->Controller->request->params['action']);
if (isset($this->Controller->RequestHandler) && ($api || $this->Controller->RequestHandler->isXml() || $this->isJson() || $this->isCsv())) { if (isset($this->Controller->RequestHandler) && ($api || $this->isJson() || $this->Controller->RequestHandler->isXml() || $this->isCsv())) {
if ($this->isJson()) { if ($this->isJson()) {
if (!empty($this->Controller->request->input()) && empty($this->Controller->request->input('json_decode'))) { if (!empty($this->Controller->request->input()) && empty($this->Controller->request->input('json_decode'))) {
throw new MethodNotAllowedException('Invalid JSON input. Make sure that the JSON input is a correctly formatted JSON string. This request has been blocked to avoid an unfiltered request.'); throw new MethodNotAllowedException('Invalid JSON input. Make sure that the JSON input is a correctly formatted JSON string. This request has been blocked to avoid an unfiltered request.');
@ -117,12 +118,13 @@ class IndexFilterComponent extends Component
} }
/**
* @param string $controller
* @param string $action
* @return bool
*/
public function isApiFunction($controller, $action) public function isApiFunction($controller, $action)
{ {
if (isset($this->Controller->automationArray[$controller]) && in_array($action, $this->Controller->automationArray[$controller])) { return isset($this->Controller->automationArray[$controller]) && in_array($action, $this->Controller->automationArray[$controller], true);
return true;
}
return false;
} }
} }

View File

@ -519,11 +519,12 @@ class RestResponseComponent extends Component
} else { } else {
$type = $format; $type = $format;
} }
$dumpSql = !empty($this->Controller->sql_dump) && Configure::read('debug') > 1;
if (!$raw) { if (!$raw) {
if (is_string($response)) { if (is_string($response)) {
$response = array('message' => $response); $response = array('message' => $response);
} }
if (Configure::read('debug') > 1 && !empty($this->Controller->sql_dump)) { if ($dumpSql) {
$this->Log = ClassRegistry::init('Log'); $this->Log = ClassRegistry::init('Log');
if ($this->Controller->sql_dump === 2) { if ($this->Controller->sql_dump === 2) {
$response = array('sql_dump' => $this->Log->getDataSource()->getLog(false, false)); $response = array('sql_dump' => $this->Log->getDataSource()->getLog(false, false));
@ -533,7 +534,7 @@ class RestResponseComponent extends Component
} }
$response = json_encode($response, JSON_PRETTY_PRINT); $response = json_encode($response, JSON_PRETTY_PRINT);
} else { } else {
if (Configure::read('debug') > 1 && !empty($this->Controller->sql_dump)) { if ($dumpSql) {
$this->Log = ClassRegistry::init('Log'); $this->Log = ClassRegistry::init('Log');
if ($this->Controller->sql_dump === 2) { if ($this->Controller->sql_dump === 2) {
$response = json_encode(array('sql_dump' => $this->Log->getDataSource()->getLog(false, false))); $response = json_encode(array('sql_dump' => $this->Log->getDataSource()->getLog(false, false)));
@ -547,7 +548,15 @@ class RestResponseComponent extends Component
} }
} }
} }
$cakeResponse = new CakeResponse(array('body' => $response, 'status' => $code, 'type' => $type));
App::uses('TmpFileTool', 'Tools');
if ($response instanceof TmpFileTool) {
App::uses('CakeResponseTmp', 'Tools');
$cakeResponse = new CakeResponseTmp(['status' => $code, 'type' => $type]);
$cakeResponse->file($response);
} else {
$cakeResponse = new CakeResponse(array('body' => $response, 'status' => $code, 'type' => $type));
}
if (Configure::read('Security.allow_cors')) { if (Configure::read('Security.allow_cors')) {
$headers["Access-Control-Allow-Headers"] = "Origin, Content-Type, Authorization, Accept"; $headers["Access-Control-Allow-Headers"] = "Origin, Content-Type, Authorization, Accept";

View File

@ -17,24 +17,19 @@ class NewsController extends AppController
{ {
$this->paginate['contain'] = array('User' => array('fields' => array('User.email'))); $this->paginate['contain'] = array('User' => array('fields' => array('User.email')));
$newsItems = $this->paginate(); $newsItems = $this->paginate();
$this->loadModel('User');
$currentUser = $this->User->find('first', array( $newsread = $this->Auth->user('newsread');
'recursive' => -1,
'conditions' => array('User.id' => $this->Auth->user('id')),
'fields' => array('User.newsread')
));
foreach ($newsItems as $key => $item) { foreach ($newsItems as $key => $item) {
if ($item['News']['date_created'] > $currentUser['User']['newsread']) { if ($item['News']['date_created'] > $newsread) {
$newsItems[$key]['News']['new'] = true; $newsItems[$key]['News']['new'] = true;
} else { } else {
$newsItems[$key]['News']['new'] = false; $newsItems[$key]['News']['new'] = false;
} }
} }
$this->User->id = $this->Auth->user('id');
//if ($this->User->exists()) {
$this->User->saveField('newsread', time());
$this->set('newsItems', $newsItems); $this->set('newsItems', $newsItems);
//}
$this->loadModel('User');
$this->User->updateField($this->Auth->user(), 'newsread', time());
} }
public function add() public function add()

View File

@ -1448,7 +1448,7 @@ class ServersController extends AppController
if (!isset($this->request->data['Server'])) { if (!isset($this->request->data['Server'])) {
$this->request->data = array('Server' => $this->request->data); $this->request->data = array('Server' => $this->request->data);
} }
if (!isset($this->request->data['Server']['value'])) { if (!isset($this->request->data['Server']['value']) || !is_scalar($this->request->data['Server']['value'])) {
if ($this->_isRest()) { if ($this->_isRest()) {
return $this->RestResponse->saveFailResponse('Servers', 'serverSettingsEdit', false, 'Invalid input. Expected: {"value": "new_setting"}', $this->response->type()); return $this->RestResponse->saveFailResponse('Servers', 'serverSettingsEdit', false, 'Invalid input. Expected: {"value": "new_setting"}', $this->response->type());
} }
@ -1491,7 +1491,7 @@ class ServersController extends AppController
return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => 'Field updated.')), 'status'=>200, 'type' => 'json')); return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => 'Field updated.')), 'status'=>200, 'type' => 'json'));
} }
} else { } else {
if ($this->_isRest) { if ($this->_isRest()) {
return $this->RestResponse->saveFailResponse('Servers', 'serverSettingsEdit', false, $result, $this->response->type()); return $this->RestResponse->saveFailResponse('Servers', 'serverSettingsEdit', false, $result, $this->response->type());
} else { } else {
return new CakeResponse(array('body'=> json_encode(array('saved' => false, 'errors' => $result)), 'status'=>200, 'type' => 'json')); return new CakeResponse(array('body'=> json_encode(array('saved' => false, 'errors' => $result)), 'status'=>200, 'type' => 'json'));

View File

@ -51,14 +51,6 @@ class UsersController extends AppController
if (!$this->_isSiteAdmin() && $this->Auth->user('id') != $id) { if (!$this->_isSiteAdmin() && $this->Auth->user('id') != $id) {
throw new NotFoundException(__('Invalid user or not authorised.')); throw new NotFoundException(__('Invalid user or not authorised.'));
} }
if (!is_numeric($id) && !empty($id)) {
$userId = $this->User->find('first', array(
'conditions' => array('email' => $id),
'fields' => array('id')
));
$id = $userid['User']['id'];
}
$user = $this->User->read(null, $id);
$user = $this->User->find('first', array( $user = $this->User->find('first', array(
'recursive' => -1, 'recursive' => -1,
'conditions' => array('User.id' => $id), 'conditions' => array('User.id' => $id),
@ -182,7 +174,7 @@ class UsersController extends AppController
} }
if (!$abortPost) { if (!$abortPost) {
// What fields should be saved (allowed to be saved) // What fields should be saved (allowed to be saved)
$fieldList = array('autoalert', 'gpgkey', 'certif_public', 'nids_sid', 'contactalert', 'disabled'); $fieldList = array('autoalert', 'gpgkey', 'certif_public', 'nids_sid', 'contactalert', 'disabled', 'date_modified');
if ($this->__canChangeLogin()) { if ($this->__canChangeLogin()) {
$fieldList[] = 'email'; $fieldList[] = 'email';
} }
@ -217,7 +209,6 @@ class UsersController extends AppController
return $this->RestResponse->viewData($this->__massageUserObject($user), $this->response->type()); return $this->RestResponse->viewData($this->__massageUserObject($user), $this->response->type());
} else { } else {
$this->Flash->success(__('The profile has been updated')); $this->Flash->success(__('The profile has been updated'));
$this->_refreshAuth();
$this->redirect(array('action' => 'view', $id)); $this->redirect(array('action' => 'view', $id));
} }
} else { } else {
@ -305,7 +296,6 @@ class UsersController extends AppController
return $this->RestResponse->saveSuccessResponse('User', 'change_pw', false, $this->response->type(), $message); return $this->RestResponse->saveSuccessResponse('User', 'change_pw', false, $this->response->type(), $message);
} }
$this->Flash->success($message); $this->Flash->success($message);
$this->_refreshAuth();
$this->redirect(array('action' => 'view', $id)); $this->redirect(array('action' => 'view', $id));
} else { } else {
$message = __('The password could not be updated. Make sure you meet the minimum password length / complexity requirements.'); $message = __('The password could not be updated. Make sure you meet the minimum password length / complexity requirements.');
@ -915,8 +905,8 @@ class UsersController extends AppController
if (isset($this->request->data['User']['role_id']) && !array_key_exists($this->request->data['User']['role_id'], $syncRoles)) { if (isset($this->request->data['User']['role_id']) && !array_key_exists($this->request->data['User']['role_id'], $syncRoles)) {
$this->request->data['User']['server_id'] = 0; $this->request->data['User']['server_id'] = 0;
} }
$fields = array(); $fields = [];
$blockedFields = array('id', 'invited_by'); $blockedFields = array('id', 'invited_by', 'date_modified');
if (!$this->_isSiteAdmin()) { if (!$this->_isSiteAdmin()) {
$blockedFields[] = 'org_id'; $blockedFields[] = 'org_id';
} }
@ -967,11 +957,15 @@ class UsersController extends AppController
throw new Exception('You are not authorised to assign that role to a user.'); throw new Exception('You are not authorised to assign that role to a user.');
} }
} }
if (!empty($fields) && $this->User->save($this->request->data, true, $fields)) { $fields[] = 'date_modified'; // time will be inserted in `beforeSave` action
if ($this->User->save($this->request->data, true, $fields)) {
// newValues to array // newValues to array
$fieldsNewValues = array(); $fieldsNewValues = array();
foreach ($fields as $field) { foreach ($fields as $field) {
if ($field != 'confirm_password') { if ($field === 'date_modified') {
continue;
}
if ($field !== 'confirm_password') {
$newValue = $this->data['User'][$field]; $newValue = $this->data['User'][$field];
if (gettype($newValue) == 'array') { if (gettype($newValue) == 'array') {
$newValueStr = ''; $newValueStr = '';
@ -1014,7 +1008,6 @@ class UsersController extends AppController
return $this->RestResponse->viewData($user, $this->response->type()); return $this->RestResponse->viewData($user, $this->response->type());
} else { } else {
$this->Flash->success(__('The user has been saved')); $this->Flash->success(__('The user has been saved'));
$this->_refreshAuth(); // in case we modify ourselves
$this->redirect(array('action' => 'index')); $this->redirect(array('action' => 'index'));
} }
} else { } else {
@ -1252,16 +1245,9 @@ class UsersController extends AppController
// Events list // Events list
$url = $this->Session->consume('pre_login_requested_url'); $url = $this->Session->consume('pre_login_requested_url');
if (empty($url)) { if (empty($url)) {
$homepage = $this->User->UserSetting->find('first', array( $homepage = $this->User->UserSetting->getValueForUser($this->Auth->user('id'), 'homepage');
'recursive' => -1,
'conditions' => array(
'UserSetting.user_id' => $this->Auth->user('id'),
'UserSetting.setting' => 'homepage'
),
'contain' => array('User.id', 'User.org_id')
));
if (!empty($homepage)) { if (!empty($homepage)) {
$url = $homepage['UserSetting']['value']['path']; $url = $homepage['path'];
} else { } else {
$url = array('controller' => 'events', 'action' => 'index'); $url = array('controller' => 'events', 'action' => 'index');
} }
@ -1309,7 +1295,6 @@ class UsersController extends AppController
} }
if (!$this->_isRest()) { if (!$this->_isRest()) {
$this->Flash->success(__('New authkey generated.', true)); $this->Flash->success(__('New authkey generated.', true));
$this->_refreshAuth();
$this->redirect($this->referer()); $this->redirect($this->referer());
} else { } else {
return $this->RestResponse->saveSuccessResponse('User', 'resetauthkey', $id, $this->response->type(), 'Authkey updated: ' . $newkey); return $this->RestResponse->saveSuccessResponse('User', 'resetauthkey', $id, $this->response->type(), 'Authkey updated: ' . $newkey);
@ -1436,9 +1421,7 @@ class UsersController extends AppController
public function terms() public function terms()
{ {
if ($this->request->is('post') || $this->request->is('put')) { if ($this->request->is('post') || $this->request->is('put')) {
$this->User->id = $this->Auth->user('id'); $this->User->updateField($this->Auth->user(), 'termsaccepted', true);
$this->User->saveField('termsaccepted', true);
$this->_refreshAuth(); // refresh auth info
$this->Flash->success(__('You accepted the Terms and Conditions.')); $this->Flash->success(__('You accepted the Terms and Conditions.'));
$this->redirect(array('action' => 'routeafterlogin')); $this->redirect(array('action' => 'routeafterlogin'));
} }
@ -2278,18 +2261,6 @@ class UsersController extends AppController
$this->set('users', $user_results); $this->set('users', $user_results);
} }
// Refreshes the Auth session with new/updated data
protected function _refreshAuth()
{
$oldUser = $this->Auth->user();
$newUser = $this->User->find('first', array('conditions' => array('User.id' => $oldUser['id']), 'recursive' => -1,'contain' => array('Organisation', 'Role')));
// Rearrange it a bit to match the Auth object created during the login
$newUser['User']['Role'] = $newUser['Role'];
$newUser['User']['Organisation'] = $newUser['Organisation'];
unset($newUser['Organisation'], $newUser['Role']);
$this->Auth->login($newUser['User']);
}
public function searchGpgKey($email = false) public function searchGpgKey($email = false)
{ {
if (!$email) { if (!$email) {

View File

@ -0,0 +1,40 @@
<?php
class CakeResponseTmp extends CakeResponse
{
public function file($path, $options = array())
{
if ($path instanceof TmpFileTool) {
$this->header('Content-Length', $path->size());
$this->_clearBuffer();
$this->_file = $path;
} else {
parent::file($path, $options);
}
}
/**
* @param File|TmpFileTool $file
* @param array $range
* @return bool
* @throws Exception
*/
protected function _sendFile($file, $range)
{
if ($file instanceof TmpFileTool) {
set_time_limit(0);
session_write_close();
foreach ($file->intoChunks() as $chunk) {
if (!$this->_isActive()) {
$file->close();
return false;
}
echo $chunk;
$this->_flushBuffer();
}
return true;
} else {
return parent::_sendFile($file, $range);
}
}
}

View File

@ -177,7 +177,7 @@ class ComplexTypeTool
unset($input); unset($input);
$iocArray = []; $iocArray = [];
foreach ($tmpFile->csv($delimiter) as $row) { foreach ($tmpFile->intoParsedCsv($delimiter) as $row) {
if (!empty($row[0][0]) && $row[0][0] === '#') { // Comment if (!empty($row[0][0]) && $row[0][0] === '#') { // Comment
continue; continue;
} }

View File

@ -68,7 +68,7 @@ class TmpFileTool
} }
/** /**
* Get one line from file parsed as CSV. * Returns generator of parsed CSV line from file.
* *
* @param string $delimiter * @param string $delimiter
* @param string $enclosure * @param string $enclosure
@ -76,7 +76,7 @@ class TmpFileTool
* @return Generator * @return Generator
* @throws Exception * @throws Exception
*/ */
public function csv($delimiter = ',', $enclosure = '"', $escape = "\\") public function intoParsedCsv($delimiter = ',', $enclosure = '"', $escape = "\\")
{ {
$this->rewind(); $this->rewind();
$line = 0; $line = 0;
@ -88,15 +88,16 @@ class TmpFileTool
$line++; $line++;
yield $result; yield $result;
} }
fclose($this->tmpfile); $this->close();
$this->tmpfile = null;
} }
/** /**
* Returns generator of line from file.
*
* @return Generator * @return Generator
* @throws Exception * @throws Exception
*/ */
public function lines() public function intoLines()
{ {
$this->rewind(); $this->rewind();
while (!feof($this->tmpfile)) { while (!feof($this->tmpfile)) {
@ -106,24 +107,64 @@ class TmpFileTool
} }
yield $result; yield $result;
} }
fclose($this->tmpfile); $this->close();
$this->tmpfile = null; }
/**
* @param int $chunkSize In bytes
* @return Generator
* @throws Exception
*/
public function intoChunks($chunkSize = 8192)
{
$this->rewind();
while (!feof($this->tmpfile)) {
$result = fread($this->tmpfile, $chunkSize);
if ($result === false) {
throw new Exception('Could not read from temporary file.');
}
yield $result;
}
$this->close();
} }
/** /**
* @return string * @return string
* @throws Exception * @throws Exception
*/ */
public function finish() public function intoString()
{ {
$this->rewind(); $this->rewind();
$final = stream_get_contents($this->tmpfile); $string = stream_get_contents($this->tmpfile);
if ($final === false) { if ($string === false) {
throw new Exception('Could not read from temporary file.'); throw new Exception('Could not read from temporary file.');
} }
fclose($this->tmpfile); $this->close();
$this->tmpfile = null; return $string;
return $final; }
/**
* Pass data to output.
*
* @throws Exception
*/
public function intoOutput()
{
$this->rewind();
if (fpassthru($this->tmpfile) === false) {
throw new Exception('Could not pass temporary file to output.');
}
$this->close();
}
/**
* @return int
* @throws Exception
*/
public function size()
{
$this->isOpen();
return fstat($this->tmpfile)['size'];
} }
/** /**
@ -132,7 +173,30 @@ class TmpFileTool
*/ */
public function __toString() public function __toString()
{ {
return $this->finish(); return $this->intoString();
}
/**
* @return bool
*/
public function close()
{
if ($this->tmpfile) {
$result = fclose($this->tmpfile);
$this->tmpfile = null;
return $result;
}
return true;
}
/**
* @throws Exception
*/
private function isOpen()
{
if ($this->tmpfile === null) {
throw new Exception('Temporary file is already closed.');
}
} }
/** /**
@ -142,6 +206,7 @@ class TmpFileTool
*/ */
private function rewind() private function rewind()
{ {
$this->isOpen();
if (fseek($this->tmpfile, 0) === -1) { if (fseek($this->tmpfile, 0) === -1) {
throw new Exception('Could not seek to start of temporary file.'); throw new Exception('Could not seek to start of temporary file.');
} }

View File

@ -1627,10 +1627,8 @@ class AppModel extends Model
break; break;
default: default:
return false; return false;
break;
} }
$now = new DateTime();
// switch MISP instance live to false // switch MISP instance live to false
if ($liveOff) { if ($liveOff) {
$this->Server = Classregistry::init('Server'); $this->Server = Classregistry::init('Server');
@ -1643,7 +1641,7 @@ class AppModel extends Model
$this->__setUpdateProgress(0, $total_update_count, $command); $this->__setUpdateProgress(0, $total_update_count, $command);
$str_index_array = array(); $str_index_array = array();
foreach($indexArray as $toIndex) { foreach($indexArray as $toIndex) {
$str_index_array[] = __('Indexing ') . sprintf('%s -> %s', $toIndex[0], $toIndex[1]); $str_index_array[] = __('Indexing %s -> %s', $toIndex[0], $toIndex[1]);
} }
$this->__setUpdateCmdMessages(array_merge($sqlArray, $str_index_array)); $this->__setUpdateCmdMessages(array_merge($sqlArray, $str_index_array));
$flagStop = false; $flagStop = false;
@ -1679,10 +1677,10 @@ class AppModel extends Model
'email' => 'SYSTEM', 'email' => 'SYSTEM',
'action' => 'update_database', 'action' => 'update_database',
'user_id' => 0, 'user_id' => 0,
'title' => __('Successfuly executed the SQL query for ') . $command, 'title' => __('Successfully executed the SQL query for ') . $command,
'change' => sprintf(__('The executed SQL query was: %s'), $sql) 'change' => sprintf(__('The executed SQL query was: %s'), $sql)
)); ));
$this->__setUpdateResMessages($i, sprintf(__('Successfuly executed the SQL query for %s'), $command)); $this->__setUpdateResMessages($i, sprintf(__('Successfully executed the SQL query for %s'), $command));
} catch (Exception $e) { } catch (Exception $e) {
$errorMessage = $e->getMessage(); $errorMessage = $e->getMessage();
$this->Log->create(); $this->Log->create();
@ -1736,14 +1734,13 @@ class AppModel extends Model
} }
} }
} }
$this->__setUpdateProgress(count($sqlArray)+count($indexArray), false); $this->__setUpdateProgress(count($sqlArray) + count($indexArray), false);
} }
if ($clean) { if ($clean) {
$this->cleanCacheFiles(); $this->cleanCacheFiles();
} }
if ($liveOff) { if ($liveOff) {
$liveSetting = 'MISP.live'; $this->Server->serverSettingsSaveValue('MISP.live', true);
$this->Server->serverSettingsSaveValue($liveSetting, true);
} }
if (!$flagStop && $errorCount == 0) { if (!$flagStop && $errorCount == 0) {
$this->__postUpdate($command); $this->__postUpdate($command);
@ -2132,11 +2129,20 @@ class AppModel extends Model
} }
} }
if ($requiresLogout) { if ($requiresLogout) {
$this->updateDatabase('destroyAllSessions'); $this->refreshSessions();
} }
return true; return true;
} }
/**
* Update date_modified for all users, this will ensure that all users will refresh their session data.
*/
private function refreshSessions()
{
$this->User = ClassRegistry::init('User');
$this->User->updateAll(['date_modified' => time()]);
}
private function __setUpdateProgress($current, $total=false, $toward_db_version=false) private function __setUpdateProgress($current, $total=false, $toward_db_version=false)
{ {
$updateProgress = $this->getUpdateProgress(); $updateProgress = $this->getUpdateProgress();
@ -2735,7 +2741,7 @@ class AppModel extends Model
{ {
static $versionArray; static $versionArray;
if ($versionArray === null) { if ($versionArray === null) {
$file = new File(ROOT . DS . 'VERSION.json', true); $file = new File(ROOT . DS . 'VERSION.json');
$versionArray = $this->jsonDecode($file->read()); $versionArray = $this->jsonDecode($file->read());
$file->close(); $file->close();
} }

View File

@ -4697,7 +4697,7 @@ class Attribute extends AppModel
$elementCounter = $this->__iteratedFetch($user, $params, $loop, $tmpfile, $exportTool, $exportToolParams); $elementCounter = $this->__iteratedFetch($user, $params, $loop, $tmpfile, $exportTool, $exportToolParams);
} }
$tmpfile->write($exportTool->footer($exportToolParams)); $tmpfile->write($exportTool->footer($exportToolParams));
return $tmpfile->finish(); return $tmpfile;
} }
/** /**

View File

@ -42,22 +42,38 @@ class AuthKey extends AppModel
$this->data['AuthKey']['authkey_end'] = substr($authkey, -4); $this->data['AuthKey']['authkey_end'] = substr($authkey, -4);
$this->data['AuthKey']['authkey_raw'] = $authkey; $this->data['AuthKey']['authkey_raw'] = $authkey;
$this->authkey_raw = $authkey; $this->authkey_raw = $authkey;
$validity = Configure::read('Security.advanced_authkeys_validity');
if (empty($this->data['AuthKey']['expiration'])) { if (empty($this->data['AuthKey']['expiration'])) {
$this->data['AuthKey']['expiration'] = 0; $this->data['AuthKey']['expiration'] = $validity ? strtotime("+$validity days") : 0;
} else { } else {
$this->data['AuthKey']['expiration'] = strtotime($this->data['AuthKey']['expiration']); $expiration = is_numeric($this->data['AuthKey']['expiration']) ?
(int)$this->data['AuthKey']['expiration'] :
strtotime($this->data['AuthKey']['expiration']);
if ($expiration === false) {
$this->invalidate('expiration', __('Expiration must be in YYYY-MM-DD format.'));
}
if ($validity && $expiration > strtotime("+$validity days")) {
$this->invalidate('expiration', __('Maximal key validity is %s days.', $validity));
}
$this->data['AuthKey']['expiration'] = $expiration;
} }
} }
return true; return true;
} }
/**
* @param string $authkey
* @return array|false
*/
public function getAuthUserByAuthKey($authkey) public function getAuthUserByAuthKey($authkey)
{ {
$start = substr($authkey, 0, 4); $start = substr($authkey, 0, 4);
$end = substr($authkey, -4); $end = substr($authkey, -4);
$existing_authkeys = $this->find('all', [ $existing_authkeys = $this->find('all', [
'recursive' => -1, 'recursive' => -1,
'fields' => ['authkey', 'user_id'], 'fields' => ['id', 'authkey', 'user_id', 'expiration'],
'conditions' => [ 'conditions' => [
'OR' => [ 'OR' => [
'expiration >' => time(), 'expiration >' => time(),
@ -70,7 +86,12 @@ class AuthKey extends AppModel
$passwordHasher = $this->getHasher(); $passwordHasher = $this->getHasher();
foreach ($existing_authkeys as $existing_authkey) { foreach ($existing_authkeys as $existing_authkey) {
if ($passwordHasher->check($authkey, $existing_authkey['AuthKey']['authkey'])) { if ($passwordHasher->check($authkey, $existing_authkey['AuthKey']['authkey'])) {
return $this->User->getAuthUser($existing_authkey['AuthKey']['user_id']); $user = $this->User->getAuthUser($existing_authkey['AuthKey']['user_id']);
if ($user) {
$user['authkey_id'] = $existing_authkey['AuthKey']['id'];
$user['authkey_expiration'] = $existing_authkey['AuthKey']['expiration'];
}
return $user;
} }
} }
return false; return false;
@ -105,6 +126,67 @@ class AuthKey extends AppModel
} }
} }
/**
* @param int $id
* @return array
* @throws Exception
*/
public function getKeyUsage($id)
{
$redis = $this->setupRedisWithException();
$data = $redis->hGetAll("misp:authkey_usage:$id");
$output = [];
$uniqueIps = [];
foreach ($data as $key => $count) {
list($date, $ip) = explode(':', $key);
$uniqueIps[$ip] = true;
if (isset($output[$date])) {
$output[$date] += $count;
} else {
$output[$date] = $count;
}
}
// Data from redis are not sorted
ksort($output);
$lastUsage = $redis->get("misp:authkey_last_usage:$id");
$lastUsage = $lastUsage === false ? null : (int)$lastUsage;
return [$output, $lastUsage, count($uniqueIps)];
}
/**
* @param array $ids
* @return array<DateTime|null>
* @throws Exception
*/
public function getLastUsageForKeys(array $ids)
{
$redis = $this->setupRedisWithException();
$keys = array_map(function($id) {
return "misp:authkey_last_usage:$id";
}, $ids);
$lastUsages = $redis->mget($keys);
$output = [];
foreach (array_values($ids) as $i => $id) {
$output[$id] = $lastUsages[$i] === false ? null : (int)$lastUsages[$i];
}
return $output;
}
/**
* When key is deleted, update after `date_modified` for user that was assigned to that key, so session data
* will be realoaded and canceled.
* @see AppController::_refreshAuth
*/
public function afterDelete()
{
parent::afterDelete();
$userId = $this->data['AuthKey']['user_id'];
$this->User->updateAll(['date_modified' => time()], ['User.id' => $userId]);
}
/** /**
* @return AbstractPasswordHasher * @return AbstractPasswordHasher
*/ */

View File

@ -6999,7 +6999,7 @@ class Event extends AppModel
unset($result); unset($result);
unset($temp); unset($temp);
$tmpfile->write($exportTool->footer($exportToolParams)); $tmpfile->write($exportTool->footer($exportToolParams));
return $tmpfile->finish(); return $tmpfile;
} }
/* /*

View File

@ -180,7 +180,7 @@ class Feed extends AppModel
$tmpFile->write(trim($data)); $tmpFile->write(trim($data));
unset($data); unset($data);
return $tmpFile->csv(); return $tmpFile->intoParsedCsv();
} }
/** /**

View File

@ -336,6 +336,11 @@ class Log extends AppModel
$elasticSearchClient->pushDocument($logIndex, "log", $data); $elasticSearchClient->pushDocument($logIndex, "log", $data);
} }
// Do not save request action logs to syslog, because they contain no information
if ($data['Log']['action'] === 'request') {
return true;
}
// write to syslogd as well if enabled // write to syslogd as well if enabled
if ($this->syslog === null) { if ($this->syslog === null) {
if (Configure::read('Security.syslog')) { if (Configure::read('Security.syslog')) {

View File

@ -1443,7 +1443,7 @@ class MispObject extends AppModel
} }
$this->__iteratedFetch($user, $params, $loop, $tmpfile, $exportTool, $exportToolParams, $elementCounter); $this->__iteratedFetch($user, $params, $loop, $tmpfile, $exportTool, $exportToolParams, $elementCounter);
$tmpfile->write($exportTool->footer($exportToolParams)); $tmpfile->write($exportTool->footer($exportToolParams));
return $tmpfile->finish(); return $tmpfile;
} }
private function __iteratedFetch($user, &$params, &$loop, TmpFileTool $tmpfile, $exportTool, $exportToolParams, &$elementCounter = 0) private function __iteratedFetch($user, &$params, &$loop, TmpFileTool $tmpfile, $exportTool, $exportToolParams, &$elementCounter = 0)

View File

@ -1,6 +1,9 @@
<?php <?php
App::uses('AppModel', 'Model'); App::uses('AppModel', 'Model');
/**
* @property User $User
*/
class Role extends AppModel class Role extends AppModel
{ {
public $validate = array( public $validate = array(
@ -232,6 +235,18 @@ class Role extends AppModel
return true; return true;
} }
public function afterSave($created, $options = array())
{
// After role change, update `date_modified` field for all user with this role to apply this change to already
// logged users.
if (!$created && !empty($this->data)) {
$roleId = $this->data['Role']['id'];
$this->User->updateAll(['date_modified' => time()], ['role_id' => $roleId]);
}
parent::afterSave($created, $options);
}
public function afterFind($results, $primary = false) public function afterFind($results, $primary = false)
{ {
foreach ($results as $key => $val) { foreach ($results as $key => $val) {

View File

@ -865,6 +865,15 @@ class Server extends AppModel
'type' => 'boolean', 'type' => 'boolean',
'null' => true 'null' => true
), ),
'log_user_ips_authkeys' => [
'level' => self::SETTING_RECOMMENDED,
'description' => __('Log user IP and key usage on each API request. All logs for given keys are deleted after one year when this key is not used.'),
'value' => false,
'errorMessage' => '',
'test' => 'testBool',
'type' => 'boolean',
'null' => true
],
'delegation' => array( 'delegation' => array(
'level' => 1, 'level' => 1,
'description' => __('This feature allows users to create org only events and ask another organisation to take ownership of the event. This allows organisations to remain anonymous by asking a partner to publish an event for them.'), 'description' => __('This feature allows users to create org only events and ask another organisation to take ownership of the event. This allows organisations to remain anonymous by asking a partner to publish an event for them.'),
@ -1156,7 +1165,7 @@ class Server extends AppModel
'test' => 'testForPositiveInteger', 'test' => 'testForPositiveInteger',
'type' => 'numeric', 'type' => 'numeric',
'null' => true, 'null' => true,
] ],
), ),
'GnuPG' => array( 'GnuPG' => array(
'branch' => 1, 'branch' => 1,
@ -1343,6 +1352,24 @@ class Server extends AppModel
'test' => 'testBool', 'test' => 'testBool',
'type' => 'boolean', 'type' => 'boolean',
), ),
'advanced_authkeys_validity' => [
'level' => self::SETTING_OPTIONAL,
'description' => __('Maximal key lifetime in days. Use can limit that validity even more. Just newly created keys will be affected. When not set, key validity is not limited.'),
'value' => '',
'errorMessage' => '',
'type' => 'numeric',
'test' => 'testForNumeric',
'null' => true,
],
'authkey_keep_session' => [
'level' => self::SETTING_OPTIONAL,
'description' => __('When enabled, session is kept between API requests.'),
'value' => false,
'errorMessage' => '',
'test' => 'testBool',
'type' => 'boolean',
'null' => true,
],
'auth_enforced' => [ 'auth_enforced' => [
'level' => self::SETTING_OPTIONAL, 'level' => self::SETTING_OPTIONAL,
'description' => __('This optional can be enabled if external auth provider is used and when set to true, it will disable default form authentication.'), 'description' => __('This optional can be enabled if external auth provider is used and when set to true, it will disable default form authentication.'),
@ -1585,7 +1612,16 @@ class Server extends AppModel
'test' => 'testBool', 'test' => 'testBool',
'type' => 'boolean', 'type' => 'boolean',
'null' => true 'null' => true
) ),
'username_in_response_header' => [
'level' => self::SETTING_OPTIONAL,
'description' => __('When enabled, logged in username will be included in X-Username HTTP response header. This is useful for request logging on webserver/proxy side.'),
'value' => false,
'errorMessage' => '',
'test' => 'testBool',
'type' => 'boolean',
'null' => true
]
), ),
'SecureAuth' => array( 'SecureAuth' => array(
'branch' => 1, 'branch' => 1,
@ -4916,6 +4952,7 @@ class Server extends AppModel
public function dbSchemaDiagnostic() public function dbSchemaDiagnostic()
{ {
$this->AdminSetting = ClassRegistry::init('AdminSetting');
$actualDbVersion = $this->AdminSetting->find('first', array( $actualDbVersion = $this->AdminSetting->find('first', array(
'conditions' => array('setting' => 'db_version') 'conditions' => array('setting' => 'db_version')
))['AdminSetting']['value']; ))['AdminSetting']['value'];
@ -6599,7 +6636,7 @@ class Server extends AppModel
} catch (Exception $e) { } catch (Exception $e) {
$this->Log = ClassRegistry::init('Log'); $this->Log = ClassRegistry::init('Log');
$this->Log->create(); $this->Log->create();
$message = __('Could not reset fetch remote user account.'); $message = __('Could not fetch remote user account.');
$this->Log->save(array( $this->Log->save(array(
'org' => 'SYSTEM', 'org' => 'SYSTEM',
'model' => 'Server', 'model' => 'Server',
@ -6612,14 +6649,18 @@ class Server extends AppModel
return $message; return $message;
} }
if ($response->isOk()) { if ($response->isOk()) {
$user = json_decode($response->body, true); $user = $this->jsonDecode($response->body);
if (!empty($user['User'])) { if (!empty($user['User'])) {
$result = array( $results = [
'Email' => $user['User']['email'], __('User') => $user['User']['email'],
'Role name' => isset($user['Role']['name']) ? $user['Role']['name'] : 'Unknown, outdated instance', __('Role name') => isset($user['Role']['name']) ? $user['Role']['name'] : __('Unknown, outdated instance'),
'Sync flag' => isset($user['Role']['perm_sync']) ? ($user['Role']['perm_sync'] ? 1 : 0) : 'Unknown, outdated instance' __('Sync flag') => isset($user['Role']['perm_sync']) ? ($user['Role']['perm_sync'] ? __('Yes') : __('No')) : __('Unknown, outdated instance'),
); ];
return $result; if (isset($response->headers['X-Auth-Key-Expiration'])) {
$date = new DateTime($response->headers['X-Auth-Key-Expiration']);
$results[__('Auth key expiration')] = $date->format('Y-m-d H:i:s');
}
return $results;
} else { } else {
return __('No user object received in response.'); return __('No user object received in response.');
} }

View File

@ -140,7 +140,13 @@ class Sighting extends AppModel
return $this->save($sighting); return $this->save($sighting);
} }
public function getSighting($id, $user) /**
* @param int $id
* @param array $user
* @param bool $withEvent
* @return array
*/
public function getSighting($id, array $user, $withEvent = true)
{ {
$sighting = $this->find('first', array( $sighting = $this->find('first', array(
'recursive' => -1, 'recursive' => -1,
@ -149,10 +155,7 @@ class Sighting extends AppModel
'fields' => array('Attribute.value', 'Attribute.id', 'Attribute.uuid', 'Attribute.type', 'Attribute.category', 'Attribute.to_ids') 'fields' => array('Attribute.value', 'Attribute.id', 'Attribute.uuid', 'Attribute.type', 'Attribute.category', 'Attribute.to_ids')
), ),
'Event' => array( 'Event' => array(
'fields' => array('Event.id', 'Event.uuid', 'Event.orgc_id', 'Event.org_id', 'Event.info'), 'fields' => $withEvent ? ['Event.id', 'Event.uuid', 'Event.orgc_id', 'Event.org_id', 'Event.info'] : ['Event.org_id'],
'Orgc' => array(
'fields' => array('Orgc.name')
)
) )
), ),
'conditions' => array('Sighting.id' => $id) 'conditions' => array('Sighting.id' => $id)
@ -161,11 +164,7 @@ class Sighting extends AppModel
return array(); return array();
} }
if (!isset($event)) { $ownEvent = $user['Role']['perm_site_admin'] || $sighting['Event']['org_id'] == $user['org_id'];
$event = array('Event' => $sighting['Event']);
}
$ownEvent = $user['Role']['perm_site_admin'] || $event['Event']['org_id'] == $user['org_id'];
if (!$ownEvent) { if (!$ownEvent) {
$sightingPolicy = $this->sightingsPolicy(); $sightingPolicy = $this->sightingsPolicy();
// if sighting policy == 0 then return false if the sighting doesn't belong to the user // if sighting policy == 0 then return false if the sighting doesn't belong to the user
@ -181,26 +180,24 @@ class Sighting extends AppModel
} }
} }
} }
// Put event organisation name from cache
if ($withEvent) {
$sighting['Event']['Orgc']['name'] = $this->getOrganisationById($sighting['Event']['orgc_id'])['name'];
}
$anonymise = Configure::read('Plugin.Sightings_anonymise'); $anonymise = Configure::read('Plugin.Sightings_anonymise');
if ($anonymise) { if ($anonymise && $sighting['Sighting']['org_id'] != $user['org_id']) {
if ($sighting['Sighting']['org_id'] != $user['org_id']) { unset($sighting['Sighting']['org_id']);
unset($sighting['Sighting']['org_id']);
unset($sighting['Organisation']);
}
} }
// rearrange it to match the event format of fetchevent // rearrange it to match the event format of fetchevent
if (isset($sighting['Organisation'])) {
$sighting['Sighting']['Organisation'] = $sighting['Organisation'];
unset($sighting['Organisation']);
}
$result = array( $result = array(
'Sighting' => $sighting['Sighting'] 'Sighting' => $sighting['Sighting']
); );
$result['Sighting']['Event'] = $sighting['Event']; if ($withEvent) {
$result['Sighting']['Attribute'] = $sighting['Attribute']; $result['Sighting']['Event'] = $sighting['Event'];
if (!empty($sighting['Organisation'])) {
$result['Sighting']['Organisation'] = $sighting['Organisation'];
} }
$result['Sighting']['Attribute'] = $sighting['Attribute'];
return $result; return $result;
} }
@ -332,7 +329,7 @@ class Sighting extends AppModel
'fields' => ['org_id', 'attribute_id', 'type', 'date', 'last_timestamp', 'sighting_count'], 'fields' => ['org_id', 'attribute_id', 'type', 'date', 'last_timestamp', 'sighting_count'],
'recursive' => -1, 'recursive' => -1,
'group' => ['org_id', 'attribute_id', 'type', 'date'], 'group' => ['org_id', 'attribute_id', 'type', 'date'],
'order' => ['date_sighting'], // from oldest 'order' => ['date'], // from oldest
)); ));
unset( unset(
$this->virtualFields['date'], $this->virtualFields['date'],
@ -369,7 +366,7 @@ class Sighting extends AppModel
'conditions' => $conditions, 'conditions' => $conditions,
'fields' => [ucfirst($context) . 'Tag.tag_id', 'date', 'sighting_count'], 'fields' => [ucfirst($context) . 'Tag.tag_id', 'date', 'sighting_count'],
'group' => [ucfirst($context) . 'Tag.id', 'date'], 'group' => [ucfirst($context) . 'Tag.id', 'date'],
'order' => ['date_sighting'], // from oldest 'order' => ['date'], // from oldest
]); ]);
unset($this->virtualFields['date'], $this->virtualFields['sighting_count']); unset($this->virtualFields['date'], $this->virtualFields['sighting_count']);
return $sightings; return $sightings;
@ -659,6 +656,10 @@ class Sighting extends AppModel
return $result; return $result;
} }
/**
* @return bool
* @deprecated
*/
public function addUuids() public function addUuids()
{ {
$sightings = $this->find('all', array( $sightings = $this->find('all', array(
@ -864,48 +865,27 @@ class Sighting extends AppModel
} }
// fetch sightings matching the query // fetch sightings matching the query
$sightings = $this->find('list', array( $sightingIds = $this->find('list', array(
'recursive' => -1, 'recursive' => -1,
'conditions' => $conditions, 'conditions' => $conditions,
'fields' => array('id'), 'fields' => array('id'),
'contain' => $contain, 'contain' => $contain,
)); ));
$filters['requested_attributes'] = array('id', 'attribute_id', 'event_id', 'org_id', 'date_sighting', 'uuid', 'source', 'type'); $includeAttribute = isset($filters['includeAttribute']) && $filters['includeAttribute'];
$includeEvent = isset($filters['includeEvent']) && $filters['includeEvent'];
// apply ACL and sighting policies $requestedAttributes = ['id', 'attribute_id', 'event_id', 'org_id', 'date_sighting', 'uuid', 'source', 'type'];
$allowedSightings = array(); if ($includeAttribute) {
$additional_attribute_added = false; $requestedAttributes = array_merge($requestedAttributes, ['attribute_uuid', 'attribute_type', 'attribute_category', 'attribute_to_ids', 'attribute_value']);
$additional_event_added = false;
foreach ($sightings as $sid) {
$sight = $this->getSighting($sid, $user);
if (!empty($sight)) {
$sight['Sighting']['value'] = $sight['Sighting']['Attribute']['value'];
// by default, do not include event and attribute
if (!isset($filters['includeAttribute']) || !$filters['includeAttribute']) {
unset($sight["Sighting"]["Attribute"]);
} else if (!$additional_attribute_added) {
$filters['requested_attributes'] = array_merge($filters['requested_attributes'], array('attribute_uuid', 'attribute_type', 'attribute_category', 'attribute_to_ids', 'attribute_value'));
$additional_attribute_added = true;
}
if (!isset($filters['includeEvent']) || !$filters['includeEvent']) {
unset($sight["Sighting"]["Event"]);
} else if (!$additional_event_added) {
$filters['requested_attributes'] = array_merge($filters['requested_attributes'], array('event_uuid', 'event_orgc_id', 'event_org_id', 'event_info', 'event_Orgc_name'));
$additional_event_added = true;
}
$allowedSightings[] = $sight;
}
} }
if ($includeEvent) {
$params = array( $requestedAttributes = array_merge($requestedAttributes, ['event_uuid', 'event_orgc_id', 'event_org_id', 'event_info', 'event_Orgc_name']);
'conditions' => array(), //result already filtered }
); $filters['requested_attributes'] = $requestedAttributes;
$exportToolParams = array( $exportToolParams = array(
'user' => $user, 'user' => $user,
'params' => $params, 'params' => ['conditions' => []], //result already filtered
'returnFormat' => $returnFormat, 'returnFormat' => $returnFormat,
'scope' => 'Sighting', 'scope' => 'Sighting',
'filters' => $filters 'filters' => $filters
@ -913,21 +893,22 @@ class Sighting extends AppModel
$tmpfile = new TmpFileTool(); $tmpfile = new TmpFileTool();
$tmpfile->write($exportTool->header($exportToolParams)); $tmpfile->write($exportTool->header($exportToolParams));
$separator = $exportTool->separator($exportToolParams);
$temp = ''; foreach ($sightingIds as $sightingId) {
$i = 0; // apply ACL and sighting policies
foreach ($allowedSightings as $sighting) { $sighting = $this->getSighting($sightingId, $user, $includeEvent);
$temp .= $exportTool->handler($sighting, $exportToolParams); if (!empty($sighting)) {
if ($temp !== '') { $sighting['Sighting']['value'] = $sighting['Sighting']['Attribute']['value'];
if ($i != count($allowedSightings) -1) { if (!$includeAttribute) {
$temp .= $exportTool->separator($exportToolParams); unset($sighting['Sighting']['Attribute']);
} }
$tmpfile->writeWithSeparator($exportTool->handler($sighting, $exportToolParams), $separator);
} }
$i++;
} }
$tmpfile->write($temp);
$tmpfile->write($exportTool->footer($exportToolParams)); $tmpfile->write($exportTool->footer($exportToolParams));
return $tmpfile->finish(); return $tmpfile;
} }
/** /**

View File

@ -641,39 +641,50 @@ class User extends AppModel
// get the current user and rearrange it to be in the same format as in the auth component // get the current user and rearrange it to be in the same format as in the auth component
public function getAuthUser($id) public function getAuthUser($id)
{ {
$user = $this->getUserById($id); if (empty($id)) {
if (empty($user)) { throw new InvalidArgumentException('Invalid user ID.');
return $user;
} }
return $this->rearrangeToAuthForm($user); $conditions = ['User.id' => $id];
return $this->getAuthUserByConditions($conditions);
} }
// get the current user and rearrange it to be in the same format as in the auth component // get the current user and rearrange it to be in the same format as in the auth component
public function getAuthUserByAuthkey($id) public function getAuthUserByAuthkey($authkey)
{ {
$conditions = array('User.authkey' => $id); if (empty($authkey)) {
$user = $this->find('first', array('conditions' => $conditions, 'recursive' => -1,'contain' => array('Organisation', 'Role', 'Server'))); throw new InvalidArgumentException('Invalid user auth key.');
if (empty($user)) {
return $user;
} }
return $this->rearrangeToAuthForm($user); $conditions = array('User.authkey' => $authkey);
return $this->getAuthUserByConditions($conditions);
} }
public function getAuthUserByExternalAuth($auth_key) public function getAuthUserByExternalAuth($auth_key)
{ {
if (empty($auth_key)) {
throw new InvalidArgumentException('Invalid user external auth key.');
}
$conditions = array( $conditions = array(
'User.external_auth_key' => $auth_key, 'User.external_auth_key' => $auth_key,
'User.external_auth_required' => true 'User.external_auth_required' => true
); );
$user = $this->find('first', array( return $this->getAuthUserByConditions($conditions);
}
/**
* @param array $conditions
* @return array|null
*/
private function getAuthUserByConditions(array $conditions)
{
$user = $this->find('first', [
'conditions' => $conditions, 'conditions' => $conditions,
'recursive' => -1, 'recursive' => -1,
'contain' => array( 'contain' => [
'Organisation', 'Organisation',
'Role', 'Role',
'Server' 'Server',
) ],
)); ]);
if (empty($user)) { if (empty($user)) {
return $user; return $user;
} }
@ -696,9 +707,6 @@ class User extends AppModel
$user['User']['Role'] = $user['Role']; $user['User']['Role'] = $user['Role'];
$user['User']['Organisation'] = $user['Organisation']; $user['User']['Organisation'] = $user['Organisation'];
$user['User']['Server'] = $user['Server']; $user['User']['Server'] = $user['Server'];
if (isset($user['UserSetting'])) {
$user['User']['UserSetting'] = $user['UserSetting'];
}
return $user['User']; return $user['User'];
} }
@ -967,7 +975,7 @@ class User extends AppModel
if ($result) { if ($result) {
$this->id = $user['User']['id']; $this->id = $user['User']['id'];
$this->saveField('password', $password); $this->saveField('password', $password);
$this->saveField('change_pw', '1'); $this->updateField($user['User'], 'change_pw', 1);
if ($simpleReturn) { if ($simpleReturn) {
return true; return true;
} else { } else {
@ -1142,7 +1150,7 @@ class User extends AppModel
if (empty(Configure::read('Security.advanced_authkeys'))) { if (empty(Configure::read('Security.advanced_authkeys'))) {
$oldKey = $this->data['User']['authkey']; $oldKey = $this->data['User']['authkey'];
$newkey = $this->generateAuthKey(); $newkey = $this->generateAuthKey();
$this->saveField('authkey', $newkey); $this->updateField($updatedUser['User'], 'authkey', $newkey);
$this->extralog( $this->extralog(
$user, $user,
'reset_auth_key', 'reset_auth_key',
@ -1286,24 +1294,6 @@ class User extends AppModel
return $data; return $data;
} }
/*
* Set the monitoring flag in Configure for the current user
* Reads the state from redis
*/
public function setMonitoring($user)
{
if (
!empty(Configure::read('Security.user_monitoring_enabled'))
) {
$redis = $this->setupRedis();
if (!empty($redis->sismember('misp:monitored_users', $user['id']))) {
Configure::write('Security.monitored', 1);
return true;
}
}
Configure::write('Security.monitored', 0);
}
public function registerUser($added_by, $registration, $org_id, $role_id) { public function registerUser($added_by, $registration, $org_id, $role_id) {
$user = array( $user = array(
'email' => $registration['data']['email'], 'email' => $registration['data']['email'],
@ -1403,7 +1393,7 @@ class User extends AppModel
$name => $value, $name => $value,
], true, ['id', $name, 'date_modified']); ], true, ['id', $name, 'date_modified']);
if (!$success) { if (!$success) {
throw new RuntimeException("Could not save field `$name` with value `$value` for user `{$user['id']}`."); throw new RuntimeException("Could not save setting $name for user {$user['id']}.");
} }
} }

View File

@ -201,34 +201,33 @@ class UserSetting extends AppModel
public function getDefaulRestSearchParameters($user) public function getDefaulRestSearchParameters($user)
{ {
$setting = $this->find('first', array( return $this->getValueForUser($user['id'], 'default_restsearch_parameters') ?: [];
'recursive' => -1,
'conditions' => array(
'UserSetting.user_id' => $user['id'],
'UserSetting.setting' => 'default_restsearch_parameters'
)
));
$parameters = array();
if (!empty($setting)) {
$parameters = $setting['UserSetting']['value'];
}
return $parameters;
} }
public function getTagNumericalValueOverride($userId) public function getTagNumericalValueOverride($userId)
{ {
$setting = $this->find('first', array( return $this->getValueForUser($userId, 'tag_numerical_value_override') ?: [];
'recursive' => -1, }
'conditions' => array(
'UserSetting.user_id' => $userId, /**
'UserSetting.setting' => 'tag_numerical_value_override' * @param int $userId
) * @param string $setting
)); * @return mixed|null
$parameters = array(); */
if (!empty($setting)) { public function getValueForUser($userId, $setting)
$parameters = $setting['UserSetting']['value']; {
} $output = $this->find('first', array(
return $parameters; 'recursive' => -1,
'fields' => ['value'],
'conditions' => array(
'UserSetting.user_id' => $userId,
'UserSetting.setting' => $setting,
)
));
if ($output) {
return $output['UserSetting']['value'];
}
return null;
} }
/* /*

View File

@ -1,5 +1,4 @@
<?php <?php
echo $this->element('genericElements/Form/genericForm', [ echo $this->element('genericElements/Form/genericForm', [
'data' => [ 'data' => [
'title' => __('Add auth key'), 'title' => __('Add auth key'),
@ -13,11 +12,12 @@ echo $this->element('genericElements/Form/genericForm', [
], ],
[ [
'field' => 'comment', 'field' => 'comment',
'label' => __('Comment'),
'class' => 'span6' 'class' => 'span6'
], ],
[ [
'field' => 'expiration', 'field' => 'expiration',
'label' => 'Expiration', 'label' => __('Expiration (%s)', $validity ? __('keep empty for maximal validity of %s days', $validity) : __('keep empty for indefinite')),
'class' => 'datepicker span6', 'class' => 'datepicker span6',
'placeholder' => "YYYY-MM-DD", 'placeholder' => "YYYY-MM-DD",
'type' => 'text' 'type' => 'text'

View File

@ -28,8 +28,7 @@
'type' => 'search', 'type' => 'search',
'button' => __('Filter'), 'button' => __('Filter'),
'placeholder' => __('Enter value to search'), 'placeholder' => __('Enter value to search'),
'data' => '', 'searchKey' => 'quickFilter',
'searchKey' => 'value'
] ]
] ]
], ],
@ -43,9 +42,12 @@
'name' => __('User'), 'name' => __('User'),
'sort' => 'User.email', 'sort' => 'User.email',
'data_path' => 'User.email', 'data_path' => 'User.email',
'element' => empty($user_id) ? 'links' : 'generic_field',
'url' => $baseurl . '/users/view',
'url_params_data_paths' => ['User.id'],
], ],
[ [
'name' => __('Auth key'), 'name' => __('Auth Key'),
'sort' => 'AuthKey.authkey_start', 'sort' => 'AuthKey.authkey_start',
'element' => 'authkey', 'element' => 'authkey',
'data_path' => 'AuthKey', 'data_path' => 'AuthKey',
@ -56,6 +58,12 @@
'data_path' => 'AuthKey.expiration', 'data_path' => 'AuthKey.expiration',
'element' => 'expiration' 'element' => 'expiration'
], ],
[
'name' => ('Last used'),
'data_path' => 'AuthKey.last_used',
'element' => 'datetime',
'requirements' => $keyUsageEnabled,
],
[ [
'name' => __('Comment'), 'name' => __('Comment'),
'sort' => 'AuthKey.comment', 'sort' => 'AuthKey.comment',
@ -66,6 +74,14 @@
'description' => empty($ajax) ? __('A list of API keys bound to a user.') : false, 'description' => empty($ajax) ? __('A list of API keys bound to a user.') : false,
'pull' => 'right', 'pull' => 'right',
'actions' => [ 'actions' => [
[
'url' => $baseurl . '/auth_keys/view',
'url_params_data_paths' => array(
'AuthKey.id'
),
'icon' => 'eye',
'dbclickAction' => true
],
[ [
'onclick' => sprintf( 'onclick' => sprintf(
'openGenericModal(\'%s/authKeys/delete/[onclick_params_data_path]\');', 'openGenericModal(\'%s/authKeys/delete/[onclick_params_data_path]\');',
@ -80,19 +96,14 @@
]); ]);
echo '</div>'; echo '</div>';
if (empty($ajax)) { if (empty($ajax)) {
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => $metaGroup, 'menuItem' => $this->action)); echo $this->element('/genericElements/SideMenu/side_menu', $menuData);
} }
?> ?>
<script type="text/javascript"> <script type="text/javascript">
var passedArgsArray = <?php echo $passedArgs; ?>; var passedArgsArray = <?php echo $passedArgs; ?>;
$(document).ready(function() { $(function() {
$('#quickFilterButton').click(function() { $('#quickFilterButton').click(function() {
runIndexQuickFilter(); runIndexQuickFilter();
}); });
$('#quickFilterField').on('keypress', function (e) {
if(e.which === 13) {
runIndexQuickFilter();
}
});
}); });
</script> </script>

View File

@ -1,4 +1,17 @@
<?php <?php
$keyUsageCsv = null;
if (isset($keyUsage)) {
$todayString = date('Y-m-d');
$today = strtotime($todayString);
$startDate = key($keyUsage); // oldest date for sparkline
$startDate = strtotime($startDate) - (3600 * 24 * 3);
$keyUsageCsv = 'Date,Close\n';
for ($date = $startDate; $date <= $today; $date += (3600 * 24)) {
$dateAsString = date('Y-m-d', $date);
$keyUsageCsv .= $dateAsString . ',' . (isset($keyUsage[$dateAsString]) ? $keyUsage[$dateAsString] : 0) . '\n';
}
}
echo $this->element( echo $this->element(
'genericElements/SingleViews/single_view', 'genericElements/SingleViews/single_view',
[ [
@ -14,10 +27,21 @@ echo $this->element(
'path' => 'AuthKey.uuid', 'path' => 'AuthKey.uuid',
], ],
[ [
'key' => __('Auth key'), 'key' => __('Auth Key'),
'path' => 'AuthKey', 'path' => 'AuthKey',
'type' => 'authkey' 'type' => 'authkey'
], ],
[
'key' => __('User'),
'path' => 'User.id',
'pathName' => 'User.email',
'model' => 'users',
'type' => 'model'
],
[
'key' => __('Comment'),
'path' => 'AuthKey.comment'
],
[ [
'key' => __('Created'), 'key' => __('Created'),
'path' => 'AuthKey.created', 'path' => 'AuthKey.created',
@ -29,18 +53,24 @@ echo $this->element(
'type' => 'expiration' 'type' => 'expiration'
], ],
[ [
'key' => __('User'), 'key' => __('Key usage'),
'path' => 'User.id', 'type' => 'sparkline',
'pathName' => 'User.email', 'path' => 'AuthKey.id',
'model' => 'users', 'csv' => [
'type' => 'model' 'data' => $keyUsageCsv,
],
'requirement' => isset($keyUsage),
], ],
[ [
'key' => __('Comment'), 'key' => __('Last used'),
'path' => 'AuthKey.comment' 'raw' => $lastUsed ? date('Y-m-d H:i:s', $lastUsed) : __('Not used yet'),
'requirement' => isset($keyUsage),
],
[
'key' => __('Unique IPs'),
'raw' => $uniqueIps,
'requirement' => isset($keyUsage),
] ]
], ],
'children' => [
]
] ]
); );

View File

@ -20,9 +20,9 @@
$title = __('Expired at %s', date('Y-m-d H:i:s', $data)); $title = __('Expired at %s', date('Y-m-d H:i:s', $data));
$data = '<span class="red bold" title="' . $title . '">' . __('Expired') . '</span>'; $data = '<span class="red bold" title="' . $title . '">' . __('Expired') . '</span>';
} else { } else {
$diffInDays = floor(($data - time()) / 3600 * 24); $diffInDays = floor(($data - time()) / (3600 * 24));
$class = $diffInDays <= 14 ? 'text-warning bold' : 'text-success'; $class = $diffInDays <= 14 ? 'text-warning bold' : 'text-success';
$title = __n('Will expire at %s day', 'Will expire at %s days', $diffInDays, $diffInDays); $title = __n('Will expire in %s day', 'Will expire in %s days', $diffInDays, $diffInDays);
$data = '<span class="' . $class . '" title="' . $title . '">' . date('Y-m-d H:i:s', $data) . '</span>'; $data = '<span class="' . $class . '" title="' . $title . '">' . date('Y-m-d H:i:s', $data) . '</span>';
} }
} }

View File

@ -20,9 +20,9 @@
$title = __('Expired at %s', date('Y-m-d H:i:s', $data)); $title = __('Expired at %s', date('Y-m-d H:i:s', $data));
$data = '<span class="red bold" title="' . $title . '">' . __('Expired') . '</span>'; $data = '<span class="red bold" title="' . $title . '">' . __('Expired') . '</span>';
} else { } else {
$diffInDays = floor(($data - time()) / 3600 * 24); $diffInDays = floor(($data - time()) / (3600 * 24));
$class = $diffInDays <= 14 ? 'text-warning bold' : 'text-success'; $class = $diffInDays <= 14 ? 'text-warning bold' : 'text-success';
$title = __n('Will expire at %s day', 'Will expire at %s days', $diffInDays, $diffInDays); $title = __n('Will expire in %s day', 'Will expire in %s days', $diffInDays, $diffInDays);
$data = '<span class="' . $class . '" title="' . $title . '">' . date('Y-m-d H:i:s', $data) . '</span>'; $data = '<span class="' . $class . '" title="' . $title . '">' . date('Y-m-d H:i:s', $data) . '</span>';
} }
} }

View File

@ -1,5 +1,5 @@
<?php <?php
if (!empty($field['raw'])) { if (isset($field['raw'])) {
$string = $field['raw']; $string = $field['raw'];
} else { } else {
$value = Hash::extract($data, $field['path']); $value = Hash::extract($data, $field['path']);

View File

@ -0,0 +1,14 @@
<?php
$elementId = Hash::extract($data, $field['path'])[0];
if (!empty($field['csv_data_path'])) {
$csv = Hash::extract($data, $field['csv_data_path']);
if (!empty($csv)) {
$csv = $csv[0];
}
} else {
$csv = $field['csv']['data'];
}
if (!empty($csv)) {
$scope = empty($field['csv']['scope']) ? '' : $field['csv']['scope'];
echo $this->element('sparkline', array('scope' => $scope, 'id' => $elementId, 'csv' => $csv));
}

View File

@ -30,6 +30,10 @@
$listElements = ''; $listElements = '';
if (!empty($fields)) { if (!empty($fields)) {
foreach ($fields as $field) { foreach ($fields as $field) {
if (isset($field['requirement']) && !$field['requirement']) {
continue;
}
if (empty($field['type'])) { if (empty($field['type'])) {
$field['type'] = 'generic'; $field['type'] = 'generic';
} }

View File

@ -1,12 +0,0 @@
<?php
App::uses('AppHelper', 'View/Helper');
class UtilityHelper extends AppHelper {
public function space2nbsp($string) {
$string = str_replace("\t", "&nbsp&nbsp&nbsp&nbsp", $string);
$string = preg_replace("/\s\s+/", "&nbsp;", $string);
//$string = str_replace(' ', "&nbsp", $string);
return $string;
}
}

View File

@ -86,7 +86,7 @@
'type' => 'checkbox', 'type' => 'checkbox',
'checked' => isset($this->request->data['User']['contactalert']) ? $this->request->data['User']['contactalert'] : true 'checked' => isset($this->request->data['User']['contactalert']) ? $this->request->data['User']['contactalert'] : true
)); ));
echo $this->Form->input('disabled', array('type' => 'checkbox', 'label' => __('Disable this user account'))); echo $this->Form->input('disabled', array('type' => 'checkbox', 'label' => __('Immediately disable this user account')));
echo $this->Form->input('notify', array( echo $this->Form->input('notify', array(
'label' => __('Send credentials automatically'), 'label' => __('Send credentials automatically'),
'type' => 'checkbox', 'type' => 'checkbox',

View File

@ -81,13 +81,13 @@
echo $this->Form->input('termsaccepted', array('type' => 'checkbox', 'label' => __('Terms accepted'))); echo $this->Form->input('termsaccepted', array('type' => 'checkbox', 'label' => __('Terms accepted')));
echo $this->Form->input('change_pw', [ echo $this->Form->input('change_pw', [
'type' => 'checkbox', 'type' => 'checkbox',
'label' => __('User must change password after next login'), 'label' => __('User must change password'),
'disabled' => !$canChangePassword, 'disabled' => !$canChangePassword,
'data-disabled-reason' => !$canChangePassword ? __('User password change is disabled on this instance') : '', 'data-disabled-reason' => !$canChangePassword ? __('User password change is disabled on this instance') : '',
]); ]);
echo $this->Form->input('autoalert', array('label' => __('Receive email alerts when events are published'), 'type' => 'checkbox')); echo $this->Form->input('autoalert', array('label' => __('Receive email alerts when events are published'), 'type' => 'checkbox'));
echo $this->Form->input('contactalert', array('label' => __('Receive email alerts from "Contact reporter" requests'), 'type' => 'checkbox')); echo $this->Form->input('contactalert', array('label' => __('Receive email alerts from "Contact reporter" requests'), 'type' => 'checkbox'));
echo $this->Form->input('disabled', array('type' => 'checkbox', 'label' => __('Disable this user account'))); echo $this->Form->input('disabled', array('type' => 'checkbox', 'label' => __('Immediately disable this user account')));
echo '</div>'; echo '</div>';
?> ?>
</fieldset> </fieldset>

View File

@ -1,12 +1,15 @@
<?php
$title = Inflector::singularize(Inflector::humanize(Inflector::underscore($this->params['controller'])));
?>
<div id="genericModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="genericModalLabel" aria-hidden="true"> <div id="genericModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="genericModalLabel" aria-hidden="true">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
<h3 id="genericModalLabel"><?= __('Delete %s', Inflector::singularize(Inflector::humanize($this->params['controller']))) ?></h3> <h3 id="genericModalLabel"><?= __('Delete %s', h($title)) ?></h3>
</div> </div>
<div class="modal-body modal-body-long"> <div class="modal-body modal-body-long">
<p><?= __('Are you sure you want to delete %s #%s?', h(Inflector::singularize($this->params['controller'])), h($id)) ?></p> <p><?= __('Are you sure you want to delete %s #%s?', h($title), h($id)) ?></p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<?= $this->Form->postLink( <?= $this->Form->postLink(
@ -20,7 +23,7 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
$(document).keydown(function(e) { $(document).keydown(function(e) {
if(e.which === 13 && e.ctrlKey) { if (e.which === 13 && e.ctrlKey) {
$('.button-execute').click(); $('.button-execute').click();
} }
}); });

View File

@ -3371,15 +3371,15 @@ function getRemoteSyncUser(id) {
$.ajax({ $.ajax({
url: baseurl + '/servers/getRemoteUser/' + id, url: baseurl + '/servers/getRemoteUser/' + id,
type:'GET', type:'GET',
beforeSend: function (XMLHttpRequest) { beforeSend: function () {
resultContainer.html('Running test...'); resultContainer.html('Running test...');
}, },
error: function(){ error: function() {
resultContainer.html('Internal error.'); resultContainer.html('Internal error.');
}, },
success: function(response) { success: function(response) {
resultContainer.empty();
if (typeof(response.message) != 'undefined') { if (typeof(response.message) != 'undefined') {
resultContainer.empty();
resultContainer.append( resultContainer.append(
$('<span>') $('<span>')
.attr('class', 'red bold') .attr('class', 'red bold')
@ -3389,7 +3389,6 @@ function getRemoteSyncUser(id) {
.text(': #' + response.message) .text(': #' + response.message)
); );
} else { } else {
resultContainer.empty();
Object.keys(response).forEach(function(key) { Object.keys(response).forEach(function(key) {
var value = response[key]; var value = response[key];
resultContainer.append( resultContainer.append(
@ -3404,7 +3403,6 @@ function getRemoteSyncUser(id) {
); );
}); });
} }
var result = response;
} }
}); });
} }

View File

@ -1,9 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import sys import sys
import time
import json import json
import datetime
import unittest import unittest
from typing import Union from typing import Union, List
import urllib3 # type: ignore import urllib3 # type: ignore
import logging import logging
import uuid import uuid
@ -14,7 +16,7 @@ from lxml.html import fromstring
from enum import Enum from enum import Enum
try: try:
from pymisp import PyMISP, MISPOrganisation, MISPUser, MISPRole, MISPSharingGroup, MISPEvent from pymisp import PyMISP, MISPOrganisation, MISPUser, MISPRole, MISPSharingGroup, MISPEvent, MISPLog
from pymisp.exceptions import PyMISPError, NoKey, MISPServerError from pymisp.exceptions import PyMISPError, NoKey, MISPServerError
except ImportError: except ImportError:
if sys.version_info < (3, 6): if sys.version_info < (3, 6):
@ -45,7 +47,7 @@ def check_response(response):
raise Exception(response["errors"]) raise Exception(response["errors"])
def login(url: str, email: str, password: str) -> bool: def login(url: str, email: str, password: str) -> requests.Session:
session = requests.Session() session = requests.Session()
r = session.get(url) r = session.get(url)
@ -77,7 +79,7 @@ def login(url: str, email: str, password: str) -> bool:
r = r.json() r = r.json()
if email != r["User"]["email"]: if email != r["User"]["email"]:
raise Exception(r) # logged in as different user raise Exception(r) # logged in as different user
return True return session
class MISPSetting: class MISPSetting:
@ -166,7 +168,7 @@ class TestSecurity(unittest.TestCase):
# Try to connect as user to check if everything works # Try to connect as user to check if everything works
PyMISP(url, cls.test_usr.authkey) PyMISP(url, cls.test_usr.authkey)
# Check if user can login with given password # Check if user can login with given password
assert login(url, cls.test_usr.email, cls.test_usr_password) assert isinstance(login(url, cls.test_usr.email, cls.test_usr_password), requests.Session)
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
@ -179,8 +181,6 @@ class TestSecurity(unittest.TestCase):
def setUp(self): def setUp(self):
# Do not show warning about not closed resources, because that something we want # Do not show warning about not closed resources, because that something we want
warnings.simplefilter("ignore", ResourceWarning) warnings.simplefilter("ignore", ResourceWarning)
# TODO: Try to reload config cache
self.admin_misp_connector.get_server_setting("MISP.live")
def test_not_logged_in(self): def test_not_logged_in(self):
session = requests.Session() session = requests.Session()
@ -302,7 +302,7 @@ class TestSecurity(unittest.TestCase):
self.assertFalse(updated_user.disabled) self.assertFalse(updated_user.disabled)
# Try to login # Try to login
self.assertTrue(login(url, self.test_usr.email, self.test_usr_password)) self.assertIsInstance(login(url, self.test_usr.email, self.test_usr_password), requests.Session)
def test_disabled_user_api_access(self): def test_disabled_user_api_access(self):
# Disable user # Disable user
@ -327,7 +327,7 @@ class TestSecurity(unittest.TestCase):
self.assertFalse(login(url, self.test_usr.email, self.test_usr_password)) self.assertFalse(login(url, self.test_usr.email, self.test_usr_password))
# Check if user can login with given password # Check if user can login with given password
self.assertTrue(login(url, self.test_usr.email, self.test_usr_password)) self.assertIsInstance(login(url, self.test_usr.email, self.test_usr_password), requests.Session)
def test_disabled_misp_api_access(self): def test_disabled_misp_api_access(self):
with self.__setting("MISP.live", False): with self.__setting("MISP.live", False):
@ -342,6 +342,7 @@ class TestSecurity(unittest.TestCase):
with self.__setting("Security.advanced_authkeys", True): with self.__setting("Security.advanced_authkeys", True):
# Create advanced authkey # Create advanced authkey
auth_key = self.__create_advanced_authkey(self.test_usr.id) auth_key = self.__create_advanced_authkey(self.test_usr.id)
self.assertNotIn("authkey", auth_key)
# Try to login # Try to login
logged_in = PyMISP(url, auth_key["authkey_raw"]) logged_in = PyMISP(url, auth_key["authkey_raw"])
@ -374,6 +375,46 @@ class TestSecurity(unittest.TestCase):
self.__delete_advanced_authkey(auth_key["id"]) self.__delete_advanced_authkey(auth_key["id"])
def test_advanced_authkeys_deleted(self):
with self.__setting("Security.advanced_authkeys", True):
auth_key = self.__create_advanced_authkey(self.test_usr.id)
logged_in = PyMISP(url, auth_key["authkey_raw"])
self.assertEqual(logged_in._current_user.id, self.test_usr.id)
self.__delete_advanced_authkey(auth_key["id"])
self.__assertErrorResponse(logged_in.get_user())
def test_advanced_authkeys_deleted_keep_session(self):
with self.__setting({
"Security": {
"advanced_authkeys": True,
"authkey_keep_session": True,
}
}):
auth_key = self.__create_advanced_authkey(self.test_usr.id)
logged_in = PyMISP(url, auth_key["authkey_raw"])
self.assertEqual(logged_in._current_user.id, self.test_usr.id)
# Wait one second to really know that session will be reloaded
time.sleep(1)
self.__delete_advanced_authkey(auth_key["id"])
with self.assertRaises(MISPServerError):
logged_in.get_user()
time.sleep(1)
def test_advanced_authkeys_own_key_not_possible(self):
with self.__setting("Security.advanced_authkeys", True):
authkey = ("a" * 40)
auth_key = self.__create_advanced_authkey(self.test_usr.id, {"authkey": authkey})
self.__delete_advanced_authkey(auth_key["id"])
self.assertNotEqual(authkey, auth_key["authkey_raw"])
def test_advanced_authkeys_reset_own(self): def test_advanced_authkeys_reset_own(self):
with self.__setting("Security.advanced_authkeys", True): with self.__setting("Security.advanced_authkeys", True):
# Create advanced authkey # Create advanced authkey
@ -441,6 +482,46 @@ class TestSecurity(unittest.TestCase):
self.__delete_advanced_authkey(auth_key["id"]) self.__delete_advanced_authkey(auth_key["id"])
# TODO: Delete new key # TODO: Delete new key
def test_advanced_authkeys_expiration_invalid(self):
with self.__setting("Security.advanced_authkeys", True):
with self.assertRaises(Exception) as cm:
self.__create_advanced_authkey(self.test_usr.id, {"expiration": "__nonsense__"})
self.assertIn("expiration", cm.exception.args[0][1]["errors"])
def test_advanced_authkeys_validity_autoset(self):
with self.__setting({
"Security": {
"advanced_authkeys": True,
"advanced_authkeys_validity": 365,
}
}):
auth_key = self.__create_advanced_authkey(self.test_usr.id)
self.assertNotEqual(0, auth_key["expiration"])
def test_advanced_authkeys_validity_in_range(self):
with self.__setting({
"Security": {
"advanced_authkeys": True,
"advanced_authkeys_validity": 365,
}
}):
expiration = int((datetime.datetime.now() + datetime.timedelta(days=300)).timestamp())
auth_key = self.__create_advanced_authkey(self.test_usr.id, {"expiration": expiration})
self.__delete_advanced_authkey(auth_key["id"])
self.assertEqual(expiration, int(auth_key["expiration"]))
def test_advanced_authkeys_validity_not_in_range(self):
with self.__setting({
"Security": {
"advanced_authkeys": True,
"advanced_authkeys_validity": 365,
}
}):
expiration = int((datetime.datetime.now() + datetime.timedelta(days=400)).timestamp())
with self.assertRaises(Exception) as cm:
self.__create_advanced_authkey(self.test_usr.id, {"expiration": expiration})
self.assertIn("expiration", cm.exception.args[0][1]["errors"])
def test_advanced_authkeys_view(self): def test_advanced_authkeys_view(self):
with self.__setting("Security.advanced_authkeys", True): with self.__setting("Security.advanced_authkeys", True):
auth_key = self.__create_advanced_authkey(self.test_usr.id) auth_key = self.__create_advanced_authkey(self.test_usr.id)
@ -481,6 +562,12 @@ class TestSecurity(unittest.TestCase):
self.__delete_advanced_authkey(auth_key["id"]) self.__delete_advanced_authkey(auth_key["id"])
def test_authkey_keep_session(self):
with self.__setting( "Security.authkey_keep_session", True):
logged_in = PyMISP(url, self.test_usr.authkey)
check_response(logged_in.get_user())
check_response(logged_in.get_user())
def test_change_login(self): def test_change_login(self):
new_email = 'testusr@user' + random() + '.local' new_email = 'testusr@user' + random() + '.local'
@ -530,7 +617,7 @@ class TestSecurity(unittest.TestCase):
# Try to change email as org admin # Try to change email as org admin
new_email = 'testusr@user' + random() + '.local' new_email = 'testusr@user' + random() + '.local'
updated_user = self.org_admin_misp_connector.update_user({'email': new_email}, self.test_usr) updated_user = self.org_admin_misp_connector.update_user({'email': new_email}, self.test_usr)
self.__assertErrorResponse(updated_user) self.assertEqual(self.test_usr.email, updated_user.email, "Email should be still same")
def test_change_pw_disabled(self): def test_change_pw_disabled(self):
with self.__setting("MISP.disable_user_password_change", True): with self.__setting("MISP.disable_user_password_change", True):
@ -539,7 +626,7 @@ class TestSecurity(unittest.TestCase):
logged_in.change_user_password(str(uuid.uuid4())) logged_in.change_user_password(str(uuid.uuid4()))
# Password should be still the same # Password should be still the same
self.assertTrue(login(url, self.test_usr.email, self.test_usr_password)) self.assertIsInstance(login(url, self.test_usr.email, self.test_usr_password), requests.Session)
def test_change_pw_disabled_different_way(self): def test_change_pw_disabled_different_way(self):
with self.__setting("MISP.disable_user_password_change", True): with self.__setting("MISP.disable_user_password_change", True):
@ -548,14 +635,14 @@ class TestSecurity(unittest.TestCase):
logged_in.update_user({"password": str(uuid.uuid4())}, self.test_usr.id) logged_in.update_user({"password": str(uuid.uuid4())}, self.test_usr.id)
# Password should be still the same # Password should be still the same
self.assertTrue(login(url, self.test_usr.email, self.test_usr_password)) self.assertIsInstance(login(url, self.test_usr.email, self.test_usr_password), requests.Session)
def test_change_pw_disabled_by_org_admin(self): def test_change_pw_disabled_by_org_admin(self):
with self.__setting("MISP.disable_user_password_change", True): with self.__setting("MISP.disable_user_password_change", True):
self.org_admin_misp_connector.update_user({"password": str(uuid.uuid4())}, self.test_usr.id) self.org_admin_misp_connector.update_user({"password": str(uuid.uuid4())}, self.test_usr.id)
# Password should be still the same # Password should be still the same
self.assertTrue(login(url, self.test_usr.email, self.test_usr_password)) self.assertIsInstance(login(url, self.test_usr.email, self.test_usr_password), requests.Session)
def test_add_user_by_org_admin(self): def test_add_user_by_org_admin(self):
user = MISPUser() user = MISPUser()
@ -784,7 +871,7 @@ class TestSecurity(unittest.TestCase):
def test_shibb_form_login(self): def test_shibb_form_login(self):
with self.__setting(self.__default_shibb_config()): with self.__setting(self.__default_shibb_config()):
# Form login should still works when no header provided # Form login should still works when no header provided
self.assertTrue(login(url, self.test_usr.email, self.test_usr_password)) self.assertIsInstance(login(url, self.test_usr.email, self.test_usr_password), requests.Session)
def test_shibb_api_login(self): def test_shibb_api_login(self):
with self.__setting(self.__default_shibb_config()): with self.__setting(self.__default_shibb_config()):
@ -819,6 +906,139 @@ class TestSecurity(unittest.TestCase):
with self.__setting(config): with self.__setting(config):
PyMISP(url, self.test_usr.authkey) PyMISP(url, self.test_usr.authkey)
def test_user_monitoring_enabled_no_user(self):
request_logs_before = self.__get_logs(action="request")
with self.__setting("Security.user_monitoring_enabled", True):
logged_in = PyMISP(url, self.test_usr.authkey)
check_response(logged_in.get_user())
request_logs_after = self.__get_logs(action="request")
# Number of logs should be same, because user is not monitored
self.assertEqual(len(request_logs_after), len(request_logs_before))
def test_user_monitoring_enabled_add_user(self):
request_logs_before = self.__get_logs(action="request")
with self.__setting("Security.user_monitoring_enabled", True):
# Enable monitoring of test user
send(self.admin_misp_connector, "POST", f"/admin/users/monitor/{self.test_usr.id}", {
"value": 1,
})
logged_in = PyMISP(url, self.test_usr.authkey)
check_response(logged_in.get_user())
# Disable monitoring of test user
send(self.admin_misp_connector, "POST", f"/admin/users/monitor/{self.test_usr.id}", {
"value": 0,
})
request_logs_after = self.__get_logs(action="request")
self.assertGreater(len(request_logs_after), len(request_logs_before))
def test_log_paranoid(self):
request_logs_before = self.__get_logs(action="request")
with self.__setting("MISP.log_paranoid", True):
logged_in = PyMISP(url, self.test_usr.authkey)
check_response(logged_in.get_user())
request_logs_after = self.__get_logs(action="request")
self.assertGreater(len(request_logs_after), len(request_logs_before), "Number of logs should be greater")
def test_log_paranoid_include_post_body(self):
request_logs_before = self.__get_logs(action="request")
with self.__setting({
"MISP": {
"log_paranoid": True,
"log_paranoid_include_post_body": True,
}
}):
logged_in = PyMISP(url, self.test_usr.authkey)
check_response(logged_in.get_user())
request_logs_after = self.__get_logs(action="request")
self.assertGreater(len(request_logs_after), len(request_logs_before), "Number of logs should be greater")
def test_log_paranoid_skip_db(self):
request_logs_before = self.__get_logs(action="request")
with self.__setting({
"MISP": {
"log_paranoid": True,
"log_paranoid_skip_db": True,
}
}):
logged_in = PyMISP(url, self.test_usr.authkey)
check_response(logged_in.get_user())
request_logs_after = self.__get_logs(action="request")
# Number of logs should be same, because saving to database is disabled
self.assertEqual(len(request_logs_after), len(request_logs_before))
def test_log_auth_fail_multiple(self):
request_logs_before = self.__get_logs(action="auth_fail")
with self.assertRaises(PyMISPError):
PyMISP(url, "JCZDbBr3wYPlY0DrlQzoD8EWrcClGc0Dqu2yMYyE")
with self.assertRaises(PyMISPError):
PyMISP(url, "JCZDbBr3wYPlY0DrlQzoD8EWrcClGc0Dqu2yMYyE")
request_logs_after = self.__get_logs(action="auth_fail")
# Just one new record should be logged for multiple tries with same key
self.assertEqual(len(request_logs_after), len(request_logs_before) + 1)
def test_log_user_ips(self):
with self.__setting("MISP.log_user_ips", True):
logged_in = PyMISP(url, self.test_usr.authkey)
check_response(logged_in.get_user())
def test_log_user_ips_auth(self):
with self.__setting({
"MISP": {
"log_user_ips": True,
"log_user_ips_authkeys": True,
}
}):
logged_in = PyMISP(url, self.test_usr.authkey)
check_response(logged_in.get_user())
def test_username_in_response_header(self):
with self.__setting("Security.username_in_response_header", True):
logged_in = login(url, self.test_usr.email, self.test_usr_password)
self.assertIsInstance(logged_in, requests.Session)
response = logged_in.get(url + "/users/view/me.json")
self.assertIn("X-Username", response.headers)
self.assertEqual(self.test_usr.email, response.headers["X-Username"])
def test_username_in_response_header_api_access(self):
with self.__setting("Security.username_in_response_header", True):
logged_in = PyMISP(url, self.test_usr.authkey)
response = logged_in._prepare_request('GET', 'users/view/me')
self.assertIn("X-Username", response.headers)
self.assertEqual(self.test_usr.email + "/API/default", response.headers["X-Username"])
def test_username_in_response_header_advanced_api_access(self):
with self.__setting({
"Security": {
"advanced_authkeys": True,
"username_in_response_header": True,
}
}):
auth_key = self.__create_advanced_authkey(self.test_usr.id)
logged_in = PyMISP(url, auth_key["authkey_raw"])
response = logged_in._prepare_request('GET', 'users/view/me')
self.__delete_advanced_authkey(auth_key["id"])
self.assertIn("X-Username", response.headers)
self.assertEqual(f"{self.test_usr.email}/API/{auth_key['id']}", response.headers["X-Username"])
def test_sg_index_user_cannot_see(self): def test_sg_index_user_cannot_see(self):
org = self.__create_org() org = self.__create_org()
hidden_sg = self.__create_sharing_group() hidden_sg = self.__create_sharing_group()
@ -1119,6 +1339,11 @@ class TestSecurity(unittest.TestCase):
def __delete_advanced_authkey(self, key_id: int): def __delete_advanced_authkey(self, key_id: int):
return send(self.admin_misp_connector, "POST", f'authKeys/delete/{key_id}') return send(self.admin_misp_connector, "POST", f'authKeys/delete/{key_id}')
def __get_logs(self, action: str) -> List[MISPLog]:
response = self.admin_misp_connector.search_logs(action=action)
check_response(response)
return response
def __setting(self, key, value=None) -> MISPSetting: def __setting(self, key, value=None) -> MISPSetting:
if not isinstance(key, dict): if not isinstance(key, dict):
new_setting = {key: value} new_setting = {key: value}