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
/**
* 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('Controller', 'Controller');
App::uses('File', 'Utility');
@ -36,18 +13,17 @@ App::uses('RequestRearrangeTool', 'Tools');
* @package app.Controller
* @link http://book.cakephp.org/2.0/en/controllers.html#the-app-controller
*
* @throws ForbiddenException // TODO Exception
* @property ACLComponent $ACL
* @property RestResponseComponent $RestResponse
* @property CRUDComponent $CRUD
* @property IndexFilterComponent $IndexFilter
* @property RateLimitComponent $RateLimit
*/
class AppController extends Controller
{
public $defaultModel = '';
public $debugMode = false;
public $helpers = array('Utility', 'OrgImg', 'FontAwesome', 'UserName', 'DataPathCollector');
public $helpers = array('OrgImg', 'FontAwesome', 'UserName', 'DataPathCollector');
private $__queryVersion = '119';
public $pyMispVersion = '2.4.135';
@ -55,12 +31,11 @@ class AppController extends Controller
public $phprec = '7.4';
public $pythonmin = '3.6';
public $pythonrec = '3.7';
public $isApiAuthed = false;
private $isApiAuthed = false;
public $baseurl = '';
public $sql_dump = false;
private $isRest = 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
@ -72,6 +47,8 @@ class AppController extends Controller
);
protected $_legacyParams = array();
/** @var array */
public $userRole;
/** @var User */
public $User;
@ -114,14 +91,12 @@ class AppController extends Controller
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');
if ($customLogout) {
$this->Auth->logoutRedirect = $customLogout;
} else {
$this->Auth->logoutRedirect = Configure::read('MISP.baseurl') . '/users/login';
}
$this->Auth->logoutRedirect = $customLogout ?: ($this->baseurl . '/users/login');
$this->__sessionMassage();
if (Configure::read('Security.allow_cors')) {
// Add CORS headers
@ -152,8 +127,8 @@ class AppController extends Controller
$this->sql_dump = intval($this->params['named']['sql']);
}
$this->_setupDatabaseConnection();
$this->_setupDebugMode();
$this->_setupDatabaseConnection();
$this->set('ajax', $this->request->is('ajax'));
$this->set('queryVersion', $this->__queryVersion);
@ -166,17 +141,19 @@ class AppController extends Controller
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')) {
$this->loadModel('Server');
$this->Server->serverSettingsSaveValue('Security.salt', $this->User->generateRandomPassword(32));
}
// Check if the instance has a UUID, if not assign one.
if (!Configure::read('MISP.uuid')) {
$this->loadModel('Server');
$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();
$envvar = Configure::read('ApacheSecureAuth.apacheEnv');
if ($envvar && isset($_SERVER[$envvar])) {
@ -196,10 +173,9 @@ class AppController extends Controller
}
Configure::write('CurrentController', $this->params['controller']);
Configure::write('CurrentAction', $this->params['action']);
$versionArray = $this->{$this->modelClass}->checkMISPVersion();
$versionArray = $this->User->checkMISPVersion();
$this->mispVersion = implode('.', array_values($versionArray));
$this->Security->blackHoleCallback = 'blackHole';
$this->_setupBaseurl();
// send users away that are using ancient versions of IE
// Make sure to update this if IE 20 comes out :)
@ -227,232 +203,68 @@ class AppController extends Controller
// REST authentication
if ($this->_isRest() || $this->_isAutomation()) {
// disable CSRF for REST access
if (array_key_exists('Security', $this->components)) {
if (isset($this->components['Security'])) {
$this->Security->csrfCheck = false;
}
// 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)) {
$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) {
if ($this->__loginByAuthKey() === false || $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.');
}
} elseif (!$this->Session->read(AuthComponent::$sessionKey)) {
$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
if ($base_dir == '/') {
$base_dir = '';
}
$user = $this->Auth->user();
if ($user) {
Configure::write('CurrentUserId', $user['id']);
$this->__logAccess($user);
if ($this->Auth->user()) {
Configure::write('CurrentUserId', $this->Auth->user('id'));
$this->User->setMonitoring($this->Auth->user());
if (Configure::read('MISP.log_user_ips')) {
$redis = $this->{$this->modelClass}->setupRedis();
if ($redis) {
$redis->set('misp:ip_user:' . trim($_SERVER['REMOTE_ADDR']), $this->Auth->user('id'));
$redis->expire('misp:ip_user:' . trim($_SERVER['REMOTE_ADDR']), 60*60*24*30);
$redis->sadd('misp:user_ip:' . $this->Auth->user('id'), trim($_SERVER['REMOTE_ADDR']));
// Try to run updates
if ($user['Role']['perm_site_admin'] || (Configure::read('MISP.live') && !$this->_isRest())) {
$this->User->runUpdates();
}
// Put username to response header for webserver or proxy logging
if (Configure::read('Security.username_in_response_header')) {
$headerValue = $user['email'];
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())) {
$this->{$this->modelClass}->runUpdates();
if (!$this->__verifyUser($user)) {
$this->_stop(); // just for sure
}
$user = $this->Auth->user();
if (!isset($user['force_logout']) || $user['force_logout']) {
$this->loadModel('User');
$this->User->id = $this->Auth->user('id');
$this->User->saveField('force_logout', false);
if (isset($user['logged_by_authkey']) && $user['logged_by_authkey'] && !($this->_isRest() || $this->_isAutomation())) {
throw new ForbiddenException("When user is authenticated by authkey, just REST request can be processed");
}
if ($this->Auth->user('disabled')) {
$this->Log = ClassRegistry::init('Log');
$this->Log->create();
$log = array(
'org' => $this->Auth->user('Organisation')['name'],
'model' => 'User',
'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();
}
// Put token expiration time to response header that can be processed by automation tool
if (isset($user['authkey_expiration']) && $user['authkey_expiration']) {
$expiration = date('c', $user['authkey_expiration']);
$this->response->header('X-Auth-Key-Expiration', $expiration);
$this->RestResponse->setHeader('X-Auth-Key-Expiration', $expiration);
}
$this->set('default_memory_limit', ini_get('memory_limit'));
if (isset($this->Auth->user('Role')['memory_limit'])) {
if ($this->Auth->user('Role')['memory_limit'] !== '') {
ini_set('memory_limit', $this->Auth->user('Role')['memory_limit']);
}
if (isset($user['Role']['memory_limit']) && $user['Role']['memory_limit'] !== '') {
ini_set('memory_limit', $user['Role']['memory_limit']);
}
$this->set('default_max_execution_time', ini_get('max_execution_time'));
if (isset($this->Auth->user('Role')['max_execution_time'])) {
if ($this->Auth->user('Role')['max_execution_time'] !== '') {
ini_set('max_execution_time', $this->Auth->user('Role')['max_execution_time']);
}
if (isset($user['Role']['max_execution_time']) && $user['Role']['max_execution_time'] !== '') {
ini_set('max_execution_time', $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
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('mispVersion', "{$versionArray['major']}.{$versionArray['minor']}.0");
$this->set('mispVersionFull', $this->mispVersion);
$role = $this->getActions();
$this->set('me', $this->Auth->user());
$this->set('me', $user);
$role = $user['Role'];
$this->set('isAdmin', $role['perm_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('isAclModify', $role['perm_modify']);
$this->set('isAclModifyOrg', $role['perm_modify_org']);
@ -475,48 +287,26 @@ class AppController extends Controller
$this->set('aclComponent', $this->ACL);
$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 {
$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);
}
if ($this->Auth->user() && $this->_isSiteAdmin()) {
if (Configure::read('Session.defaults') == 'database') {
if (Configure::read('Session.defaults') === 'database') {
$db = ConnectionManager::getDataSource('default');
$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) {
@ -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
if ($this->Auth->user() && !$this->_isRest() && !$this->request->is('ajax')) {
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 {
$notifications = $this->{$this->modelClass}->populateNotifications($this->Auth->user(), 'fast');
$notifications = $this->User->populateNotifications($this->Auth->user(), 'fast');
}
$this->set('notifications', $notifications);
$this->loadModel('UserSetting');
$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')
));
$homepage = $this->User->UserSetting->getValueForUser($this->Auth->user('id'), '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()
{
if ($this->isApiAuthed && $this->_isRest() && $this->Session->started()) {
if ($this->isApiAuthed && $this->_isRest() && !Configure::read('Security.authkey_keep_session')) {
$this->Session->destroy();
}
}
@ -648,16 +736,17 @@ class AppController extends Controller
/*
* 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
$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
$baseurl = rtrim($baseurl, '/');
$this->loadModel('Server');
$this->Server->serverSettingsSaveValue('MISP.baseurl', $baseurl);
}
if (trim($baseurl) == 'http://') {
if (trim($baseurl) === 'http://') {
$this->Server->serverSettingsSaveValue('MISP.baseurl', '');
}
$this->baseurl = $baseurl;
@ -683,8 +772,6 @@ class AppController extends Controller
throw new BadRequestException('The request has been black-holed');
}
public $userRole = null;
protected function _isRest()
{
return $this->IndexFilter->isRest();
@ -692,12 +779,7 @@ class AppController extends Controller
protected function _isAutomation()
{
foreach ($this->automationArray as $controllerName => $controllerActions) {
if ($this->params['controller'] == $controllerName && in_array($this->params['action'], $controllerActions)) {
return true;
}
}
return false;
return $this->IndexFilter->isApiFunction($this->params['controller'], $this->params['action']);
}
/**
@ -829,34 +911,12 @@ class AppController extends Controller
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)
{
if (Configure::read('Security.advanced_authkeys')) {
$this->loadModel('AuthKey');
$user = $this->AuthKey->getAuthUserByAuthKey($authkey);
} else {
$this->loadModel('User');
$user = $this->User->getAuthUserByAuthKey($authkey);
}
@ -866,22 +926,16 @@ class AppController extends Controller
if (!$user['Role']['perm_auth']) {
return false;
}
if ($user['Role']['perm_site_admin']) {
$user['siteadmin'] = true;
}
$user['logged_by_authkey'] = true;
return $user;
}
public function checkExternalAuthUser($authkey)
{
$this->loadModel('User');
$user = $this->User->getAuthUserByExternalAuth($authkey);
if (empty($user)) {
return false;
}
if ($user['Role']['perm_site_admin']) {
$user['siteadmin'] = true;
}
return $user;
}
@ -1270,7 +1324,7 @@ class AppController extends Controller
$final = $this->$scope->restSearch($user, $returnFormat, $filters, false, false, $elementCounter, $renderView);
if (!empty($renderView) && !empty($final)) {
$this->layout = false;
$final = json_decode($final, true);
$final = json_decode($final->intoString(), true);
foreach ($final as $key => $data) {
$this->set($key, $data);
}
@ -1331,4 +1385,30 @@ class AppController extends Controller
}
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
App::uses('AppController', 'Controller');
/**
* @property AuthKey $AuthKey
*/
class AuthKeysController extends AppController
{
public $components = array(
@ -23,13 +26,22 @@ class AuthKeysController extends AppController
$this->set('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([
'filters' => ['User.username', 'authkey', 'comment', 'User.id'],
'quickFilters' => ['authkey', 'comment'],
'contain' => ['User'],
'filters' => ['User.email', 'authkey_start', 'authkey_end', 'comment', 'User.id'],
'quickFilters' => ['comment', 'authkey_start', 'authkey_end', 'User.email'],
'contain' => ['User.id', 'User.email'],
'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) {
if ($keyUsageEnabled) {
$lastUsed = $lastUsedById[$authKey['AuthKey']['id']];
$authKey['AuthKey']['last_used'] = $lastUsed;
}
unset($authKey['AuthKey']['authkey']);
}
return $authKeys;
@ -38,8 +50,12 @@ class AuthKeysController extends AppController
if ($this->IndexFilter->isRest()) {
return $this->restResponsePayload;
}
$this->set('metaGroup', $this->_isAdmin ? 'admin' : 'globalActions');
$this->set('metaAction', 'authkeys_index');
$this->set('title_for_layout', __('Auth Keys'));
$this->set('keyUsageEnabled', $keyUsageEnabled);
$this->set('menuData', [
'menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions',
'menuItem' => 'authkeys_index',
]);
}
public function delete($id)
@ -61,18 +77,22 @@ class AuthKeysController extends AppController
public function add($user_id = false)
{
$this->set('menuData', array('menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions', 'menuItem' => 'authKeyAdd'));
$params = [
'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 = [];
if (!$this->_isSiteAdmin()) {
$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) {
$selectConditions['AND'][] = ['User.id' => $user_id];
$params['override'] = ['user_id' => $user_id];
$params['override']['user_id'] = $user_id;
}
$this->CRUD->add($params);
if ($this->IndexFilter->isRest()) {
@ -86,6 +106,11 @@ class AuthKeysController extends AppController
])
];
$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)
@ -101,6 +126,15 @@ class AuthKeysController extends AppController
if ($this->IndexFilter->isRest()) {
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', [
'menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions',
'menuItem' => 'authKeyView',

View File

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

View File

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

View File

@ -6,7 +6,8 @@
class IndexFilterComponent extends Component
{
public $Controller = false;
/** @var Controller */
public $Controller;
public $isRest = null;
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;
}
@ -85,7 +86,7 @@ class IndexFilterComponent extends Component
return $this->isRest;
}
$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 (!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.');
@ -117,12 +118,13 @@ class IndexFilterComponent extends Component
}
/**
* @param string $controller
* @param string $action
* @return bool
*/
public function isApiFunction($controller, $action)
{
if (isset($this->Controller->automationArray[$controller]) && in_array($action, $this->Controller->automationArray[$controller])) {
return true;
}
return false;
return isset($this->Controller->automationArray[$controller]) && in_array($action, $this->Controller->automationArray[$controller], true);
}
}

View File

@ -519,11 +519,12 @@ class RestResponseComponent extends Component
} else {
$type = $format;
}
$dumpSql = !empty($this->Controller->sql_dump) && Configure::read('debug') > 1;
if (!$raw) {
if (is_string($response)) {
$response = array('message' => $response);
}
if (Configure::read('debug') > 1 && !empty($this->Controller->sql_dump)) {
if ($dumpSql) {
$this->Log = ClassRegistry::init('Log');
if ($this->Controller->sql_dump === 2) {
$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);
} else {
if (Configure::read('debug') > 1 && !empty($this->Controller->sql_dump)) {
if ($dumpSql) {
$this->Log = ClassRegistry::init('Log');
if ($this->Controller->sql_dump === 2) {
$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')) {
$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')));
$newsItems = $this->paginate();
$this->loadModel('User');
$currentUser = $this->User->find('first', array(
'recursive' => -1,
'conditions' => array('User.id' => $this->Auth->user('id')),
'fields' => array('User.newsread')
));
$newsread = $this->Auth->user('newsread');
foreach ($newsItems as $key => $item) {
if ($item['News']['date_created'] > $currentUser['User']['newsread']) {
if ($item['News']['date_created'] > $newsread) {
$newsItems[$key]['News']['new'] = true;
} else {
$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->loadModel('User');
$this->User->updateField($this->Auth->user(), 'newsread', time());
}
public function add()

View File

@ -1448,7 +1448,7 @@ class ServersController extends AppController
if (!isset($this->request->data['Server'])) {
$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()) {
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'));
}
} else {
if ($this->_isRest) {
if ($this->_isRest()) {
return $this->RestResponse->saveFailResponse('Servers', 'serverSettingsEdit', false, $result, $this->response->type());
} else {
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) {
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(
'recursive' => -1,
'conditions' => array('User.id' => $id),
@ -182,7 +174,7 @@ class UsersController extends AppController
}
if (!$abortPost) {
// 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()) {
$fieldList[] = 'email';
}
@ -217,7 +209,6 @@ class UsersController extends AppController
return $this->RestResponse->viewData($this->__massageUserObject($user), $this->response->type());
} else {
$this->Flash->success(__('The profile has been updated'));
$this->_refreshAuth();
$this->redirect(array('action' => 'view', $id));
}
} else {
@ -305,7 +296,6 @@ class UsersController extends AppController
return $this->RestResponse->saveSuccessResponse('User', 'change_pw', false, $this->response->type(), $message);
}
$this->Flash->success($message);
$this->_refreshAuth();
$this->redirect(array('action' => 'view', $id));
} else {
$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)) {
$this->request->data['User']['server_id'] = 0;
}
$fields = array();
$blockedFields = array('id', 'invited_by');
$fields = [];
$blockedFields = array('id', 'invited_by', 'date_modified');
if (!$this->_isSiteAdmin()) {
$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.');
}
}
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
$fieldsNewValues = array();
foreach ($fields as $field) {
if ($field != 'confirm_password') {
if ($field === 'date_modified') {
continue;
}
if ($field !== 'confirm_password') {
$newValue = $this->data['User'][$field];
if (gettype($newValue) == 'array') {
$newValueStr = '';
@ -1014,7 +1008,6 @@ class UsersController extends AppController
return $this->RestResponse->viewData($user, $this->response->type());
} else {
$this->Flash->success(__('The user has been saved'));
$this->_refreshAuth(); // in case we modify ourselves
$this->redirect(array('action' => 'index'));
}
} else {
@ -1252,16 +1245,9 @@ class UsersController extends AppController
// Events list
$url = $this->Session->consume('pre_login_requested_url');
if (empty($url)) {
$homepage = $this->User->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')
));
$homepage = $this->User->UserSetting->getValueForUser($this->Auth->user('id'), 'homepage');
if (!empty($homepage)) {
$url = $homepage['UserSetting']['value']['path'];
$url = $homepage['path'];
} else {
$url = array('controller' => 'events', 'action' => 'index');
}
@ -1309,7 +1295,6 @@ class UsersController extends AppController
}
if (!$this->_isRest()) {
$this->Flash->success(__('New authkey generated.', true));
$this->_refreshAuth();
$this->redirect($this->referer());
} else {
return $this->RestResponse->saveSuccessResponse('User', 'resetauthkey', $id, $this->response->type(), 'Authkey updated: ' . $newkey);
@ -1436,9 +1421,7 @@ class UsersController extends AppController
public function terms()
{
if ($this->request->is('post') || $this->request->is('put')) {
$this->User->id = $this->Auth->user('id');
$this->User->saveField('termsaccepted', true);
$this->_refreshAuth(); // refresh auth info
$this->User->updateField($this->Auth->user(), 'termsaccepted', true);
$this->Flash->success(__('You accepted the Terms and Conditions.'));
$this->redirect(array('action' => 'routeafterlogin'));
}
@ -2278,18 +2261,6 @@ class UsersController extends AppController
$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)
{
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);
$iocArray = [];
foreach ($tmpFile->csv($delimiter) as $row) {
foreach ($tmpFile->intoParsedCsv($delimiter) as $row) {
if (!empty($row[0][0]) && $row[0][0] === '#') { // Comment
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 $enclosure
@ -76,7 +76,7 @@ class TmpFileTool
* @return Generator
* @throws Exception
*/
public function csv($delimiter = ',', $enclosure = '"', $escape = "\\")
public function intoParsedCsv($delimiter = ',', $enclosure = '"', $escape = "\\")
{
$this->rewind();
$line = 0;
@ -88,15 +88,16 @@ class TmpFileTool
$line++;
yield $result;
}
fclose($this->tmpfile);
$this->tmpfile = null;
$this->close();
}
/**
* Returns generator of line from file.
*
* @return Generator
* @throws Exception
*/
public function lines()
public function intoLines()
{
$this->rewind();
while (!feof($this->tmpfile)) {
@ -106,24 +107,64 @@ class TmpFileTool
}
yield $result;
}
fclose($this->tmpfile);
$this->tmpfile = null;
$this->close();
}
/**
* @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
* @throws Exception
*/
public function finish()
public function intoString()
{
$this->rewind();
$final = stream_get_contents($this->tmpfile);
if ($final === false) {
$string = stream_get_contents($this->tmpfile);
if ($string === false) {
throw new Exception('Could not read from temporary file.');
}
fclose($this->tmpfile);
$this->tmpfile = null;
return $final;
$this->close();
return $string;
}
/**
* 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()
{
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()
{
$this->isOpen();
if (fseek($this->tmpfile, 0) === -1) {
throw new Exception('Could not seek to start of temporary file.');
}

View File

@ -1627,10 +1627,8 @@ class AppModel extends Model
break;
default:
return false;
break;
}
$now = new DateTime();
// switch MISP instance live to false
if ($liveOff) {
$this->Server = Classregistry::init('Server');
@ -1643,7 +1641,7 @@ class AppModel extends Model
$this->__setUpdateProgress(0, $total_update_count, $command);
$str_index_array = array();
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));
$flagStop = false;
@ -1679,10 +1677,10 @@ class AppModel extends Model
'email' => 'SYSTEM',
'action' => 'update_database',
'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)
));
$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) {
$errorMessage = $e->getMessage();
$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) {
$this->cleanCacheFiles();
}
if ($liveOff) {
$liveSetting = 'MISP.live';
$this->Server->serverSettingsSaveValue($liveSetting, true);
$this->Server->serverSettingsSaveValue('MISP.live', true);
}
if (!$flagStop && $errorCount == 0) {
$this->__postUpdate($command);
@ -2132,11 +2129,20 @@ class AppModel extends Model
}
}
if ($requiresLogout) {
$this->updateDatabase('destroyAllSessions');
$this->refreshSessions();
}
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)
{
$updateProgress = $this->getUpdateProgress();
@ -2735,7 +2741,7 @@ class AppModel extends Model
{
static $versionArray;
if ($versionArray === null) {
$file = new File(ROOT . DS . 'VERSION.json', true);
$file = new File(ROOT . DS . 'VERSION.json');
$versionArray = $this->jsonDecode($file->read());
$file->close();
}

View File

@ -4697,7 +4697,7 @@ class Attribute extends AppModel
$elementCounter = $this->__iteratedFetch($user, $params, $loop, $tmpfile, $exportTool, $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_raw'] = $authkey;
$this->authkey_raw = $authkey;
$validity = Configure::read('Security.advanced_authkeys_validity');
if (empty($this->data['AuthKey']['expiration'])) {
$this->data['AuthKey']['expiration'] = 0;
$this->data['AuthKey']['expiration'] = $validity ? strtotime("+$validity days") : 0;
} 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;
}
/**
* @param string $authkey
* @return array|false
*/
public function getAuthUserByAuthKey($authkey)
{
$start = substr($authkey, 0, 4);
$end = substr($authkey, -4);
$existing_authkeys = $this->find('all', [
'recursive' => -1,
'fields' => ['authkey', 'user_id'],
'fields' => ['id', 'authkey', 'user_id', 'expiration'],
'conditions' => [
'OR' => [
'expiration >' => time(),
@ -70,7 +86,12 @@ class AuthKey extends AppModel
$passwordHasher = $this->getHasher();
foreach ($existing_authkeys as $existing_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;
@ -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
*/

View File

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

View File

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

View File

@ -336,6 +336,11 @@ class Log extends AppModel
$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
if ($this->syslog === null) {
if (Configure::read('Security.syslog')) {

View File

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

View File

@ -1,6 +1,9 @@
<?php
App::uses('AppModel', 'Model');
/**
* @property User $User
*/
class Role extends AppModel
{
public $validate = array(
@ -232,6 +235,18 @@ class Role extends AppModel
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)
{
foreach ($results as $key => $val) {

View File

@ -865,6 +865,15 @@ class Server extends AppModel
'type' => 'boolean',
'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(
'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.'),
@ -1156,7 +1165,7 @@ class Server extends AppModel
'test' => 'testForPositiveInteger',
'type' => 'numeric',
'null' => true,
]
],
),
'GnuPG' => array(
'branch' => 1,
@ -1343,6 +1352,24 @@ class Server extends AppModel
'test' => 'testBool',
'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' => [
'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.'),
@ -1585,7 +1612,16 @@ class Server extends AppModel
'test' => 'testBool',
'type' => 'boolean',
'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(
'branch' => 1,
@ -4916,6 +4952,7 @@ class Server extends AppModel
public function dbSchemaDiagnostic()
{
$this->AdminSetting = ClassRegistry::init('AdminSetting');
$actualDbVersion = $this->AdminSetting->find('first', array(
'conditions' => array('setting' => 'db_version')
))['AdminSetting']['value'];
@ -6599,7 +6636,7 @@ class Server extends AppModel
} catch (Exception $e) {
$this->Log = ClassRegistry::init('Log');
$this->Log->create();
$message = __('Could not reset fetch remote user account.');
$message = __('Could not fetch remote user account.');
$this->Log->save(array(
'org' => 'SYSTEM',
'model' => 'Server',
@ -6612,14 +6649,18 @@ class Server extends AppModel
return $message;
}
if ($response->isOk()) {
$user = json_decode($response->body, true);
$user = $this->jsonDecode($response->body);
if (!empty($user['User'])) {
$result = array(
'Email' => $user['User']['email'],
'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'
);
return $result;
$results = [
__('User') => $user['User']['email'],
__('Role name') => isset($user['Role']['name']) ? $user['Role']['name'] : __('Unknown, outdated instance'),
__('Sync flag') => isset($user['Role']['perm_sync']) ? ($user['Role']['perm_sync'] ? __('Yes') : __('No')) : __('Unknown, outdated instance'),
];
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 {
return __('No user object received in response.');
}

View File

@ -140,7 +140,13 @@ class Sighting extends AppModel
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(
'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')
),
'Event' => array(
'fields' => array('Event.id', 'Event.uuid', 'Event.orgc_id', 'Event.org_id', 'Event.info'),
'Orgc' => array(
'fields' => array('Orgc.name')
)
'fields' => $withEvent ? ['Event.id', 'Event.uuid', 'Event.orgc_id', 'Event.org_id', 'Event.info'] : ['Event.org_id'],
)
),
'conditions' => array('Sighting.id' => $id)
@ -161,11 +164,7 @@ class Sighting extends AppModel
return array();
}
if (!isset($event)) {
$event = array('Event' => $sighting['Event']);
}
$ownEvent = $user['Role']['perm_site_admin'] || $event['Event']['org_id'] == $user['org_id'];
$ownEvent = $user['Role']['perm_site_admin'] || $sighting['Event']['org_id'] == $user['org_id'];
if (!$ownEvent) {
$sightingPolicy = $this->sightingsPolicy();
// 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');
if ($anonymise) {
if ($sighting['Sighting']['org_id'] != $user['org_id']) {
unset($sighting['Sighting']['org_id']);
unset($sighting['Organisation']);
}
if ($anonymise && $sighting['Sighting']['org_id'] != $user['org_id']) {
unset($sighting['Sighting']['org_id']);
}
// rearrange it to match the event format of fetchevent
if (isset($sighting['Organisation'])) {
$sighting['Sighting']['Organisation'] = $sighting['Organisation'];
unset($sighting['Organisation']);
}
$result = array(
'Sighting' => $sighting['Sighting']
);
$result['Sighting']['Event'] = $sighting['Event'];
$result['Sighting']['Attribute'] = $sighting['Attribute'];
if (!empty($sighting['Organisation'])) {
$result['Sighting']['Organisation'] = $sighting['Organisation'];
if ($withEvent) {
$result['Sighting']['Event'] = $sighting['Event'];
}
$result['Sighting']['Attribute'] = $sighting['Attribute'];
return $result;
}
@ -332,7 +329,7 @@ class Sighting extends AppModel
'fields' => ['org_id', 'attribute_id', 'type', 'date', 'last_timestamp', 'sighting_count'],
'recursive' => -1,
'group' => ['org_id', 'attribute_id', 'type', 'date'],
'order' => ['date_sighting'], // from oldest
'order' => ['date'], // from oldest
));
unset(
$this->virtualFields['date'],
@ -369,7 +366,7 @@ class Sighting extends AppModel
'conditions' => $conditions,
'fields' => [ucfirst($context) . 'Tag.tag_id', 'date', 'sighting_count'],
'group' => [ucfirst($context) . 'Tag.id', 'date'],
'order' => ['date_sighting'], // from oldest
'order' => ['date'], // from oldest
]);
unset($this->virtualFields['date'], $this->virtualFields['sighting_count']);
return $sightings;
@ -659,6 +656,10 @@ class Sighting extends AppModel
return $result;
}
/**
* @return bool
* @deprecated
*/
public function addUuids()
{
$sightings = $this->find('all', array(
@ -864,48 +865,27 @@ class Sighting extends AppModel
}
// fetch sightings matching the query
$sightings = $this->find('list', array(
$sightingIds = $this->find('list', array(
'recursive' => -1,
'conditions' => $conditions,
'fields' => array('id'),
'contain' => $contain,
));
$filters['requested_attributes'] = array('id', 'attribute_id', 'event_id', 'org_id', 'date_sighting', 'uuid', 'source', 'type');
// apply ACL and sighting policies
$allowedSightings = array();
$additional_attribute_added = false;
$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;
}
$includeAttribute = isset($filters['includeAttribute']) && $filters['includeAttribute'];
$includeEvent = isset($filters['includeEvent']) && $filters['includeEvent'];
$requestedAttributes = ['id', 'attribute_id', 'event_id', 'org_id', 'date_sighting', 'uuid', 'source', 'type'];
if ($includeAttribute) {
$requestedAttributes = array_merge($requestedAttributes, ['attribute_uuid', 'attribute_type', 'attribute_category', 'attribute_to_ids', 'attribute_value']);
}
$params = array(
'conditions' => array(), //result already filtered
);
if ($includeEvent) {
$requestedAttributes = array_merge($requestedAttributes, ['event_uuid', 'event_orgc_id', 'event_org_id', 'event_info', 'event_Orgc_name']);
}
$filters['requested_attributes'] = $requestedAttributes;
$exportToolParams = array(
'user' => $user,
'params' => $params,
'params' => ['conditions' => []], //result already filtered
'returnFormat' => $returnFormat,
'scope' => 'Sighting',
'filters' => $filters
@ -913,21 +893,22 @@ class Sighting extends AppModel
$tmpfile = new TmpFileTool();
$tmpfile->write($exportTool->header($exportToolParams));
$separator = $exportTool->separator($exportToolParams);
$temp = '';
$i = 0;
foreach ($allowedSightings as $sighting) {
$temp .= $exportTool->handler($sighting, $exportToolParams);
if ($temp !== '') {
if ($i != count($allowedSightings) -1) {
$temp .= $exportTool->separator($exportToolParams);
foreach ($sightingIds as $sightingId) {
// apply ACL and sighting policies
$sighting = $this->getSighting($sightingId, $user, $includeEvent);
if (!empty($sighting)) {
$sighting['Sighting']['value'] = $sighting['Sighting']['Attribute']['value'];
if (!$includeAttribute) {
unset($sighting['Sighting']['Attribute']);
}
$tmpfile->writeWithSeparator($exportTool->handler($sighting, $exportToolParams), $separator);
}
$i++;
}
$tmpfile->write($temp);
$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
public function getAuthUser($id)
{
$user = $this->getUserById($id);
if (empty($user)) {
return $user;
if (empty($id)) {
throw new InvalidArgumentException('Invalid user ID.');
}
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
public function getAuthUserByAuthkey($id)
public function getAuthUserByAuthkey($authkey)
{
$conditions = array('User.authkey' => $id);
$user = $this->find('first', array('conditions' => $conditions, 'recursive' => -1,'contain' => array('Organisation', 'Role', 'Server')));
if (empty($user)) {
return $user;
if (empty($authkey)) {
throw new InvalidArgumentException('Invalid user auth key.');
}
return $this->rearrangeToAuthForm($user);
$conditions = array('User.authkey' => $authkey);
return $this->getAuthUserByConditions($conditions);
}
public function getAuthUserByExternalAuth($auth_key)
{
if (empty($auth_key)) {
throw new InvalidArgumentException('Invalid user external auth key.');
}
$conditions = array(
'User.external_auth_key' => $auth_key,
'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,
'recursive' => -1,
'contain' => array(
'contain' => [
'Organisation',
'Role',
'Server'
)
));
'Server',
],
]);
if (empty($user)) {
return $user;
}
@ -696,9 +707,6 @@ class User extends AppModel
$user['User']['Role'] = $user['Role'];
$user['User']['Organisation'] = $user['Organisation'];
$user['User']['Server'] = $user['Server'];
if (isset($user['UserSetting'])) {
$user['User']['UserSetting'] = $user['UserSetting'];
}
return $user['User'];
}
@ -967,7 +975,7 @@ class User extends AppModel
if ($result) {
$this->id = $user['User']['id'];
$this->saveField('password', $password);
$this->saveField('change_pw', '1');
$this->updateField($user['User'], 'change_pw', 1);
if ($simpleReturn) {
return true;
} else {
@ -1142,7 +1150,7 @@ class User extends AppModel
if (empty(Configure::read('Security.advanced_authkeys'))) {
$oldKey = $this->data['User']['authkey'];
$newkey = $this->generateAuthKey();
$this->saveField('authkey', $newkey);
$this->updateField($updatedUser['User'], 'authkey', $newkey);
$this->extralog(
$user,
'reset_auth_key',
@ -1286,24 +1294,6 @@ class User extends AppModel
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) {
$user = array(
'email' => $registration['data']['email'],
@ -1403,7 +1393,7 @@ class User extends AppModel
$name => $value,
], true, ['id', $name, 'date_modified']);
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)
{
$setting = $this->find('first', array(
'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;
return $this->getValueForUser($user['id'], 'default_restsearch_parameters') ?: [];
}
public function getTagNumericalValueOverride($userId)
{
$setting = $this->find('first', array(
'recursive' => -1,
'conditions' => array(
'UserSetting.user_id' => $userId,
'UserSetting.setting' => 'tag_numerical_value_override'
)
));
$parameters = array();
if (!empty($setting)) {
$parameters = $setting['UserSetting']['value'];
}
return $parameters;
return $this->getValueForUser($userId, 'tag_numerical_value_override') ?: [];
}
/**
* @param int $userId
* @param string $setting
* @return mixed|null
*/
public function getValueForUser($userId, $setting)
{
$output = $this->find('first', array(
'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
echo $this->element('genericElements/Form/genericForm', [
'data' => [
'title' => __('Add auth key'),
@ -13,11 +12,12 @@ echo $this->element('genericElements/Form/genericForm', [
],
[
'field' => 'comment',
'label' => __('Comment'),
'class' => 'span6'
],
[
'field' => 'expiration',
'label' => 'Expiration',
'label' => __('Expiration (%s)', $validity ? __('keep empty for maximal validity of %s days', $validity) : __('keep empty for indefinite')),
'class' => 'datepicker span6',
'placeholder' => "YYYY-MM-DD",
'type' => 'text'

View File

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

View File

@ -1,4 +1,17 @@
<?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(
'genericElements/SingleViews/single_view',
[
@ -14,10 +27,21 @@ echo $this->element(
'path' => 'AuthKey.uuid',
],
[
'key' => __('Auth key'),
'key' => __('Auth Key'),
'path' => 'AuthKey',
'type' => 'authkey'
],
[
'key' => __('User'),
'path' => 'User.id',
'pathName' => 'User.email',
'model' => 'users',
'type' => 'model'
],
[
'key' => __('Comment'),
'path' => 'AuthKey.comment'
],
[
'key' => __('Created'),
'path' => 'AuthKey.created',
@ -29,18 +53,24 @@ echo $this->element(
'type' => 'expiration'
],
[
'key' => __('User'),
'path' => 'User.id',
'pathName' => 'User.email',
'model' => 'users',
'type' => 'model'
'key' => __('Key usage'),
'type' => 'sparkline',
'path' => 'AuthKey.id',
'csv' => [
'data' => $keyUsageCsv,
],
'requirement' => isset($keyUsage),
],
[
'key' => __('Comment'),
'path' => 'AuthKey.comment'
'key' => __('Last used'),
'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));
$data = '<span class="red bold" title="' . $title . '">' . __('Expired') . '</span>';
} else {
$diffInDays = floor(($data - time()) / 3600 * 24);
$diffInDays = floor(($data - time()) / (3600 * 24));
$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>';
}
}

View File

@ -20,9 +20,9 @@
$title = __('Expired at %s', date('Y-m-d H:i:s', $data));
$data = '<span class="red bold" title="' . $title . '">' . __('Expired') . '</span>';
} else {
$diffInDays = floor(($data - time()) / 3600 * 24);
$diffInDays = floor(($data - time()) / (3600 * 24));
$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>';
}
}

View File

@ -1,5 +1,5 @@
<?php
if (!empty($field['raw'])) {
if (isset($field['raw'])) {
$string = $field['raw'];
} else {
$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 = '';
if (!empty($fields)) {
foreach ($fields as $field) {
if (isset($field['requirement']) && !$field['requirement']) {
continue;
}
if (empty($field['type'])) {
$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',
'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(
'label' => __('Send credentials automatically'),
'type' => 'checkbox',

View File

@ -81,13 +81,13 @@
echo $this->Form->input('termsaccepted', array('type' => 'checkbox', 'label' => __('Terms accepted')));
echo $this->Form->input('change_pw', [
'type' => 'checkbox',
'label' => __('User must change password after next login'),
'label' => __('User must change password'),
'disabled' => !$canChangePassword,
'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('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>';
?>
</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 class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">
<span aria-hidden="true">&times;</span>
</button>
<h3 id="genericModalLabel"><?= __('Delete %s', Inflector::singularize(Inflector::humanize($this->params['controller']))) ?></h3>
<h3 id="genericModalLabel"><?= __('Delete %s', h($title)) ?></h3>
</div>
<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 class="modal-footer">
<?= $this->Form->postLink(
@ -20,7 +23,7 @@
</div>
<script type="text/javascript">
$(document).keydown(function(e) {
if(e.which === 13 && e.ctrlKey) {
if (e.which === 13 && e.ctrlKey) {
$('.button-execute').click();
}
});

View File

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

View File

@ -1,9 +1,11 @@
#!/usr/bin/env python3
import os
import sys
import time
import json
import datetime
import unittest
from typing import Union
from typing import Union, List
import urllib3 # type: ignore
import logging
import uuid
@ -14,7 +16,7 @@ from lxml.html import fromstring
from enum import Enum
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
except ImportError:
if sys.version_info < (3, 6):
@ -45,7 +47,7 @@ def check_response(response):
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()
r = session.get(url)
@ -77,7 +79,7 @@ def login(url: str, email: str, password: str) -> bool:
r = r.json()
if email != r["User"]["email"]:
raise Exception(r) # logged in as different user
return True
return session
class MISPSetting:
@ -166,7 +168,7 @@ class TestSecurity(unittest.TestCase):
# Try to connect as user to check if everything works
PyMISP(url, cls.test_usr.authkey)
# 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
def tearDownClass(cls):
@ -179,8 +181,6 @@ class TestSecurity(unittest.TestCase):
def setUp(self):
# Do not show warning about not closed resources, because that something we want
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):
session = requests.Session()
@ -302,7 +302,7 @@ class TestSecurity(unittest.TestCase):
self.assertFalse(updated_user.disabled)
# 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):
# Disable user
@ -327,7 +327,7 @@ class TestSecurity(unittest.TestCase):
self.assertFalse(login(url, self.test_usr.email, self.test_usr_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):
with self.__setting("MISP.live", False):
@ -342,6 +342,7 @@ class TestSecurity(unittest.TestCase):
with self.__setting("Security.advanced_authkeys", True):
# Create advanced authkey
auth_key = self.__create_advanced_authkey(self.test_usr.id)
self.assertNotIn("authkey", auth_key)
# Try to login
logged_in = PyMISP(url, auth_key["authkey_raw"])
@ -374,6 +375,46 @@ class TestSecurity(unittest.TestCase):
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):
with self.__setting("Security.advanced_authkeys", True):
# Create advanced authkey
@ -441,6 +482,46 @@ class TestSecurity(unittest.TestCase):
self.__delete_advanced_authkey(auth_key["id"])
# 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):
with self.__setting("Security.advanced_authkeys", True):
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"])
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):
new_email = 'testusr@user' + random() + '.local'
@ -530,7 +617,7 @@ class TestSecurity(unittest.TestCase):
# Try to change email as org admin
new_email = 'testusr@user' + random() + '.local'
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):
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()))
# 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):
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)
# 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):
with self.__setting("MISP.disable_user_password_change", True):
self.org_admin_misp_connector.update_user({"password": str(uuid.uuid4())}, self.test_usr.id)
# 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):
user = MISPUser()
@ -784,7 +871,7 @@ class TestSecurity(unittest.TestCase):
def test_shibb_form_login(self):
with self.__setting(self.__default_shibb_config()):
# 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):
with self.__setting(self.__default_shibb_config()):
@ -819,6 +906,139 @@ class TestSecurity(unittest.TestCase):
with self.__setting(config):
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):
org = self.__create_org()
hidden_sg = self.__create_sharing_group()
@ -1119,6 +1339,11 @@ class TestSecurity(unittest.TestCase):
def __delete_advanced_authkey(self, key_id: int):
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:
if not isinstance(key, dict):
new_setting = {key: value}