new: [API] Read only authkeys

pull/7482/head
Jakub Onderka 2021-06-08 15:19:49 +02:00
parent 51821b5de2
commit 017249451b
10 changed files with 186 additions and 36 deletions

View File

@ -1525,17 +1525,7 @@ class AppController extends Controller
if (isset($sessionUser['authkey_id'])) {
// Reload authkey
$this->loadModel('AuthKey');
$authKey = $this->AuthKey->find('first', [
'conditions' => ['id' => $sessionUser['authkey_id'], 'user_id' => $user['id']],
'fields' => ['id', 'expiration', 'allowed_ips'],
'recursive' => -1,
]);
if (empty($authKey)) {
throw new RuntimeException("Auth key with ID {$sessionUser['authkey_id']} not exists.");
}
$user['authkey_id'] = $authKey['AuthKey']['id'];
$user['authkey_expiration'] = $authKey['AuthKey']['expiration'];
$user['allowed_ips'] = $authKey['AuthKey']['allowed_ips'];
$user = $this->AuthKey->updateUserData($user, $sessionUser['authkey_id']);
}
if (isset($sessionUser['logged_by_authkey'])) {
$user['logged_by_authkey'] = $sessionUser['logged_by_authkey'];

View File

@ -82,7 +82,7 @@ class AuthKeysController extends AppController
$authKey['AuthKey']['expiration'] = date('Y-m-d H:i:s', $authKey['AuthKey']['expiration']);
return $authKey;
},
'fields' => ['comment', 'allowed_ips', 'expiration'],
'fields' => ['comment', 'allowed_ips', 'expiration', 'read_only'],
'contain' => ['User.id', 'User.org_id']
]);
if ($this->IndexFilter->isRest()) {
@ -100,6 +100,7 @@ class AuthKeysController extends AppController
]);
$this->set('edit', true);
$this->set('validity', Configure::read('Security.advanced_authkeys_validity'));
$this->set('title_for_layout', __('Edit auth key'));
$this->render('add');
}
@ -134,6 +135,7 @@ class AuthKeysController extends AppController
])
];
$this->set(compact('dropdownData'));
$this->set('title_for_layout', __('Add auth key'));
$this->set('menuData', [
'menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions',
'menuItem' => 'authKeyAdd',
@ -162,7 +164,7 @@ class AuthKeysController extends AppController
$this->set('uniqueIps', $uniqueIps);
}
$this->set('title_for_layout', __('Auth Key'));
$this->set('title_for_layout', __('Auth key'));
$this->set('menuData', [
'menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions',
'menuItem' => 'authKeyView',

View File

@ -71,9 +71,9 @@ class ACLComponent extends Component
'viewPicture' => array('*'),
),
'authKeys' => [
'add' => ['perm_auth'],
'delete' => ['perm_auth'],
'edit' => ['perm_auth'],
'add' => ['AND' => ['perm_auth', 'not_read_only_authkey']],
'delete' => ['AND' => ['perm_auth', 'not_read_only_authkey']],
'edit' => ['AND' => ['perm_auth', 'not_read_only_authkey']],
'index' => ['perm_auth'],
'view' => ['perm_auth']
],
@ -468,9 +468,9 @@ class ACLComponent extends Component
'display' => array('*'),
),
'posts' => array(
'add' => array('*'),
'delete' => array('*'),
'edit' => array('*'),
'add' => array('not_read_only_authkey'),
'delete' => array('not_read_only_authkey'),
'edit' => array('not_read_only_authkey'),
'pushMessageToZMQ' => array('perm_site_admin')
),
'regexp' => array(
@ -483,7 +483,7 @@ class ACLComponent extends Component
'index' => array('*'),
),
'restClientHistory' => array(
'delete' => array('*'),
'delete' => array('not_read_only_authkey'),
'index' => array('*')
),
'roles' => array(
@ -693,7 +693,7 @@ class ACLComponent extends Component
'admin_quickEmail' => array('perm_admin'),
'admin_view' => array('perm_admin'),
'attributehistogram' => array('*'),
'change_pw' => ['AND' => ['self_management_enabled', 'password_change_enabled']],
'change_pw' => ['AND' => ['self_management_enabled', 'password_change_enabled', 'not_read_only_authkey']],
'checkAndCorrectPgps' => array(),
'checkIfLoggedIn' => array('*'),
'dashboard' => array('*'),
@ -711,7 +711,7 @@ class ACLComponent extends Component
'register' => array('*'),
'registrations' => array('perm_site_admin'),
'resetAllSyncAuthKeys' => array(),
'resetauthkey' => ['AND' => ['self_management_enabled', 'perm_auth']],
'resetauthkey' => ['AND' => ['self_management_enabled', 'perm_auth', 'not_read_only_authkey']],
'request_API' => array('*'),
'routeafterlogin' => array('*'),
'statistics' => array('*'),
@ -727,10 +727,10 @@ class ACLComponent extends Component
'userSettings' => array(
'index' => array('*'),
'view' => array('*'),
'setSetting' => array('*'),
'setSetting' => array('not_read_only_authkey'),
'getSetting' => array('*'),
'delete' => array('*'),
'setHomePage' => array('*'),
'delete' => array('not_read_only_authkey'),
'setHomePage' => array('not_read_only_authkey'),
'eventIndexColumnToggle' => ['*'],
),
'warninglists' => array(
@ -792,6 +792,10 @@ class ACLComponent extends Component
$this->dynamicChecks['delegation_enabled'] = function (array $user) {
return (bool)Configure::read('MISP.delegation');
};
// Returns true if current user is not using advanced auth key or if authkey is not read only
$this->dynamicChecks['not_read_only_authkey'] = function (array $user) {
return !isset($user['authkey_read_only']) || !$user['authkey_read_only'];
};
}
private function __checkLoggedActions($user, $controller, $action)

View File

@ -90,7 +90,7 @@ class AppModel extends Model
51 => false, 52 => false, 53 => false, 54 => false, 55 => false, 56 => false,
57 => false, 58 => false, 59 => false, 60 => false, 61 => false, 62 => false,
63 => true, 64 => false, 65 => false, 66 => false, 67 => false, 68 => false,
69 => false, 70 => false, 71 => true,
69 => false, 70 => false, 71 => true, 72 => true,
);
public $advanced_updates_description = array(
@ -1608,6 +1608,9 @@ class AppModel extends Model
$sqlArray[] = "ALTER TABLE `warninglist_entries` ADD `comment` text DEFAULT NULL;";
$sqlArray[] = "ALTER TABLE `warninglists` ADD `default` tinyint(1) NOT NULL DEFAULT 1, ADD `category` varchar(20) NOT NULL DEFAULT 'false_positive', DROP COLUMN `warninglist_entry_count`";
break;
case 72:
$sqlArray[] = "ALTER TABLE `auth_keys` ADD `read_only` tinyint(1) NOT NULL DEFAULT 0 AFTER `expiration`;";
break;
case 'fixNonEmptySharingGroupID':
$sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
$sqlArray[] = 'UPDATE `attributes` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';

View File

@ -108,6 +108,24 @@ class AuthKey extends AppModel
return true;
}
/**
* @param array $user
* @param int $authKeyId
* @return array
*/
public function updateUserData(array $user, $authKeyId)
{
$authKey = $this->find('first', [
'conditions' => ['id' => $authKeyId, 'user_id' => $user['id']],
'fields' => ['id', 'expiration', 'allowed_ips', 'read_only'],
'recursive' => -1,
]);
if (empty($authKey)) {
throw new RuntimeException("Auth key with ID $authKeyId doesn't exist anymore.");
}
return $this->setUserData($user, $authKey);
}
/**
* @param string $authkey
* @return array|false
@ -118,7 +136,7 @@ class AuthKey extends AppModel
$end = substr($authkey, -4);
$possibleAuthkeys = $this->find('all', [
'recursive' => -1,
'fields' => ['id', 'authkey', 'user_id', 'expiration', 'allowed_ips'],
'fields' => ['id', 'authkey', 'user_id', 'expiration', 'allowed_ips', 'read_only'],
'conditions' => [
'OR' => [
'expiration >' => time(),
@ -133,9 +151,7 @@ class AuthKey extends AppModel
if ($passwordHasher->check($authkey, $possibleAuthkey['AuthKey']['authkey'])) {
$user = $this->User->getAuthUser($possibleAuthkey['AuthKey']['user_id']);
if ($user) {
$user['authkey_id'] = $possibleAuthkey['AuthKey']['id'];
$user['authkey_expiration'] = $possibleAuthkey['AuthKey']['expiration'];
$user['allowed_ips'] = $possibleAuthkey['AuthKey']['allowed_ips'];
$user = $this->setUserData($user, $possibleAuthkey);
}
return $user;
}
@ -143,6 +159,30 @@ class AuthKey extends AppModel
return false;
}
/**
* @param array $user
* @param array $authkey
* @return array
*/
private function setUserData(array $user, array $authkey)
{
$user['authkey_id'] = $authkey['AuthKey']['id'];
$user['authkey_expiration'] = $authkey['AuthKey']['expiration'];
$user['allowed_ips'] = $authkey['AuthKey']['allowed_ips'];
$user['authkey_read_only'] = (bool)$authkey['AuthKey']['read_only'];
if ($authkey['AuthKey']['read_only']) {
// Disable all permissions, keep just `perm_auth` unchanged
foreach ($user['Role'] as $key => &$value) {
if (substr($key, 0, 5) === 'perm_' && $key !== 'perm_auth') {
$value = 0;
}
}
}
return $user;
}
/**
* @param int $userId
* @param int|null $keyId
@ -264,7 +304,7 @@ class AuthKey extends AppModel
/**
* When key is modified, update `date_modified` for user that was assigned to that key, so session data
* will be realoaded.
* will be reloaded.
* @see AppController::_refreshAuth
*/
public function afterSave($created, $options = array())

View File

@ -28,6 +28,11 @@ echo $this->element('genericElements/Form/genericForm', [
'class' => 'datepicker span6',
'placeholder' => "YYYY-MM-DD",
'type' => 'text'
],
[
'field' => 'read_only',
'label' => __('Read only (it will be not possible to do any change operation with this token)'),
'type' => 'checkbox',
]
],
'submit' => [

View File

@ -1,8 +1,8 @@
<?php
echo sprintf('<div%s>', empty($ajax) ? ' class="index"' : '');
if (!$advancedEnabled) {
echo '<div class="alert">' . __('Advanced auth keys are not enabled.') . '</div>';
}
echo sprintf('<div%s>', empty($ajax) ? ' class="index"' : '');
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
@ -14,6 +14,7 @@
'children' => [
'data' => [
'type' => 'simple',
'fa-icon' => 'plus',
'text' => __('Add authentication key'),
'class' => 'btn btn-primary',
'onClick' => 'openGenericModal',

View File

@ -63,6 +63,11 @@ echo $this->element('genericElements/SingleViews/single_view', [
'path' => 'AuthKey.expiration',
'type' => 'expiration'
],
[
'key' => __('Read only'),
'path' => 'AuthKey.read_only',
'type' => 'boolean'
],
[
'key' => __('Key usage'),
'type' => 'sparkline',

View File

@ -618,6 +618,17 @@
"column_default": null,
"extra": ""
},
{
"column_name": "read_only",
"is_nullable": "NO",
"data_type": "tinyint",
"character_maximum_length": null,
"numeric_precision": "3",
"collation_name": null,
"column_type": "tinyint(1)",
"column_default": "0",
"extra": ""
},
{
"column_name": "user_id",
"is_nullable": "NO",
@ -8191,5 +8202,5 @@
"id": true
}
},
"db_version": "71"
"db_version": "72"
}

View File

@ -5,7 +5,7 @@ import time
import json
import datetime
import unittest
from typing import Union, List
from typing import Union, List, Optional
import urllib3 # type: ignore
import logging
import uuid
@ -588,6 +588,87 @@ class TestSecurity(unittest.TestCase):
self.__delete_advanced_authkey(auth_key["id"])
def test_advanced_authkeys_read_only_false(self):
with self.__setting("Security.advanced_authkeys", True):
auth_key = self.__create_advanced_authkey(self.test_usr.id, {
"read_only": 0,
})
self.assertFalse(auth_key["read_only"])
# Try to login
logged_in = self.__login_by_advanced_authkey(auth_key)
# Create new event should not be possible with read only key
event = logged_in.add_event(self.__generate_event())
check_response(event)
self.__delete_advanced_authkey(auth_key["id"])
def test_advanced_authkeys_read_only(self):
with self.__setting("Security.advanced_authkeys", True):
auth_key = self.__create_advanced_authkey(self.test_usr.id, {
"read_only": 1,
})
self.assertTrue(auth_key["read_only"])
# Try to login
logged_in = self.__login_by_advanced_authkey(auth_key)
# Create new event should not be possible with read only key
event = logged_in.add_event(self.__generate_event())
with self.assertRaises(Exception):
check_response(event)
self.__delete_advanced_authkey(auth_key["id"])
def test_advanced_authkeys_read_only_edit_self(self):
with self.__setting("Security.advanced_authkeys", True):
auth_key = self.__create_advanced_authkey(self.test_usr.id, {
"read_only": 1,
})
self.assertTrue(auth_key["read_only"])
# Try to login
logged_in = self.__login_by_advanced_authkey(auth_key)
# Edit current auth key and set it to not read_only should be not possible
with self.assertRaises(Exception):
send(logged_in, "POST", f'authKeys/edit/{auth_key["id"]}', {"read_only": 0})
self.__delete_advanced_authkey(auth_key["id"])
def test_advanced_authkeys_read_only_create_new_authkey(self):
with self.__setting("Security.advanced_authkeys", True):
auth_key = self.__create_advanced_authkey(self.test_usr.id, {
"read_only": 1,
})
self.assertTrue(auth_key["read_only"])
# Try to login
logged_in = self.__login_by_advanced_authkey(auth_key)
# Create new auth key should be not possible
with self.assertRaises(Exception):
send(logged_in, "POST", f'authKeys/add/{logged_in._current_user.id}')
self.__delete_advanced_authkey(auth_key["id"])
def test_advanced_authkeys_read_only_reset_authkey(self):
with self.__setting("Security.advanced_authkeys", True):
auth_key = self.__create_advanced_authkey(self.test_usr.id, {
"read_only": 1,
})
self.assertTrue(auth_key["read_only"])
# Try to login
logged_in = self.__login_by_advanced_authkey(auth_key)
# Create new auth key should be not possible
with self.assertRaises(Exception):
send(logged_in, "POST", "users/resetauthkey/me")
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)
@ -1424,8 +1505,16 @@ class TestSecurity(unittest.TestCase):
self.assertEqual(int(role_id), int(user.role_id))
return user
def __create_advanced_authkey(self, user_id: int, data=None):
return send(self.admin_misp_connector, "POST", f'authKeys/add/{user_id}', data=data)["AuthKey"]
def __create_advanced_authkey(self, user_id: int, data: Optional[dict] = None) -> dict:
auth_key = send(self.admin_misp_connector, "POST", f'authKeys/add/{user_id}', data=data)["AuthKey"]
# it is not possible to call `assertEqual`, because we use this method in `setUpClass` method
assert user_id == auth_key["user_id"], "Key was created for different user"
return auth_key
def __login_by_advanced_authkey(self, auth_key: dict) -> PyMISP:
logged_in = PyMISP(url, auth_key["authkey_raw"])
self.assertEqual(logged_in._current_user.id, auth_key["user_id"], "Logged in by different user")
return logged_in
def __delete_advanced_authkey(self, key_id: int):
return send(self.admin_misp_connector, "POST", f'authKeys/delete/{key_id}')