Compare commits

...

9 Commits

Author SHA1 Message Date
Jakub Onderka 902c99ac82
Merge pull request #9690 from JakubOnderka/opt_disabled
new: [security] Make possible to disable (T/H)OTP
2024-04-26 13:40:56 +02:00
Jakub Onderka bbb5ee96ab
Merge pull request #9700 from JakubOnderka/oidc-issuer-fix
fix: [oidc] Fix issuer if not set
2024-04-26 13:40:38 +02:00
Jakub Onderka 34c85cfe7e fix: [oidc] Fix issuer if not set 2024-04-22 16:57:06 +02:00
Jakub Onderka 9ea64750bc new: [test] Security test for OTP disabled 2024-04-20 14:27:37 +02:00
Jakub Onderka 97e6224755 new: [test] Security test for forget password 2024-04-20 14:27:37 +02:00
Jakub Onderka b5100dcedd chg: [test] Avoid sleep for 6 seconds 2024-04-20 14:27:37 +02:00
Jakub Onderka 0ca6a47ef8 chg: [acl] Move site admin check as last check 2024-04-20 14:27:37 +02:00
Jakub Onderka d5ba5af530 chg: [security] Disable resetting password when password change is disabled 2024-04-20 14:27:37 +02:00
Jakub Onderka 79f6124bd2 new: [security] Make possible to disable (T/H)OTP
This is useful if MISP is connected to identity provider that already provides strong authentication
2024-04-20 14:27:35 +02:00
9 changed files with 294 additions and 238 deletions

View File

@ -1557,7 +1557,7 @@ class AppController extends Controller
* Close session without writing changes to them and return current user.
* @return array
*/
protected function _closeSession()
protected function _closeSession($saveSession = false)
{
$user = $this->Auth->user();
@ -1566,7 +1566,11 @@ class AppController extends Controller
AuthComponent::$sessionKey = null;
$this->Auth->login($user);
session_abort();
if ($saveSession) {
@session_write_close();
} else {
session_abort();
}
return $user;
}

View File

