From 599819f7f9cdae2e5d9d3a32caa9f25e1a5a2d7c Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 27 Feb 2021 11:13:47 +0100 Subject: [PATCH] new: [authkeys] Allowed IPs --- app/Controller/AppController.php | 95 +++++++++---- app/Controller/AuthKeysController.php | 30 +++- app/Controller/Component/CRUDComponent.php | 31 +++-- app/Controller/ServersController.php | 6 +- app/Lib/Tools/CidrTool.php | 20 +++ app/Model/AppModel.php | 5 +- app/Model/AuthKey.php | 101 +++++++++++--- app/Model/Log.php | 6 +- app/View/AuthKeys/add.ctp | 11 +- app/View/AuthKeys/index.ctp | 16 ++- app/View/AuthKeys/view.ctp | 129 +++++++++--------- .../IndexTable/Fields/datetime.ctp | 41 +++--- .../SingleViews/Fields/customField.ctp | 1 + app/webroot/css/main.css | 6 +- app/webroot/js/misp.js | 3 +- db_schema.json | 13 +- tests/testlive_security.py | 23 ++++ 17 files changed, 380 insertions(+), 157 deletions(-) create mode 100644 app/View/Elements/genericElements/SingleViews/Fields/customField.ctp diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php index 63c388cb1..7601603a4 100755 --- a/app/Controller/AppController.php +++ b/app/Controller/AppController.php @@ -125,8 +125,8 @@ class AppController extends Controller } if (!$this->_isRest()) { $this->__contentSecurityPolicy(); + $this->response->header('X-XSS-Protection', '1; mode=block'); } - $this->response->header('X-XSS-Protection', '1; mode=block'); if (!empty($this->params['named']['sql'])) { $this->sql_dump = intval($this->params['named']['sql']); @@ -446,22 +446,9 @@ class AppController extends Controller } 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); + if ($this->_shouldLog($authKeyToStore)) { $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->Log->createLogEntry('SYSTEM', 'auth_fail', 'User', 0, "Failed authentication using API key ($authKeyToStore)"); } $this->Session->destroy(); } @@ -548,8 +535,10 @@ class AppController extends Controller } if ($user['disabled']) { - $this->Log = ClassRegistry::init('Log'); - $this->Log->createLogEntry($user, 'auth_fail', 'User', $user['id'], 'Login attempt by disabled user.'); + if ($this->_shouldLog('disabled:' . $user['id'])) { + $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()) { @@ -565,11 +554,33 @@ class AppController extends Controller if (isset($user['authkey_expiration']) && $user['authkey_expiration']) { $time = isset($_SERVER['REQUEST_TIME']) ? $_SERVER['REQUEST_TIME'] : time(); if ($user['authkey_expiration'] < $time) { + if ($this->_shouldLog('expired:' . $user['authkey_id'])) { + $this->Log = ClassRegistry::init('Log'); + $this->Log->createLogEntry($user, 'auth_fail', 'User', $user['id'], "Login attempt by expired auth key {$user['authkey_id']}."); + } $this->Auth->logout(); throw new ForbiddenException('Auth key is expired'); } } + if (!empty($user['allowed_ips'])) { + App::uses('CidrTool', 'Tools'); + $cidrTool = new CidrTool($user['allowed_ips']); + $remoteIp = $this->_remoteIp(); + if ($remoteIp === null) { + $this->Auth->logout(); + throw new ForbiddenException('Auth key is limited to IP address, but IP address not found'); + } + if (!$cidrTool->contains($remoteIp)) { + if ($this->_shouldLog('not_allowed_ip:' . $user['authkey_id'] . ':' . $remoteIp)) { + $this->Log = ClassRegistry::init('Log'); + $this->Log->createLogEntry($user, 'auth_fail', 'User', $user['id'], "Login attempt from not allowed IP address for auth key {$user['authkey_id']}."); + } + $this->Auth->logout(); + throw new ForbiddenException('It is not possible to use this Auth key from your IP address'); + } + } + $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) { @@ -632,7 +643,7 @@ class AppController extends Controller return; } - $remoteAddress = trim($_SERVER['REMOTE_ADDR']); + $remoteAddress = $this->_remoteIp(); $pipe = $redis->multi(Redis::PIPELINE); // keep for 30 days @@ -680,11 +691,7 @@ class AppController extends Controller $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 - } + $this->Log->createLogEntry($user, 'request', 'User', $user['id'], 'Paranoid log entry', $change); } } @@ -1500,17 +1507,47 @@ class AppController extends Controller throw new RuntimeException("User with ID {$sessionUser['id']} not exists."); } if (isset($sessionUser['authkey_id'])) { + // Reload authkey $this->loadModel('AuthKey'); - if (!$this->AuthKey->exists($sessionUser['authkey_id'])) { + $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']; } - foreach (['authkey_id', 'authkey_expiration', 'logged_by_authkey'] as $copy) { - if (isset($sessionUser[$copy])) { - $user[$copy] = $sessionUser[$copy]; - } + if (isset($sessionUser['logged_by_authkey'])) { + $user['logged_by_authkey'] = $sessionUser['logged_by_authkey']; } $this->Auth->login($user); return $user; } + + /** + * @return string|null + */ + protected function _remoteIp() + { + $ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR'; + return isset($_SERVER[$ipHeader]) ? trim($_SERVER[$ipHeader]) : null; + } + + /** + * @param string $key + * @return bool Returns true if the same log defined by $key was not stored in last hour + */ + protected function _shouldLog($key) + { + $redis = $this->User->setupRedis(); + if ($redis && !$redis->exists('misp:auth_fail_throttling:' . $key)) { + $redis->setex('misp:auth_fail_throttling:' . $key, 3600, 1); + return true; + } + return false; + } } diff --git a/app/Controller/AuthKeysController.php b/app/Controller/AuthKeysController.php index b90bb2fa4..47e1fef57 100644 --- a/app/Controller/AuthKeysController.php +++ b/app/Controller/AuthKeysController.php @@ -71,8 +71,34 @@ class AuthKeysController extends AppController public function edit($id) { - $this->set('metaGroup', 'admin'); - $this->set('metaAction', 'authkeys_edit'); + $this->CRUD->edit($id, [ + 'conditions' => $this->__prepareConditions(), + 'afterFind' => function (array $authKey) { + unset($authKey['AuthKey']['authkey']); + if (is_array($authKey['AuthKey']['allowed_ips'])) { + $authKey['AuthKey']['allowed_ips'] = implode("\n", $authKey['AuthKey']['allowed_ips']); + } + $authKey['AuthKey']['expiration'] = date('Y-m-d H:i:s', $authKey['AuthKey']['expiration']); + return $authKey; + }, + 'fields' => ['comment', 'allowed_ips', 'expiration'], + ]); + if ($this->IndexFilter->isRest()) { + return $this->restResponsePayload; + } + $this->set('dropdownData', [ + 'user' => $this->User->find('list', [ + 'sort' => ['username' => 'asc'], + 'conditions' => ['id' => $this->request->data['AuthKey']['user_id']], + ]) + ]); + $this->set('menuData', [ + 'menuList' => $this->_isSiteAdmin() ? 'admin' : 'globalActions', + 'menuItem' => 'authKeyAdd', + ]); + $this->set('edit', true); + $this->set('validity', Configure::read('Security.advanced_authkeys_validity')); + $this->render('add'); } public function add($user_id = false) diff --git a/app/Controller/Component/CRUDComponent.php b/app/Controller/Component/CRUDComponent.php index 1e682c197..5a63087c4 100644 --- a/app/Controller/Component/CRUDComponent.php +++ b/app/Controller/Component/CRUDComponent.php @@ -145,13 +145,21 @@ class CRUDComponent extends Component if (empty($id)) { throw new NotFoundException(__('Invalid %s.', $modelName)); } - $data = $this->Controller->{$modelName}->find('first', - isset($params['get']) ? $params['get'] : [ - 'recursive' => -1, - 'conditions' => [ - 'id' => $id - ] - ]); + $query = isset($params['get']) ? $params['get'] : [ + 'recursive' => -1, + 'conditions' => [ + 'id' => $id + ], + ]; + if (!empty($params['conditions'])) { + $query['conditions']['AND'][] = $params['conditions']; + } + /** @var Model $model */ + $model = $this->Controller->{$modelName}; + $data = $model->find('first', $query); + if (isset($params['afterFind'])) { + $data = $params['afterFind']($data); + } if ($this->Controller->request->is('post') || $this->Controller->request->is('put')) { $input = $this->Controller->request->data; if (empty($input[$modelName])) { @@ -171,7 +179,10 @@ class CRUDComponent extends Component $data[$modelName][$field] = $fieldData; } } - if ($this->Controller->{$modelName}->save($data)) { + if (isset($params['beforeSave'])) { + $data = $params['beforeSave']($data); + } + if ($model->save($data)) { $message = __('%s updated.', $modelName); if ($this->Controller->IndexFilter->isRest()) { $this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData($data, 'json'); @@ -182,7 +193,9 @@ class CRUDComponent extends Component } } else { 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 { diff --git a/app/Controller/ServersController.php b/app/Controller/ServersController.php index 7e07d5b3d..52a95d388 100644 --- a/app/Controller/ServersController.php +++ b/app/Controller/ServersController.php @@ -2434,9 +2434,9 @@ misp.direct_call(relative_path, body) } $message = 'CSP reported violation'; - $ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR'; - if (isset($_SERVER[$ipHeader])) { - $message .= ' from IP ' . $_SERVER[$ipHeader]; + $remoteIp = $this->_remoteIp(); + if ($remoteIp) { + $message .= ' from IP ' . $remoteIp; } $this->log("$message: " . json_encode($report['csp-report'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); diff --git a/app/Lib/Tools/CidrTool.php b/app/Lib/Tools/CidrTool.php index faf978f3d..b21351c0c 100644 --- a/app/Lib/Tools/CidrTool.php +++ b/app/Lib/Tools/CidrTool.php @@ -66,6 +66,26 @@ class CidrTool return $match; } + /** + * @param string $cidr + * @return bool + */ + public static function validate($cidr) + { + $parts = explode('/', $cidr, 2); + $ipBytes = inet_pton($parts[0]); + if ($ipBytes === false) { + return false; + } + + $maximumNetmask = strlen($ipBytes) === 4 ? 32 : 128; + if (isset($parts[1]) && ($parts[1] > $maximumNetmask || $parts[1] < 0)) { + return false; // Netmask part of CIDR is invalid + } + + return true; + } + /** * Using solution from https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/IpUtils.php * diff --git a/app/Model/AppModel.php b/app/Model/AppModel.php index 60c220c87..3f4e0d216 100644 --- a/app/Model/AppModel.php +++ b/app/Model/AppModel.php @@ -89,7 +89,7 @@ class AppModel extends Model 45 => false, 46 => false, 47 => false, 48 => false, 49 => false, 50 => false, 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 + 63 => true, 64 => false, 65 => false, 66 => false, 67 => false, ); public $advanced_updates_description = array( @@ -1572,6 +1572,9 @@ class AppModel extends Model $sqlArray[] = "ALTER TABLE `galaxy_clusters` MODIFY COLUMN `tag_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '';"; $indexArray[] = ['event_reports', 'event_id']; break; + case 67: + $sqlArray[] = "ALTER TABLE `auth_keys` ADD `allowed_ips` text DEFAULT NULL;"; + 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;'; diff --git a/app/Model/AuthKey.php b/app/Model/AuthKey.php index f4711b6b7..50de35909 100644 --- a/app/Model/AuthKey.php +++ b/app/Model/AuthKey.php @@ -1,6 +1,7 @@ data['AuthKey']['id'])) { if (empty($this->data['AuthKey']['uuid'])) { $this->data['AuthKey']['uuid'] = CakeText::uuid(); @@ -42,22 +42,66 @@ 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'] = $validity ? strtotime("+$validity days") : 0; + if (!empty($this->data['AuthKey']['allowed_ips'])) { + if (is_string($this->data['AuthKey']['allowed_ips'])) { + $this->data['AuthKey']['allowed_ips'] = trim($this->data['AuthKey']['allowed_ips']); + if (empty($this->data['AuthKey']['allowed_ips'])) { + $this->data['AuthKey']['allowed_ips'] = []; + } else { + $this->data['AuthKey']['allowed_ips'] = explode("\n", $this->data['AuthKey']['allowed_ips']); + $this->data['AuthKey']['allowed_ips'] = array_map('trim', $this->data['AuthKey']['allowed_ips']); + } + } + if (!is_array($this->data['AuthKey']['allowed_ips'])) { + $this->invalidate('allowed_ips', 'Allowed IPs must be array'); + } + foreach ($this->data['AuthKey']['allowed_ips'] as $cidr) { + if (!CidrTool::validate($cidr)) { + $this->invalidate('allowed_ips', "$cidr is not valid IP range"); + } + } + } + + $creationTime = isset($this->data['AuthKey']['created']) ? $this->data['AuthKey']['created'] : time(); + $validity = Configure::read('Security.advanced_authkeys_validity'); + if (empty($this->data['AuthKey']['expiration'])) { + $this->data['AuthKey']['expiration'] = $validity ? strtotime("+$validity days", $creationTime) : 0; + } else { + $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", $creationTime)) { + $this->invalidate('expiration', __('Maximal key validity is %s days.', $validity)); + } + $this->data['AuthKey']['expiration'] = $expiration; + } + + return true; + } + + public function afterFind($results, $primary = false) + { + foreach ($results as $key => $val) { + if (isset($val['AuthKey']['allowed_ips'])) { + $results[$key]['AuthKey']['allowed_ips'] = $this->jsonDecode($val['AuthKey']['allowed_ips']); + } + } + return $results; + } + + public function beforeSave($options = array()) + { + if (isset($this->data['AuthKey']['allowed_ips'])) { + if (empty($this->data['AuthKey']['allowed_ips'])) { + $this->data['AuthKey']['allowed_ips'] = null; } else { - $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; + $this->data['AuthKey']['allowed_ips'] = json_encode($this->data['AuthKey']['allowed_ips']); } } return true; @@ -71,9 +115,9 @@ class AuthKey extends AppModel { $start = substr($authkey, 0, 4); $end = substr($authkey, -4); - $existing_authkeys = $this->find('all', [ + $possibleAuthkeys = $this->find('all', [ 'recursive' => -1, - 'fields' => ['id', 'authkey', 'user_id', 'expiration'], + 'fields' => ['id', 'authkey', 'user_id', 'expiration', 'allowed_ips'], 'conditions' => [ 'OR' => [ 'expiration >' => time(), @@ -84,12 +128,13 @@ class AuthKey extends AppModel ] ]); $passwordHasher = $this->getHasher(); - foreach ($existing_authkeys as $existing_authkey) { - if ($passwordHasher->check($authkey, $existing_authkey['AuthKey']['authkey'])) { - $user = $this->User->getAuthUser($existing_authkey['AuthKey']['user_id']); + foreach ($possibleAuthkeys as $possibleAuthkey) { + if ($passwordHasher->check($authkey, $possibleAuthkey['AuthKey']['authkey'])) { + $user = $this->User->getAuthUser($possibleAuthkey['AuthKey']['user_id']); if ($user) { - $user['authkey_id'] = $existing_authkey['AuthKey']['id']; - $user['authkey_expiration'] = $existing_authkey['AuthKey']['expiration']; + $user['authkey_id'] = $possibleAuthkey['AuthKey']['id']; + $user['authkey_expiration'] = $possibleAuthkey['AuthKey']['expiration']; + $user['allowed_ips'] = $possibleAuthkey['AuthKey']['allowed_ips']; } return $user; } @@ -175,6 +220,18 @@ class AuthKey extends AppModel return $output; } + /** + * When key is modified, update `date_modified` for user that was assigned to that key, so session data + * will be realoaded. + * @see AppController::_refreshAuth + */ + public function afterSave($created, $options = array()) + { + parent::afterSave($created, $options); + $userId = $this->data['AuthKey']['user_id']; + $this->User->updateAll(['date_modified' => time()], ['User.id' => $userId]); + } + /** * When key is deleted, update after `date_modified` for user that was assigned to that key, so session data * will be realoaded and canceled. diff --git a/app/Model/Log.php b/app/Model/Log.php index 1cf7aee90..19b7aeb64 100644 --- a/app/Model/Log.php +++ b/app/Model/Log.php @@ -199,7 +199,7 @@ class Log extends AppModel * @param int $modelId * @param string $title * @param string|array $change - * @return array + * @return array|null * @throws Exception * @throws InvalidArgumentException */ @@ -238,6 +238,10 @@ class Log extends AppModel )); if (!$result) { + if ($action === 'request' && !empty(Configure::read('MISP.log_paranoid_skip_db'))) { + return null; + } + throw new Exception("Cannot save log because of validation errors: " . json_encode($this->validationErrors)); } diff --git a/app/View/AuthKeys/add.ctp b/app/View/AuthKeys/add.ctp index 62e5a40db..a3cd94042 100644 --- a/app/View/AuthKeys/add.ctp +++ b/app/View/AuthKeys/add.ctp @@ -1,7 +1,7 @@ element('genericElements/Form/genericForm', [ 'data' => [ - 'title' => __('Add auth key'), + 'title' => isset($edit) ? __('Edit auth key') : __('Add auth key'), 'description' => __('Auth keys are used for API access. A user can have more than one authkey, so if you would like to use separate keys per tool that queries MISP, add additional keys. Use the comment field to make identifying your keys easier.'), 'fields' => [ [ @@ -13,7 +13,14 @@ echo $this->element('genericElements/Form/genericForm', [ [ 'field' => 'comment', 'label' => __('Comment'), - 'class' => 'span6' + 'class' => 'span6', + 'rows' => 4, + ], + [ + 'field' => 'allowed_ips', + 'label' => __('Allowed IPs'), + 'class' => 'span6', + 'rows' => 4, ], [ 'field' => 'expiration', diff --git a/app/View/AuthKeys/index.ctp b/app/View/AuthKeys/index.ctp index 127132961..77c3ab45f 100644 --- a/app/View/AuthKeys/index.ctp +++ b/app/View/AuthKeys/index.ctp @@ -63,12 +63,17 @@ 'data_path' => 'AuthKey.last_used', 'element' => 'datetime', 'requirements' => $keyUsageEnabled, + 'empty' => __('Never'), ], [ 'name' => __('Comment'), 'sort' => 'AuthKey.comment', 'data_path' => 'AuthKey.comment', ], + [ + 'name' => __('Allowed IPs'), + 'data_path' => 'AuthKey.allowed_ips', + ], ], 'title' => empty($ajax) ? __('Authentication key Index') : false, 'description' => empty($ajax) ? __('A list of API keys bound to a user.') : false, @@ -80,7 +85,16 @@ 'AuthKey.id' ), 'icon' => 'eye', - 'dbclickAction' => true + 'dbclickAction' => true, + 'title' => 'View auth key', + ], + [ + 'url' => $baseurl . '/auth_keys/edit', + 'url_params_data_paths' => array( + 'AuthKey.id' + ), + 'icon' => 'edit', + 'title' => 'Edit auth key', ], [ 'onclick' => sprintf( diff --git a/app/View/AuthKeys/view.ctp b/app/View/AuthKeys/view.ctp index 37f786371..43b79780b 100644 --- a/app/View/AuthKeys/view.ctp +++ b/app/View/AuthKeys/view.ctp @@ -15,65 +15,72 @@ if (isset($keyUsage)) { $uniqueIps = null; } -echo $this->element( - 'genericElements/SingleViews/single_view', - [ - 'title' => 'Auth key view', - 'data' => $data, - 'fields' => [ - [ - 'key' => __('ID'), - 'path' => 'AuthKey.id' - ], - [ - 'key' => __('UUID'), - 'path' => 'AuthKey.uuid', - ], - [ - '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', - 'type' => 'datetime' - ], - [ - 'key' => __('Expiration'), - 'path' => 'AuthKey.expiration', - 'type' => 'expiration' - ], - [ - 'key' => __('Key usage'), - 'type' => 'sparkline', - 'path' => 'AuthKey.id', - 'csv' => [ - 'data' => $keyUsageCsv, - ], - 'requirement' => isset($keyUsage), - ], - [ - 'key' => __('Last used'), - 'raw' => $lastUsed ? $this->Time->time($lastUsed) : __('Not used yet'), - 'requirement' => isset($keyUsage), - ], - [ - 'key' => __('Unique IPs'), - 'raw' => $uniqueIps, - 'requirement' => isset($keyUsage), - ] +echo $this->element('genericElements/SingleViews/single_view', [ + 'title' => 'Auth key view', + 'data' => $data, + 'fields' => [ + [ + 'key' => __('ID'), + 'path' => 'AuthKey.id' ], - ] -); + [ + 'key' => __('UUID'), + 'path' => 'AuthKey.uuid', + ], + [ + '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' => __('Allowed IPs'), + 'type' => 'custom', + 'function' => function (array $data) { + if (is_array($data['AuthKey']['allowed_ips'])) { + return implode("
", array_map('h', $data['AuthKey']['allowed_ips'])); + } + return __('All'); + } + ], + [ + 'key' => __('Created'), + 'path' => 'AuthKey.created', + 'type' => 'datetime' + ], + [ + 'key' => __('Expiration'), + 'path' => 'AuthKey.expiration', + 'type' => 'expiration' + ], + [ + 'key' => __('Key usage'), + 'type' => 'sparkline', + 'path' => 'AuthKey.id', + 'csv' => [ + 'data' => $keyUsageCsv, + ], + 'requirement' => isset($keyUsage), + ], + [ + 'key' => __('Last used'), + 'raw' => $lastUsed ? $this->Time->time($lastUsed) : __('Not used yet'), + 'requirement' => isset($keyUsage), + ], + [ + 'key' => __('Unique IPs'), + 'raw' => $uniqueIps, + 'requirement' => isset($keyUsage), + ] + ], +]); diff --git a/app/View/Elements/genericElements/IndexTable/Fields/datetime.ctp b/app/View/Elements/genericElements/IndexTable/Fields/datetime.ctp index 34d3f3bbf..b78c98c5a 100644 --- a/app/View/Elements/genericElements/IndexTable/Fields/datetime.ctp +++ b/app/View/Elements/genericElements/IndexTable/Fields/datetime.ctp @@ -1,26 +1,27 @@ 1) { - $data = implode(', ', $data); +$data = Hash::extract($row, $field['data_path']); +if (is_array($data)) { + if (count($data) > 1) { + $data = implode(', ', $data); + } else { + if (count($data) > 0) { + $data = $data[0]; } else { - if (count($data) > 0) { - $data = $data[0]; - } else { - $data = ''; - } + $data = ''; } } - if (empty($data) && !empty($field['empty'])) { - $data = $field['empty']; - } +} +if (empty($data) && !empty($field['empty'])) { + $data = $field['empty']; +} else { $data = $this->Time->time($data); - if (!empty($field['onClick'])) { - $data = sprintf( - '%s', - $field['onClick'], - $data - ); - } - echo $data; +} +if (!empty($field['onClick'])) { + $data = sprintf( + '%s', + $field['onClick'], + $data + ); +} +echo $data; diff --git a/app/View/Elements/genericElements/SingleViews/Fields/customField.ctp b/app/View/Elements/genericElements/SingleViews/Fields/customField.ctp new file mode 100644 index 000000000..a056047b4 --- /dev/null +++ b/app/View/Elements/genericElements/SingleViews/Fields/customField.ctp @@ -0,0 +1 @@ +