new: [auth] Allow to enforce auth plugin authentication

pull/6656/head
Jakub Onderka 2020-11-26 20:27:35 +01:00
parent 56159d4791
commit 2c7d6e4466
7 changed files with 94 additions and 52 deletions

View File

@ -73,6 +73,9 @@ class AppController extends Controller
protected $_legacyParams = array();
/** @var User */
public $User;
public function __construct($id = false, $table = null, $ds = null)
{
parent::__construct($id, $table, $ds);
@ -154,8 +157,8 @@ class AppController extends Controller
$this->set('ajax', $this->request->is('ajax'));
$this->set('queryVersion', $this->__queryVersion);
$this->loadModel('User');
$auth_user_fields = $this->User->describeAuthFields();
$this->User = ClassRegistry::init('User');
$language = Configure::read('MISP.language');
if (!empty($language) && $language !== 'eng') {
Configure::write('Config.language', $language);
@ -174,18 +177,19 @@ class AppController extends Controller
$this->Server->serverSettingsSaveValue('MISP.uuid', CakeText::uuid());
}
// check if Apache provides kerberos authentication data
$authUserFields = $this->User->describeAuthFields();
$envvar = Configure::read('ApacheSecureAuth.apacheEnv');
if (isset($_SERVER[$envvar])) {
if ($envvar && isset($_SERVER[$envvar])) {
$this->Auth->className = 'ApacheSecureAuth';
$this->Auth->authenticate = array(
'Apache' => array(
// envvar = field returned by Apache if user is authenticated
'fields' => array('username' => 'email', 'envvar' => $envvar),
'userFields' => $auth_user_fields
'userFields' => $authUserFields,
)
);
} else {
$this->Auth->authenticate['Form']['userFields'] = $auth_user_fields;
$this->Auth->authenticate[AuthComponent::ALL]['userFields'] = $authUserFields;
}
if (!empty($this->params['named']['disable_background_processing'])) {
Configure::write('MISP.background_jobs', 0);
@ -1152,13 +1156,24 @@ class AppController extends Controller
$this->redirect($targetRoute);
}
protected function _loadAuthenticationPlugins() {
/**
* @throws Exception
*/
protected function _loadAuthenticationPlugins()
{
// load authentication plugins from Configure::read('Security.auth')
$auth = Configure::read('Security.auth');
if (!$auth) return;
if (!$auth) {
return;
}
if (!is_array($auth)) {
throw new Exception("`Security.auth` config value must be array.");
}
$this->Auth->authenticate = array_merge($auth, $this->Auth->authenticate);
// Disable Form authentication
if (Configure::read('Security.auth_enforced')) {
unset($this->Auth->authenticate['Form']);
}
if ($this->Auth->startup($this)) {
$user = $this->Auth->user();
if ($user) {

View File

@ -1139,25 +1139,9 @@ class UsersController extends AppController
if (!empty($this->request->data['User']['email'])) {
if ($this->Bruteforce->isBlocklisted($_SERVER['REMOTE_ADDR'], $this->request->data['User']['email'])) {
$expire = Configure::check('SecureAuth.expire') ? Configure::read('SecureAuth.expire') : 300;
throw new ForbiddenException('You have reached the maximum number of login attempts. Please wait ' . Configure::read('SecureAuth.expire') . ' seconds and try again.');
throw new ForbiddenException('You have reached the maximum number of login attempts. Please wait ' . $expire . ' seconds and try again.');
}
}
// Check the length of the user's authkey
$userPass = $this->User->find('first', array(
'conditions' => array('User.email' => $this->request->data['User']['email']),
'fields' => array('User.password'),
'recursive' => -1
));
if (!empty($userPass) && strlen($userPass['User']['password']) == 40) {
$this->AdminSetting = ClassRegistry::init('AdminSetting');
$db_version = $this->AdminSetting->find('all', array('conditions' => array('setting' => 'db_version')));
$versionRequirementMet = $this->User->checkVersionRequirements($db_version[0]['AdminSetting']['value'], '2.4.77');
if ($versionRequirementMet) {
$passwordToSave = $this->request->data['User']['password'];
}
unset($this->Auth->authenticate['Form']['passwordHasher']);
$this->Auth->constructAuthenticate();
}
}
if ($this->request->is('post') && Configure::read('Security.email_otp_enabled')) {
$user = $this->Auth->identify($this->request, $this->response);
@ -1166,6 +1150,9 @@ class UsersController extends AppController
return $this->redirect('email_otp');
}
}
$formLoginEnabled = isset($this->Auth->authenticate['Form']);
$this->set('formLoginEnabled', $formLoginEnabled);
if ($this->Auth->login()) {
$this->_postlogin();
} else {

View File

@ -2542,19 +2542,6 @@ class AppModel extends Model
$this->elasticSearchClient = $client;
}
public function checkVersionRequirements($versionString, $minVersion)
{
$version = explode('.', $versionString);
$minVersion = explode('.', $minVersion);
if (count($version) > $minVersion) {
return true;
}
if (count($version) == 1) {
return $minVersion <= $version;
}
return ($version[0] >= $minVersion[0] && $version[1] >= $minVersion[1] && $version[2] >= $minVersion[2]);
}
// generate a generic subquery - options needs to include conditions
public function subQueryGenerator($model, $options, $lookupKey, $negation = false)
{

View File

@ -1343,6 +1343,14 @@ class Server extends AppModel
'test' => 'testBool',
'type' => 'boolean',
),
'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.'),
'value' => false,
'errorMessage' => '',
'test' => 'testBool',
'type' => 'boolean',
],
'rest_client_enable_arbitrary_urls' => array(
'level' => 0,
'description' => __('Enable this setting if you wish for users to be able to query any arbitrary URL via the rest client. Keep in mind that queries are executed by the MISP server, so internal IPs in your MISP\'s network may be reachable.'),

View File

@ -862,18 +862,19 @@ class User extends AppModel
return $gpgTool->fetchGpgKey($fingerprint);
}
/**
* Returns fields that should be fetched from database.
* @return array
*/
public function describeAuthFields()
{
$fields = array();
$fields = array_merge($fields, array_keys($this->getColumnTypes()));
foreach ($fields as $k => $field) {
if (in_array($field, array('gpgkey', 'certif_public'))) {
unset($fields[$k]);
}
}
$fields = array_values($fields);
$relatedModels = array_keys($this->belongsTo);
foreach ($relatedModels as $relatedModel) {
$fields = $this->schema();
// Do not include keys, because they are big and usually not necessary
unset($fields['gpgkey']);
unset($fields['certif_public']);
$fields = array_keys($fields);
foreach ($this->belongsTo as $relatedModel => $foo) {
$fields[] = $relatedModel . '.*';
}
return $fields;

View File

@ -32,6 +32,7 @@
</div>
<?php
endif;
if ($formLoginEnabled):
echo $this->Form->create('User');
?>
<legend><?php echo __('Login');?></legend>
@ -52,6 +53,7 @@
<?= $this->Form->button(__('Login'), array('class' => 'btn btn-primary')); ?>
<?php
echo $this->Form->end();
endif;
if (Configure::read('ApacheShibbAuth') == true) {
echo '<div class="clear"></div><a class="btn btn-info" href="/Shibboleth.sso/Login">Login with SAML</a>';
}
@ -65,7 +67,7 @@
</div>
<script>
$(document).ready(function() {
$(function() {
$('#UserLoginForm').submit(function(event) {
event.preventDefault()
submitLoginForm()

View File

@ -778,7 +778,45 @@ class TestSecurity(unittest.TestCase):
self.admin_misp_connector.delete_user(user)
self.admin_misp_connector.delete_organisation(json_response["User"]["org_id"])
def __shibb_login(self, headers: dict):
def test_shibb_form_login(self):
with MISPComplexSetting(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))
def test_shibb_api_login(self):
with MISPComplexSetting(self.__default_shibb_config()):
PyMISP(url, self.test_usr.authkey)
def test_shibb_enforced_existing_user(self):
config = self.__default_shibb_config()
config["Security"]["auth_enforced"] = True
with MISPComplexSetting(config):
r = self.__shibb_login({
"Email-Tag": self.test_usr.email,
"Federation-Tag": self.test_org.name,
"Group-Tag": "user",
})
r.raise_for_status()
json_response = r.json()
self.assertEqual(self.test_usr.email, json_response["User"]["email"])
self.assertEqual(3, int(json_response["User"]["role_id"]))
self.assertEqual(self.test_org.name, json_response["Organisation"]["name"])
def test_shibb_enforced_form_login(self):
config = self.__default_shibb_config()
config["Security"]["auth_enforced"] = True
with MISPComplexSetting(config):
# Form login should not work when shibb is enforced, because form doesn't exists
with self.assertRaises(IndexError):
login(url, self.test_usr.email, self.test_usr_password)
def test_shibb_enforced_api_login(self):
config = self.__default_shibb_config()
config["Security"]["auth_enforced"] = True
with MISPComplexSetting(config):
PyMISP(url, self.test_usr.authkey)
def __shibb_login(self, headers: dict) -> requests.Response:
session = requests.Session()
session.headers.update(headers)
@ -786,7 +824,11 @@ class TestSecurity(unittest.TestCase):
if 500 <= r.status_code < 600:
raise Exception(r)
return session.get(url + "/users/view/me.json")
r = session.get(url + "/users/view/me.json")
if 500 <= r.status_code < 600:
raise Exception(r)
return r
def __create_user(self, org_id: int = None, role_id: Union[int, ROLE] = None) -> MISPUser:
if isinstance(role_id, ROLE):