From a8c57a83162583cd49d8f2fd88e8ab056b1e0735 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 8 Oct 2019 11:43:56 +0200 Subject: [PATCH] new: [API] Added rate limiting option to the API - / role setting - can be enabled/disabled and if enabled a limit can be set - limit counter / 15 minutes starting from the first query - x-headers inform the user about their limit/remaining queries/reset in seconds --- app/Controller/AppController.php | 36 +++++++++++++++++-- .../Component/RestResponseComponent.php | 18 +++++++--- app/Model/AppModel.php | 8 +++-- app/View/Roles/admin_add.ctp | 19 +++++++++- app/View/Roles/admin_edit.ctp | 20 ++++++++++- app/View/Roles/admin_index.ctp | 12 ++++++- app/webroot/js/misp.js | 9 +++++ 7 files changed, 110 insertions(+), 12 deletions(-) diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php index c49b4570c..3793362e1 100755 --- a/app/Controller/AppController.php +++ b/app/Controller/AppController.php @@ -46,7 +46,7 @@ class AppController extends Controller public $helpers = array('Utility', 'OrgImg', 'FontAwesome', 'UserName'); - private $__queryVersion = '88'; + private $__queryVersion = '89'; public $pyMispVersion = '2.4.114'; public $phpmin = '7.0'; public $phprec = '7.2'; @@ -89,7 +89,8 @@ class AppController extends Controller 'ACL', 'RestResponse', 'Flash', - 'Toolbox' + 'Toolbox', + 'RateLimit' //,'DebugKit.Toolbar' ); @@ -358,7 +359,7 @@ class AppController extends Controller if (!$this->User->exists()) { $message = __('Something went wrong. Your user account that you are authenticated with doesn\'t exist anymore.'); if ($this->_isRest) { - $this->RestResponse->throwException( + echo $this->RestResponse->throwException( 401, $message ); @@ -470,6 +471,35 @@ class AppController extends Controller } $this->set('notifications', $notifications); $this->ACL->checkAccess($this->Auth->user(), Inflector::variable($this->request->params['controller']), $this->action); + $this->__rateLimitCheck(); + } + + private function __rateLimitCheck() + { + $info = array(); + $rateLimitCheck = $this->RateLimit->check( + $this->Auth->user(), + $this->request->params['controller'], + $this->action, + $this->{$this->modelClass}, + $info, + $this->response->type() + ); + if (!empty($info)) { + $this->RestResponse->setHeader('X-Rate-Limit-Limit', $info['limit']); + $this->RestResponse->setHeader('X-Rate-Limit-Remaining', $info['remaining']); + $this->RestResponse->setHeader('X-Rate-Limit-Reset', $info['reset']); + } + if ($rateLimitCheck !== true) { + $this->response->header('X-Rate-Limit-Limit', $info['limit']); + $this->response->header('X-Rate-Limit-Remaining', $info['remaining']); + $this->response->header('X-Rate-Limit-Reset', $info['reset']); + $this->response->body($rateLimitCheck); + $this->response->statusCode(429); + $this->response->send(); + $this->_stop(); + } + return true; } public function afterFilter() diff --git a/app/Controller/Component/RestResponseComponent.php b/app/Controller/Component/RestResponseComponent.php index 25a1652eb..8fe456fec 100644 --- a/app/Controller/Component/RestResponseComponent.php +++ b/app/Controller/Component/RestResponseComponent.php @@ -4,6 +4,8 @@ class RestResponseComponent extends Component { public $components = array('ACL'); + public $headers = array(); + private $__convertActionToMessage = array( 'SharingGroup' => array( 'addOrg' => 'add Organisation to', @@ -452,13 +454,16 @@ class RestResponseComponent extends Component $headers["Access-Control-Allow-Origin"] = explode(',', Configure::read('Security.cors_origins')); $headers["Access-Control-Expose-Headers"] = ["X-Result-Count"]; } - + if (!empty($this->headers)) { + foreach ($this->headers as $key => $value) { + $cakeResponse->header($key, $value); + } + } if (!empty($headers)) { foreach ($headers as $key => $value) { $cakeResponse->header($key, $value); } } - if ($download) { $cakeResponse->download($download); } @@ -500,14 +505,19 @@ class RestResponseComponent extends Component return $cakeResponse; } - public function throwException($code, $message, $url = '', $format = false, $raw = false) + public function throwException($code, $message, $url = '', $format = false, $raw = false, $headers = array()) { $message = array( 'name' => $message, 'message' => $message, 'url' => $url ); - return $this->__sendResponse($message, $code, $format, $raw); + return $this->__sendResponse($message, $code, $format, $raw, false, $headers); + } + + public function setHeader($header, $value) + { + $this->headers[$header] = $value; } public function describe($controller, $action, $id = false, $format = false) diff --git a/app/Model/AppModel.php b/app/Model/AppModel.php index f7917e7e7..a0fd26313 100644 --- a/app/Model/AppModel.php +++ b/app/Model/AppModel.php @@ -76,7 +76,7 @@ class AppModel extends Model 21 => false, 22 => false, 23 => false, 24 => false, 25 => false, 26 => false, 27 => false, 28 => false, 29 => false, 30 => false, 31 => false, 32 => false, 33 => false, 34 => false, 35 => false, 36 => false, 37 => false, 38 => false, - 39 => false, 40 => false + 39 => false, 40 => false, 41 => false ); public $advanced_updates_description = array( @@ -1264,6 +1264,10 @@ class AppModel extends Model $sqlArray[] = "ALTER TABLE `user_settings` ADD `timestamp` int(11) NOT NULL;"; $indexArray[] = array('user_settings', 'timestamp'); break; + case 41: + $sqlArray[] = "ALTER TABLE `roles` ADD `enforce_rate_limit` tinyint(1) NOT NULL DEFAULT 0;"; + $sqlArray[] = "ALTER TABLE `roles` ADD `rate_limit_count` int(11) NOT NULL DEFAULT 0;"; + 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;'; @@ -2417,7 +2421,7 @@ class AppModel extends Model } } } - + /** * @param string $message * @param Exception $exception diff --git a/app/View/Roles/admin_add.ctp b/app/View/Roles/admin_add.ctp index 46b76d4fc..3ac6dd1ff 100644 --- a/app/View/Roles/admin_add.ctp +++ b/app/View/Roles/admin_add.ctp @@ -20,6 +20,19 @@ echo $this->Form->input('max_execution_time', array('label' => __('Maximum execution time') . ' (' . h($default_max_execution_time) . ')')); ?>
+ Form->input('enforce_rate_limit', array( + 'type' => 'checkbox', + 'label' => __('Enforce search rate limit') + )); + ?> +
+
+ Form->input('rate_limit_count', array('label' => __('# of searches / 15 min'))); + ?> +
+
$flag): @@ -52,8 +65,12 @@ echo $this->Form->end(); diff --git a/app/View/Roles/admin_edit.ctp b/app/View/Roles/admin_edit.ctp index 13b2ba062..12c9a83cf 100644 --- a/app/View/Roles/admin_edit.ctp +++ b/app/View/Roles/admin_edit.ctp @@ -19,6 +19,20 @@ echo $this->Form->input('max_execution_time', array('label' => __('Maximum execution time') . ' (' . h($default_max_execution_time) . ')')); ?>
+ Form->input('enforce_rate_limit', array( + 'type' => 'checkbox', + 'checked' => $this->request->data['Role']['enforce_rate_limit'], + 'label' => __('Enforce search rate limit') + )); + ?> +
+
+ Form->input('rate_limit_count', array('label' => __('# of searches / 15 min'))); + ?> +
+
$flag): @@ -50,8 +64,12 @@ diff --git a/app/View/Roles/admin_index.ctp b/app/View/Roles/admin_index.ctp index 7490cb4e7..28a5f5669 100644 --- a/app/View/Roles/admin_index.ctp +++ b/app/View/Roles/admin_index.ctp @@ -32,6 +32,7 @@ ?> Paginator->sort('memory_limit', __('Memory limit'));?> Paginator->sort('max_execution_time', __('Max execution time'));?> + Paginator->sort('rate_limit_count', __('Searches / 15 mins'));?> @@ -47,7 +48,7 @@ foreach ($list as $item): ?> echo sprintf( ' ', ($item['Role'][$k]) ? 'icon-ok' : '', - ($item['Role'][$k]) ? __('Yes') : __('No'), + ($item['Role'][$k]) ? __('Yes') : __('No'), sprintf( __('%s permission %s'), h($flagName), @@ -75,6 +76,15 @@ foreach ($list as $item): ?> } ?> + + + Html->link('', array('admin' => true, 'action' => 'edit', $item['Role']['id']), array('class' => 'fa fa-edit', 'title' => __('Edit'), 'aria-label' => __('Edit'))); ?> Form->postLink('', array('admin' => true, 'action' => 'delete', $item['Role']['id']), array('class' => 'fa fa-trash', 'title' => __('Delete'), 'aria-label' => __('Delete')), __('Are you sure you want to delete %s?', $item['Role']['name'])); ?> diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index 8baf113b3..f8309215f 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -1177,6 +1177,7 @@ function submitPopoverForm(context_id, referer, update_context_id) { break; } if (url !== null) { + url = baseurl + url; $.ajax({ beforeSend: function (XMLHttpRequest) { $(".loading").show(); @@ -4504,6 +4505,14 @@ function moveIndexRow(id, direction, endpoint) { }); } +function checkRoleEnforceRateLimit() { + if ($("#RoleEnforceRateLimit").is(':checked')) { + $('#rateLimitCountContainer').show(); + } else { + $('#rateLimitCountContainer').hide(); + } +} + (function(){ "use strict"; $(".datepicker").datepicker({