@ -792,12 +792,12 @@ class ACLComponent extends Component
'discardRegistrations' => array(),
'downloadTerms' => array('*'),
'edit' => array('self_management_enabled'),
'email_otp' => array('*'),
'forgot' => array('*'),
'otp' => array('*'),
'hotp' => array('*'),
'totp_new' => array('*'),
'totp_delete' => array('perm_admin'),
'email_otp' => array('otp_enabled'),
'forgot' => ['AND' => ['password_forgotten_enabled', 'password_change_enabled']],
'otp' => ['otp_enabled'],
'hotp' => ['otp_enabled'],
'totp_new' => ['otp_enabled'],
'totp_delete' => ['AND' => ['perm_admin', 'otp_enabled']],
'searchGpgKey' => array('*'),
'fetchGpgKey' => array('*'),
'histogram' => array('*'),
@ -806,7 +806,7 @@ class ACLComponent extends Component
'logout' => array('*'),
'logout401' => array('*'),
'notificationSettings' => ['*'],
'password_reset' => array('*'),
'password_reset' => ['AND' => ['password_forgotten_enabled', 'password_change_enabled']],
'register' => array('*'),
'registrations' => array(),
'resetAllSyncAuthKeys' => array(),
@ -913,19 +913,31 @@ class ACLComponent extends Component
};
$this->dynamicChecks['self_management_enabled'] = function (array $user) {
if (Configure::read('MISP.disableUserSelfManagement') && !$user['Role']['perm_admin']) {
throw new MethodNotAllowedException('User self-management has been disabled on this instance.');
throw new ForbiddenException('User self-management has been disabled on this instance.');
}
return true;
};
$this->dynamicChecks['password_change_enabled'] = function (array $user) {
if (Configure::read('MISP.disable_user_password_change')) {
throw new MethodNotAllowedException('User password change has been disabled on this instance.');
throw new ForbiddenException('User password change has been disabled on this instance.');
}
return true;
};
$this->dynamicChecks['otp_enabled'] = function (array $user) {
if (Configure::read('Security.otp_disabled')) {
throw new ForbiddenException('OTP has been disabled on this instance.');
}
return true;
};
$this->dynamicChecks['password_forgotten_enabled'] = function (array $user) {
if (empty(Configure::read('Security.allow_password_forgotten'))) {
throw new ForbiddenException('Password reset has been disabled on this instance.');
}
return true;
};
$this->dynamicChecks['add_user_enabled'] = function (array $user) {
if (Configure::read('MISP.disable_user_add')) {
throw new MethodNotAllowedException('Adding users has been disabled on this instance.');
throw new ForbiddenException('Adding users has been disabled on this instance.');
}
return true;
};
@ -1125,6 +1137,7 @@ class ACLComponent extends Component
*
* @param array $user
* @param array $analystData
* @param string $modelType
* @return bool
*/
public function canEditAnalystData(array $user, array $analystData, $modelType): bool
@ -1239,7 +1252,7 @@ class ACLComponent extends Component
$this->checkAccess($user, $controller, $action, false);
} catch (NotFoundException $e) {
throw new RuntimeException("Invalid controller '$controller' specified.", 0, $e);
} catch (MethodNotAllowedException $e) {
} catch (ForbiddenException $e) {
return false;
}
return true;
@ -1259,7 +1272,7 @@ class ACLComponent extends Component
* @param bool $checkLoggedActions
* @return true
* @throws NotFoundException
* @throws MethodNotAllowedException
* @throws ForbiddenException
*/
public function checkAccess($user, $controller, $action, $checkLoggedActions = true)
{
@ -1268,9 +1281,6 @@ class ACLComponent extends Component
if ($checkLoggedActions) {
$this->__checkLoggedActions($user, $controller, $action);
}
if ($user && $user['Role']['perm_site_admin']) {
return true;
}
if (!isset(self::ACL_LIST[$controller])) {
throw new NotFoundException('Invalid controller.');
}
@ -1316,7 +1326,12 @@ class ACLComponent extends Component
return true;
}
}
throw new MethodNotAllowedException('You do not have permission to use this functionality.');
// Dynamic checks can raise forbidden exception even for site admins, so we have to check permission for site
// admin as last thing.
if ($user && $user['Role']['perm_site_admin']) {
return true;
}
throw new ForbiddenException('You do not have permission to use this functionality.');
}
private function __findAllFunctions()

View File

@ -1,5 +1,5 @@
<?php
App::uses('AppController', 'Controller', 'OTPHP\TOTP');
App::uses('AppController', 'Controller');
/**
* @property User $User
@ -1811,6 +1811,7 @@ class UsersController extends AppController
$this->Flash->error(__("The required PHP libraries to support TOTP are not installed. Please contact your administrator to address this."));
$this->redirect($this->referer());
}
// only allow the users themselves to generate a TOTP secret.
// If TOTP is enforced they will be invited to generate it at first login
$user = $this->User->find('first', array(
@ -1880,8 +1881,9 @@ class UsersController extends AppController
$this->set('secret', $secret);
}
public function totp_delete($id) {
if ($this->request->is('post') || $this->request->is('delete')) {
public function totp_delete($id)
{
if ($this->request->is(['post', 'delete'])) {
$user = $this->User->find('first', array(
'conditions' => $this->__adminFetchConditions($id),
'recursive' => -1,
@ -1988,8 +1990,7 @@ class UsersController extends AppController
// shows some statistics about the instance
public function statistics($page = 'data')
{
$user = $this->Auth->user();
@session_write_close(); // loading this page can take long time, so we can close session
$user = $this->_closeSession(true); // loading this page can take long time, so we can close session
if (!$this->_isRest()) {
$pages = [
@ -3187,10 +3188,6 @@ class UsersController extends AppController
public function forgot()
{
if (empty(Configure::read('Security.allow_password_forgotten'))) {
$this->Flash->error(__('This feature is disabled.'));
$this->redirect('/');
}
if (!empty($this->Auth->user()) && !$this->_isRest()) {
$this->Flash->info(__('You are already logged in, no need to ask for a password reset. Log out first.'));
$this->redirect('/');
@ -3216,10 +3213,6 @@ class UsersController extends AppController
public function password_reset($token)
{
if (empty(Configure::read('Security.allow_password_forgotten'))) {
$this->Flash->error(__('This feature is disabled.'));
$this->redirect('/');
}
$this->loadModel('Server');
$this->set('complexity', !empty(Configure::read('Security.password_policy_complexity')) ? Configure::read('Security.password_policy_complexity') : $this->Server->serverSettings['Security']['password_policy_complexity']['value']);
$this->set('length', !empty(Configure::read('Security.password_policy_length')) ? Configure::read('Security.password_policy_length') : $this->Server->serverSettings['Security']['password_policy_length']['value']);
@ -3236,7 +3229,7 @@ class UsersController extends AppController
$this->redirect('/');
}
}
if ($this->request->is('post') || $this->request->is('put')) {
if ($this->request->is(['post', 'put'])) {
$abortPost = false;
return $this->__pw_change(['User' => $user], 'password_reset', $abortPost, $token, true);
}

View File

@ -6636,6 +6636,14 @@ class Server extends AppModel
'type' => 'boolean',
'null' => true,
],
'otp_disabled' => [
'level' => self::SETTING_OPTIONAL,
'description' => __('Disable TOTP on this instance.'),
'value' => false,
'test' => 'testBool',
'type' => 'boolean',
'null' => true,
],
'otp_required' => array(
'level' => 2,
'description' => __('Require authentication with OTP. Users that do not have (T/H)OTP configured will be forced to create a token at first login. You cannot use it in combination with external authentication plugins.'),

View File

@ -2124,11 +2124,11 @@ class User extends AppModel
return true;
} else {
return $this->forgot($email);
return $this->forgot($email, $ip);
}
}
public function forgot($email, $ip, $jobId = null)
public function forgot($email, $ip)
{
$user = $this->find('first', [
'recursive' => -1,
@ -2140,9 +2140,8 @@ class User extends AppModel
if (empty($user)) {
return false;
}
$redis = $this->setupRedis();
$token = RandomTool::random_str(true, 40, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
$redis->set('misp:forgot:' . $token, $user['User']['id'], ['nx', 'ex' => 600]);
RedisTool::init()->set('misp:forgot:' . $token, $user['User']['id'], ['nx', 'ex' => 600]);
$baseurl = Configure::check('MISP.external_baseurl') ? Configure::read('MISP.external_baseurl') : Configure::read('MISP.baseurl');
$body = __(
"Dear MISP user,\n\nyou have requested a password reset on the MISP instance at %s. Click the link below to change your password.\n\n%s\n\nThe link above is only valid for 10 minutes, feel free to request a new one if it has expired.\n\nIf you haven't requested a password reset, reach out to your admin team and let them know that someone has attempted it in your stead.\n\nMake sure you keep the contents of this e-mail confidential, do NOT ever forward it as it contains a reset token that is equivalent of a password if acted upon. The IP used to trigger the request was: %s\n\nBest regards,\nYour MISP admin team",

View File

@ -302,7 +302,7 @@ class Oidc
$providerUrl = $this->getConfig('provider_url');
$clientId = $this->getConfig('client_id');
$clientSecret = $this->getConfig('client_secret');
$issuer = $this->getConfig('issuer', $providerUrl);
$issuer = $this->getConfig('issuer', null, false);
if (class_exists("\JakubOnderka\OpenIDConnectClient")) {
$oidc = new \JakubOnderka\OpenIDConnectClient($providerUrl, $clientId, $clientSecret, $issuer);
@ -503,13 +503,15 @@ class Oidc
/**
* @param string $config
* @param mixed|null $default
* @param bool $required When true and variable is not set, RuntimeException will be thrown
* @return mixed
* @throws RuntimeException when config option is not set
*/
private function getConfig($config, $default = null)
private function getConfig($config, $default = null, $required = true)
{
$value = Configure::read("OidcAuth.$config");
if ($value === null) {
if ($default === null) {
if ($default === null && $required) {
throw new RuntimeException("Config option `OidcAuth.$config` is not set.");
}
return $default;

View File

@ -152,6 +152,7 @@
'class' => 'short',
'data_path' => 'User.totp',
'colors' => true,
'requirement' => empty(Configure::read('Security.otp_disabled')),
),
array(
'name' => '',

View File

@ -18,211 +18,215 @@ foreach ($notificationTypes as $notificationType => $description) {
}
$notificationsHtml .= '</table>';
$isTotp = isset($user['User']['totp']) ? true : false;
$boolean = sprintf(
'<span class="%s">%s</span>',
$isTotp ? 'label label-success label-padding' : 'label label-important label-padding',
$isTotp ? __('Yes') : __('No'));
$totpHtml = $boolean;
$totpHtml .= (!$isTotp && !$admin_view ? $this->Html->link(__('Generate'), array('action' => 'totp_new')) : '');
$totpHtml .= ($isTotp && !$admin_view ? $this->Html->link(__('View paper tokens'), array('action' => 'hotp', $user['User']['id'])): '');
$tableData = [
array('key' => __('ID'), 'value' => $user['User']['id']),
array(
'key' => __('Email'),
'html' => h($user['User']['email']) . ($admin_view ? sprintf(
' <a class="fas fa-envelope" style="color: #333" href="%s/admin/users/quickEmail/%s" title="%s"></a>',
$baseurl,
h($user['User']['id']),
__('Send email to user')
) : ''),
),
array(
'key' => __('Organisation'),
'html' => $this->OrgImg->getNameWithImg($user),
),
array(
'key' => __('Role'),
'html' => $this->Html->link($user['Role']['name'], array('controller' => 'roles', 'action' => 'view', $user['Role']['id'])),
),
];
if ($isAdmin && $isTotp) {
$totpHtml .= sprintf(
'<a href="#" onClick="openGenericModal(\'%s/users/totp_delete/%s\')">%s</a>',
h($baseurl),
h($user['User']['id']),
__($isTotp && !$admin_view ? ' Delete' : 'Delete')
if (empty(Configure::read('Security.otp_disabled'))) {
$isTotp = isset($user['User']['totp']);
$boolean = sprintf(
'<span class="%s">%s</span>',
$isTotp ? 'label label-success label-padding' : 'label label-important label-padding',
$isTotp ? __('Yes') : __('No'));
$totpHtml = $boolean;
$totpHtml .= (!$isTotp && !$admin_view ? $this->Html->link(__('Generate'), array('action' => 'totp_new')) : '');
$totpHtml .= ($isTotp && !$admin_view ? $this->Html->link(__('View paper tokens'), array('action' => 'hotp', $user['User']['id'])): '');
if ($isAdmin && $isTotp) {
$totpHtml .= sprintf(
'<a href="#" onClick="openGenericModal(\'%s/users/totp_delete/%s\')">%s</a>',
h($baseurl),
h($user['User']['id']),
__($isTotp && !$admin_view ? ' Delete' : 'Delete')
);
}
$tableData[] = [
'key' => __('TOTP'),
'html' => $totpHtml
];
}
$tableData[] = array(
'key' => __('Email notifications'),
'html' => $notificationsHtml,
);
$tableData[] = array('key' => __('Contact alert enabled'), 'boolean' => $user['User']['contactalert']);
if (!$admin_view && !$user['Role']['perm_auth']) {
$tableData[] = array(
'key' => __('Auth key'),
'html' => sprintf('<a onclick="requestAPIAccess();" class="useCursorPointer">%s</a>', __('Request API access')),
);
}
$table_data = [
array('key' => __('ID'), 'value' => $user['User']['id']),
array(
'key' => __('Email'),
'html' => h($user['User']['email']) . ($admin_view ? sprintf(
' <a class="fas fa-envelope" style="color: #333" href="%s/admin/users/quickEmail/%s" title="%s"></a>',
$baseurl,
h($user['User']['id']),
__('Send email to user')
) : ''),
),
array(
'key' => __('Organisation'),
'html' => $this->OrgImg->getNameWithImg($user),
),
array(
'key' => __('Role'),
'html' => $this->Html->link($user['Role']['name'], array('controller' => 'roles', 'action' => 'view', $user['Role']['id'])),
),
// array('key' => __('TOTP'), 'boolean' => isset($user['User']['totp']) ? true : false),
array(
'key' => __('TOTP'),
'html' => $totpHtml
),
array(
'key' => __('Email notifications'),
'html' => $notificationsHtml,
),
array('key' => __('Contact alert enabled'), 'boolean' => $user['User']['contactalert'])
];
if (!$admin_view && !$user['Role']['perm_auth']) {
$table_data[] = array(
'key' => __('Auth key'),
'html' => sprintf('<a onclick="requestAPIAccess();" class="useCursorPointer">%s</a>', __('Request API access')),
);
}
if (empty(Configure::read('Security.advanced_authkeys')) && $user['Role']['perm_auth']) {
$authkey_data = sprintf(
'<span class="privacy-value quickSelect authkey" data-hidden-value="%s">****************************************</span>&nbsp;<i class="privacy-toggle fas fa-eye useCursorPointer" title="%s"></i>%s',
h($user['User']['authkey']),
__('Reveal hidden value'),
sprintf(
' (%s)',
$this->Form->postLink(__('reset'), array('action' => 'resetauthkey', $user['User']['id']))
)
);
$tableData[] = array(
'key' => __('Auth key'),
'html' => $authkey_data
);
}
if (empty(Configure::read('Security.advanced_authkeys')) && $user['Role']['perm_auth']) {
$authkey_data = sprintf(
'<span class="privacy-value quickSelect authkey" data-hidden-value="%s">****************************************</span>&nbsp;<i class="privacy-toggle fas fa-eye useCursorPointer" title="%s"></i>%s',
h($user['User']['authkey']),
__('Reveal hidden value'),
sprintf(
' (%s)',
$this->Form->postLink(__('reset'), array('action' => 'resetauthkey', $user['User']['id']))
)
);
$table_data[] = array(
'key' => __('Auth key'),
'html' => $authkey_data
);
}
if (Configure::read('Plugin.CustomAuth_enable') && !empty($user['User']['external_auth_key'])) {
$header = Configure::read('Plugin.CustomAuth_header') ?: 'AUTHORIZATION';
$table_data[] = array(
'key' => __('Customauth header'),
'html' => sprintf(
'%s: <span class="green bold">%s</span>',
h($header),
h($user['User']['external_auth_key'])
)
);
}
$table_data[] = array(
'key' => __('Invited By'),
'html' => empty($invitedBy['User']['email']) ? 'N/A' : sprintf('<a href="%s/admin/users/view/%s">%s</a>', $baseurl, h($invitedBy['User']['id']), h($invitedBy['User']['email'])),
if (Configure::read('Plugin.CustomAuth_enable') && !empty($user['User']['external_auth_key'])) {
$header = Configure::read('Plugin.CustomAuth_header') ?: 'AUTHORIZATION';
$tableData[] = array(
'key' => __('Customauth header'),
'html' => sprintf(
'%s: <span class="green bold">%s</span>',
h($header),
h($user['User']['external_auth_key'])
)
);
$org_admin_data = array();
if ($admin_view) {
foreach ($user['User']['orgAdmins'] as $orgAdminId => $orgAdminEmail) {
$org_admin_data[] = sprintf(
'<a href="%s/admin/users/view/%s">%s</a> <a class="fas fa-envelope" style="color: #333" href="%s/admin/users/quickEmail/%s" title="%s"></a>',
$baseurl,
h($orgAdminId),
h($orgAdminEmail),
$baseurl,
h($orgAdminId),
__('Send email to user')
);
}
$table_data[] = array('key' => __('Org admin'), 'html' => implode('<br>', $org_admin_data));
}
$table_data[] = array('key' => __('NIDS Start SID'), 'value' => $user['User']['nids_sid']);
if ($admin_view) {
$table_data[] = array('key' => __('Terms accepted'), 'boolean' => $user['User']['termsaccepted']);
$table_data[] = array('key' => __('Must change password'), 'boolean' => $user['User']['change_pw']);
}
if (!empty($user['User']['gpgkey'])) {
$table_data[] = array(
'key' => __('PGP key'),
'element' => 'genericElements/key',
'element_params' => array('key' => $user['User']['gpgkey']),
);
$table_data[] = array(
'key' => __('PGP key fingerprint'),
'value_class' => 'quickSelect',
'value' => $user['User']['fingerprint'] ? chunk_split($user['User']['fingerprint'], 4, ' ') : 'N/A'
);
$table_data[] = array(
'key' => __('PGP key status'),
'value_class' => (empty($user['User']['pgp_status']) || $user['User']['pgp_status'] !== 'OK') ? 'red': '',
'value' => !empty($user['User']['pgp_status']) ? $user['User']['pgp_status'] : 'N/A'
);
} else {
$table_data[] = array(
'key' => __('PGP key'),
'boolean' => false,
}
$tableData[] = array(
'key' => __('Invited By'),
'html' => empty($invitedBy['User']['email']) ? 'N/A' : sprintf('<a href="%s/admin/users/view/%s">%s</a>', $baseurl, h($invitedBy['User']['id']), h($invitedBy['User']['email'])),
);
$org_admin_data = array();
if ($admin_view) {
foreach ($user['User']['orgAdmins'] as $orgAdminId => $orgAdminEmail) {
$org_admin_data[] = sprintf(
'<a href="%s/admin/users/view/%s">%s</a> <a class="fas fa-envelope" style="color: #333" href="%s/admin/users/quickEmail/%s" title="%s"></a>',
$baseurl,
h($orgAdminId),
h($orgAdminEmail),
$baseurl,
h($orgAdminId),
__('Send email to user')
);
}
if (Configure::read('SMIME.enabled')) {
$table_data[] = array(
'key' => __('S/MIME Public certificate'),
'element' => 'genericElements/key',
'element_params' => array('key' => $user['User']['certif_public']),
);
}
$table_data[] = array(
'key' => __('Created'),
'html' => $user['User']['date_created'] ? $this->Time->time($user['User']['date_created']) : __('N/A')
$tableData[] = array('key' => __('Org admin'), 'html' => implode('<br>', $org_admin_data));
}
$tableData[] = array('key' => __('NIDS Start SID'), 'value' => $user['User']['nids_sid']);
if ($admin_view) {
$tableData[] = array('key' => __('Terms accepted'), 'boolean' => $user['User']['termsaccepted']);
$tableData[] = array('key' => __('Must change password'), 'boolean' => $user['User']['change_pw']);
}
if (!empty($user['User']['gpgkey'])) {
$tableData[] = array(
'key' => __('PGP key'),
'element' => 'genericElements/key',
'element_params' => array('key' => $user['User']['gpgkey']),
);
$table_data[] = array(
'key' => __('Last password change'),
'html' => $user['User']['last_pw_change'] ? $this->Time->time($user['User']['last_pw_change']) : __('N/A')
$tableData[] = array(
'key' => __('PGP key fingerprint'),
'value_class' => 'quickSelect',
'value' => $user['User']['fingerprint'] ? chunk_split($user['User']['fingerprint'], 4, ' ') : 'N/A'
);
if ($admin_view) {
$table_data[] = array(
'key' => __('News read at'),
'html' => $user['User']['newsread'] ? $this->Time->time($user['User']['newsread']) : __('N/A')
);
$table_data[] = array(
'key' => __('Disabled'),
'class' => empty($user['User']['disabled']) ? '' : 'background-red',
'boolean' => $user['User']['disabled']
);
}
echo $this->element('genericElements/assetLoader', array(
'css' => array('vis', 'distribution-graph'),
'js' => array('vis', 'jquery-ui.min', 'network-distribution-graph')
));
echo sprintf(
'<div class="users view"><div class="row-fluid"><div class="span8" style="margin:0px;">%s</div></div>%s%s%s<div style="margin-top:20px;">%s%s%s</div></div>',
sprintf(
'<h2>%s</h2>%s',
__('User %s', h($user['User']['email'])),
$this->element('genericElements/viewMetaTable', array('table_data' => $table_data))
),
sprintf(
'<br><a href="%s" class="btn btn-inverse" download>%s</a>',
sprintf(
'%s/users/view/%s.json',
$baseurl,
h($user['User']['id'])
),
__('Download user profile for data portability')
),
sprintf(
'&nbsp;<a href="%s" class="btn btn-inverse">%s</a>',
sprintf(
'%s/logs/index',
$baseurl
),
__('Review user logs')
),
sprintf(
'&nbsp;<a href="%s" class="btn btn-inverse">%s</a>',
sprintf(
'%s/users/view_login_history/%s',
$baseurl,
h($user['User']['id'])
),
__('Review user logins')
),
$me['Role']['perm_auth'] ? $this->element('/genericElements/accordion', array('title' => __('Auth keys'), 'url' => '/auth_keys/index/' . h($user['User']['id']))) : '',
$me['Role']['perm_site_admin'] ?
$this->element(
'/genericElements/accordion',
[
'title' => __('Benchmarks'),
'url' => '/benchmarks/index/scope:user/average:1/aggregate:1/key:' . h($user['User']['id'])
]
) :
'',
$this->element('/genericElements/accordion', array('title' => 'Events', 'url' => '/events/index/searchemail:' . urlencode(h($user['User']['email']))))
$tableData[] = array(
'key' => __('PGP key status'),
'value_class' => (empty($user['User']['pgp_status']) || $user['User']['pgp_status'] !== 'OK') ? 'red': '',
'value' => !empty($user['User']['pgp_status']) ? $user['User']['pgp_status'] : 'N/A'
);
$current_menu = [
'admin_view' => ['menuList' => 'admin', 'menuItem' => 'viewUser'],
'view' => ['menuList' => 'globalActions', 'menuItem' => 'view']
];
echo $this->element('/genericElements/SideMenu/side_menu', $current_menu[$admin_view ? 'admin_view' : 'view']);
} else {
$tableData[] = array(
'key' => __('PGP key'),
'boolean' => false,
);
}
if (Configure::read('SMIME.enabled')) {
$tableData[] = array(
'key' => __('S/MIME Public certificate'),
'element' => 'genericElements/key',
'element_params' => array('key' => $user['User']['certif_public']),
);
}
$tableData[] = array(
'key' => __('Created'),
'html' => $user['User']['date_created'] ? $this->Time->time($user['User']['date_created']) : __('N/A')
);
$tableData[] = array(
'key' => __('Last password change'),
'html' => $user['User']['last_pw_change'] ? $this->Time->time($user['User']['last_pw_change']) : __('N/A')
);
if ($admin_view) {
$tableData[] = array(
'key' => __('News read at'),
'html' => $user['User']['newsread'] ? $this->Time->time($user['User']['newsread']) : __('N/A')
);
$tableData[] = array(
'key' => __('Disabled'),
'class' => empty($user['User']['disabled']) ? '' : 'background-red',
'boolean' => $user['User']['disabled']
);
}
echo $this->element('genericElements/assetLoader', array(
'css' => array('vis', 'distribution-graph'),
'js' => array('vis', 'jquery-ui.min', 'network-distribution-graph')
));
echo sprintf(
'<div class="users view"><div class="row-fluid"><div class="span8" style="margin:0px;">%s</div></div>%s%s%s<div style="margin-top:20px;">%s%s%s</div></div>',
sprintf(
'<h2>%s</h2>%s',
__('User %s', h($user['User']['email'])),
$this->element('genericElements/viewMetaTable', array('table_data' => $tableData))
),
sprintf(
'<br><a href="%s" class="btn btn-inverse" download>%s</a>',
sprintf(
'%s/users/view/%s.json',
$baseurl,
h($user['User']['id'])
),
__('Download user profile for data portability')
),
sprintf(
'&nbsp;<a href="%s" class="btn btn-inverse">%s</a>',
sprintf(
'%s/logs/index',
$baseurl
),
__('Review user logs')
),
sprintf(
'&nbsp;<a href="%s" class="btn btn-inverse">%s</a>',
sprintf(
'%s/users/view_login_history/%s',
$baseurl,
h($user['User']['id'])
),
__('Review user logins')
),
$me['Role']['perm_auth'] ? $this->element('/genericElements/accordion', array('title' => __('Auth keys'), 'url' => '/auth_keys/index/' . h($user['User']['id']))) : '',
$me['Role']['perm_site_admin'] ?
$this->element(
'/genericElements/accordion',
[
'title' => __('Benchmarks'),
'url' => '/benchmarks/index/scope:user/average:1/aggregate:1/key:' . h($user['User']['id'])
]
) :
'',
$this->element('/genericElements/accordion', array('title' => 'Events', 'url' => '/events/index/searchemail:' . urlencode(h($user['User']['email']))))
);
$current_menu = [
'admin_view' => ['menuList' => 'admin', 'menuItem' => 'viewUser'],
'view' => ['menuList' => 'globalActions', 'menuItem' => 'view']
];
echo $this->element('/genericElements/SideMenu/side_menu', $current_menu[$admin_view ? 'admin_view' : 'view']);

View File

@ -19,6 +19,7 @@ from enum import Enum
try:
from pymisp import PyMISP, MISPOrganisation, MISPUser, MISPRole, MISPSharingGroup, MISPEvent, MISPLog, MISPSighting, Distribution
from pymisp.exceptions import PyMISPError, NoKey, MISPServerError
from pymisp.api import get_uuid_or_id_from_abstract_misp
except ImportError:
if sys.version_info < (3, 6):
print('This test suite requires Python 3.6+, breaking.')
@ -126,6 +127,12 @@ def random() -> str:
return str(uuid.uuid4()).split("-")[0]
def publish_immediately(pymisp: PyMISP, event: Union[MISPEvent, int, str, uuid.UUID], with_email: bool = False):
event_id = get_uuid_or_id_from_abstract_misp(event)
action = "alert" if with_email else "publish"
return send(pymisp, 'POST', f'events/{action}/{event_id}/disable_background_processing:1')
class TestSecurity(unittest.TestCase):
@classmethod
def setUpClass(cls):
@ -782,6 +789,30 @@ class TestSecurity(unittest.TestCase):
# Password should be still the same
self.assertIsInstance(login(url, self.test_usr.email, self.test_usr_password), requests.Session)
def test_forget_password_not_enabled(self):
logged_in = PyMISP(url, self.test_usr.authkey)
logged_in.global_pythonify = True
with self.assertRaises(Exception):
send(logged_in, "GET", f"/users/forget")
with self.assertRaises(Exception):
send(logged_in, "GET", f"/users/password_reset/abcd")
def test_otp_disabled(self):
with self.__setting("Security.otp_disabled", True):
logged_in = PyMISP(url, self.test_usr.authkey)
logged_in.global_pythonify = True
with self.assertRaises(Exception):
send(logged_in, "GET", f"/users/email_otp")
with self.assertRaises(Exception):
send(logged_in, "GET", f"/users/totp_new")
with self.assertRaises(Exception):
send(logged_in, "GET", f"/users/totp_delete/1")
def test_add_user_by_org_admin(self):
user = MISPUser()
user.email = 'testusr@user' + random() + '.local' # make name always unique
@ -1253,8 +1284,7 @@ class TestSecurity(unittest.TestCase):
self.assertEqual(len(attributes["Attribute"]), 0, attributes)
# Publish
self.assertSuccessfulResponse(self.admin_misp_connector.publish(created_event))
time.sleep(6);
self.assertSuccessfulResponse(publish_immediately(self.admin_misp_connector, created_event))
# Event is published, so normal user should see that event
self.assertTrue(logged_in.event_exists(created_event.uuid))