diff --git a/INSTALL/mysql.sql b/INSTALL/mysql.sql index 9f888dc..c2f8f64 100644 --- a/INSTALL/mysql.sql +++ b/INSTALL/mysql.sql @@ -186,6 +186,21 @@ CREATE TABLE `individuals` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `local_tools` +-- + +DROP TABLE IF EXISTS `local_tools`; +CREATE TABLE `local_tools` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `connector` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `settings` text COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`), + KEY `name` (`name`), + KEY `connector` (`connector`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + -- -- Table structure for table `organisation_encryption_keys` -- diff --git a/src/Application.php b/src/Application.php index 90e3865..8eb1704 100644 --- a/src/Application.php +++ b/src/Application.php @@ -135,7 +135,7 @@ class Application extends BaseApplication implements AuthenticationServiceProvid $service->loadAuthenticator('Authentication.Session'); $service->loadAuthenticator('Authentication.Form', [ 'fields' => $fields, - 'loginUrl' => '/users/login' + 'loginUrl' => \Cake\Routing\Router::url('/users/login') ]); // Load identifiers diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index e713f50..87f968d 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -99,7 +99,7 @@ class AppController extends Controller if (!empty($user['disabled'])) { $this->Authentication->logout(); $this->Flash->error(__('The user account is disabled.')); - return $this->redirect(['controller' => 'Users', 'action' => 'login']); + return $this->redirect(\Cake\Routing\Router::url('/users/login')); } unset($user['password']); $this->ACL->setUser($user); @@ -112,11 +112,6 @@ class AppController extends Controller $this->set('ajax', $this->request->is('ajax')); $this->request->getParam('prefix'); $this->set('darkMode', !empty(Configure::read('Cerebrate.dark'))); - if (!empty(Configure::read('baseurl'))) { - Configure::write('App.fullBaseUrl', Configure::read('baseurl')); - } else if (!empty(env('CEREBRATE_BASEURL'))) { - Configure::write('App.fullBaseUrl', env('CEREBRATE_BASEURL')); - } $this->set('baseurl', Configure::read('App.fullBaseUrl')); } diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index e64470d..ee373a1 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -45,10 +45,24 @@ class CRUDComponent extends Component } if ($this->Controller->ParamHandler->isRest()) { $data = $query->all(); + if (isset($options['afterFind'])) { + if (is_callable($options['afterFind'])) { + $data = $options['afterFind']($data); + } else { + $data = $this->Table->{$options['afterFind']}($data); + } + } $this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData($data, 'json'); } else { $this->Controller->loadComponent('Paginator'); $data = $this->Controller->Paginator->paginate($query); + if (isset($options['afterFind'])) { + if (is_callable($options['afterFind'])) { + $data = $options['afterFind']($data); + } else { + $data = $this->Table->{$options['afterFind']}($data); + } + } if (!empty($options['contextFilters'])) { $this->setFilteringContext($options['contextFilters'], $params); } @@ -63,7 +77,7 @@ class CRUDComponent extends Component $this->Controller->viewBuilder()->setLayout('ajax'); $this->Controller->render('/genericTemplates/filters'); } - + /** * getResponsePayload Returns the adaquate response payload based on the request context * diff --git a/src/Controller/LocalToolsController.php b/src/Controller/LocalToolsController.php new file mode 100644 index 0000000..5546051 --- /dev/null +++ b/src/Controller/LocalToolsController.php @@ -0,0 +1,122 @@ +LocalTools->extractMeta($this->LocalTools->getConnectors(), true); + if ($this->ParamHandler->isRest()) { + return $this->RestResponse->viewData($data, 'json'); + } + $data = $this->CustomPagination->paginate($data); + $this->set('data', $data); + if ($this->request->is('ajax')) { + $this->viewBuilder()->disableAutoLayout(); + } + $this->set('metaGroup', 'LocalTools'); + } + + public function connectorIndex() + { + $this->CRUD->index([ + 'filters' => ['name', 'connector'], + 'quickFilters' => ['name', 'connector'], + 'afterFind' => function($data) { + foreach ($data as $connector) { + $connector['health'] = [$this->LocalTools->healthCheckIndividual($connector)]; + } + return $data; + } + ]); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + $this->set('metaGroup', 'LocalTools'); + } + + public function action() + { + $params = []; + $results = $this->LocalTools->runAction(); + $this->render('add'); + } + + public function add() + { + $this->CRUD->add(); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + $connectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors()); + $dropdownData = ['connectors' => []]; + foreach ($connectors as $connector) { + $dropdownData['connectors'][$connector['connector']] = $connector['name']; + } + $this->set(compact('dropdownData')); + $this->set('metaGroup', 'LocalTools'); + } + + public function viewConnector($connector_name) + { + $connectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors()); + $connector = false; + foreach ($connectors as $c) { + if ($connector === false || version_compare($c['version'], $connectors['version']) > 0) { + $connector = $c; + } + } + if ($this->ParamHandler->isRest()) { + $this->restResponsePayload = $this->Controller->RestResponse->viewData($connector, 'json'); + } + $this->set('entity', $connector); + $this->set('metaGroup', 'LocalTools'); + } + + public function edit($id) + { + $this->CRUD->edit($id); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + $connectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors()); + $dropdownData = ['connectors' => []]; + foreach ($connectors as $connector) { + $dropdownData['connectors'][$connector['connector']] = $connector['name']; + } + $this->set(compact('dropdownData')); + $this->set('metaGroup', 'LocalTools'); + $this->render('add'); + } + + public function delete($id) + { + $this->CRUD->delete($id); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + $this->set('metaGroup', 'LocalTools'); + } + + public function view($id) + { + $this->CRUD->view($id); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + $this->set('metaGroup', 'LocalTools'); + } + + public function test() + { + $connectors = $this->LocalTools->getConnectors(); + $connectors['MispConnector']->test(); + } +} diff --git a/src/Controller/Open/IndividualsController.php b/src/Controller/Open/IndividualsController.php new file mode 100644 index 0000000..097489b --- /dev/null +++ b/src/Controller/Open/IndividualsController.php @@ -0,0 +1,35 @@ +Authentication->allowUnauthenticated(['index']); + } + + public function index() + { + $this->CRUD->index([ + 'filters' => ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id'], + 'quickFilters' => ['uuid', 'email', 'first_name', 'last_name', 'position'], + 'contain' => ['Alignments' => 'Organisations'] + ]); + if ($this->ParamHandler->isRest()) { + return $this->restResponsePayload; + } + $this->set('alignmentScope', 'organisations'); + $this->set('metaGroup', 'Public'); + } +} diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index db11453..dadca75 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -131,7 +131,7 @@ class UsersController extends AppController if ($result->isValid()) { $this->Authentication->logout(); $this->Flash->success(__('Goodbye.')); - return $this->redirect(['controller' => 'Users', 'action' => 'login']); + return $this->redirect(\Cake\Routing\Router::url('/users/login')); } } } diff --git a/src/Lib/default/local_tool_connectors/CommonConnectorTools.php b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php new file mode 100644 index 0000000..1a04acb --- /dev/null +++ b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php @@ -0,0 +1,32 @@ +exposedFunctions[] = $functionName; + } + + public function runAction($action, $params) { + if (!in_array($action, $exposedFunctions)) { + throw new MethodNotAllowedException(__('Invalid connector function called.')); + } + return $this->{$action}($params); + } + + public function health(Object $connection): array + { + return 0; + } +} + +?> diff --git a/src/Lib/default/local_tool_connectors/MispConnector.php b/src/Lib/default/local_tool_connectors/MispConnector.php new file mode 100644 index 0000000..838d620 --- /dev/null +++ b/src/Lib/default/local_tool_connectors/MispConnector.php @@ -0,0 +1,127 @@ +exposedFunctions[] = $functionName; + } + + public function health(Object $connection): array + { + $settings = json_decode($connection->settings, true); + $http = new Client(); + $response = $http->post($settings['url'] . '/users/view/me.json', '{}', [ + 'headers' => [ + 'AUTHORIZATION' => $settings['authkey'], + 'Accept' => 'Application/json', + 'Content-type' => 'Application/json' + ] + ]); + $responseCode = $response->getStatusCode(); + if ($response->isOk()) { + $status = 1; + $message = __('OK'); + } else if ($responseCode == 403){ + $status = 3; + $message = __('Unauthorized'); + } else { + $status = 0; + $message = __('Something went wrong.'); + } + + return [ + 'status' => $status, + 'message' => $message + ]; + } + + private function viewOrgAction(array $params): array + { + if (empty($params['connection'])) { + throw new InvalidArgumentException(__('No connection object received.')); + } + $settings = json_decode($params['connection']->settings, true); + $http = new Client(); + $url = '/users/organisations/index/scope:all'; + if (!empty($params['page'])) { + $url .= '/page:' . $params['page']; + } + if (!empty($params['limit'])) { + $url .= '/limit:' . $params['limit']; + } + if (!empty($params['quickFilter'])) { + $url .= '/searchall:' . $params['quickFilter']; + } + $response = $http->post($settings['url'] . '/users/organisations/index/scope:all', '{}', [ + 'headers' => [ + 'AUTHORIZATION' => $settings['authkey'], + 'Accept' => 'Application/json', + 'Content-type' => 'Application/json' + ] + ]); + $responseCode = $response->getStatusCode(); + if ($response->isOk()) { + return [ + 'type' => 'index', + 'data' => [ + 'data' => json_decode($response->getBody(), true), + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value' + ] + ] + ], + 'fields' => [ + [ + 'name' => '#', + 'sort' => 'id', + 'data_path' => 'Organisation.id', + ], + [ + 'name' => __('Name'), + 'sort' => 'name', + 'data_path' => 'Organisation.name', + ], + [ + 'name' => __('UUID'), + 'sort' => 'uuid', + 'data_path' => 'Organisation.uuid', + ] + ], + 'title' => false, + 'description' => false, + 'pull' => 'right', + 'actions' => [ + [ + 'url' => '/localTools/action/fetchOrg', + 'url_params_data_paths' => ['id'], + 'icon' => 'download' + ] + ] + ] + ]; + } else { + return __('Could not fetch the organisations, error code: {0}', $response->getStatusCode()); + } + } +} + + ?> diff --git a/src/Model/Behavior/SyncTool.php b/src/Model/Behavior/SyncTool.php new file mode 100644 index 0000000..ad3f756 --- /dev/null +++ b/src/Model/Behavior/SyncTool.php @@ -0,0 +1,172 @@ +createHttpSocket($params); + } + + public function setupHttpSocketFeed($feed = null) + { + return $this->setupHttpSocket(); + } + + /** + * @param array $params + * @return HttpSocket + * @throws Exception + */ + public function createHttpSocket($params = array()) + { + App::uses('HttpSocket', 'Network/Http'); + $HttpSocket = new HttpSocket($params); + $proxy = Configure::read('Proxy'); + if (empty($params['skip_proxy']) && isset($proxy['host']) && !empty($proxy['host'])) { + $HttpSocket->configProxy($proxy['host'], $proxy['port'], $proxy['method'], $proxy['user'], $proxy['password']); + } + return $HttpSocket; + } + + /** + * @param array $server + * @return array|void + * @throws Exception + */ + public static function getServerClientCertificateInfo(array $server) + { + if (!$server['client_cert_file']) { + return; + } + + $clientCertificate = new File(APP . "files" . DS . "certs" . DS . $server['id'] . '_client.pem'); + if (!$clientCertificate->exists()) { + throw new Exception("Certificate file '{$clientCertificate->pwd()}' doesn't exists."); + } + + $certificateContent = $clientCertificate->read(); + if ($certificateContent === false) { + throw new Exception("Could not read '{$clientCertificate->pwd()}' file with client certificate."); + } + + return self::getClientCertificateInfo($certificateContent); + } + + /** + * @param string $certificateContent PEM encoded certificate and private key. + * @return array + * @throws Exception + */ + private static function getClientCertificateInfo($certificateContent) + { + $certificate = openssl_x509_read($certificateContent); + if (!$certificate) { + throw new Exception("Could't parse certificate: " . openssl_error_string()); + } + $privateKey = openssl_pkey_get_private($certificateContent); + if (!$privateKey) { + throw new Exception("Could't get private key from certificate: " . openssl_error_string()); + } + $verify = openssl_x509_check_private_key($certificate, $privateKey); + if (!$verify) { + throw new Exception('Public and private key do not match.'); + } + return self::parseCertificate($certificate); + } + + /** + * @param mixed $certificate + * @return array + * @throws Exception + */ + private static function parseCertificate($certificate) + { + $parsed = openssl_x509_parse($certificate); + if (!$parsed) { + throw new Exception("Could't get parse X.509 certificate: " . openssl_error_string()); + } + $currentTime = new DateTime(); + $output = [ + 'serial_number' => $parsed['serialNumberHex'], + 'signature_type' => $parsed['signatureTypeSN'], + 'valid_from' => isset($parsed['validFrom_time_t']) ? new DateTime("@{$parsed['validFrom_time_t']}") : null, + 'valid_to' => isset($parsed['validTo_time_t']) ? new DateTime("@{$parsed['validTo_time_t']}") : null, + 'public_key_size' => null, + 'public_key_type' => null, + 'public_key_size_ok' => null, + ]; + + $output['valid_from_ok'] = $output['valid_from'] ? ($output['valid_from'] <= $currentTime) : null; + $output['valid_to_ok'] = $output['valid_to'] ? ($output['valid_to'] >= $currentTime) : null; + + $subject = []; + foreach ($parsed['subject'] as $type => $value) { + $subject[] = "$type=$value"; + } + $output['subject'] = implode(', ', $subject); + + $issuer = []; + foreach ($parsed['issuer'] as $type => $value) { + $issuer[] = "$type=$value"; + } + $output['issuer'] = implode(', ', $issuer); + + $publicKey = openssl_pkey_get_public($certificate); + if ($publicKey) { + $publicKeyDetails = openssl_pkey_get_details($publicKey); + if ($publicKeyDetails) { + $output['public_key_size'] = $publicKeyDetails['bits']; + switch ($publicKeyDetails['type']) { + case OPENSSL_KEYTYPE_RSA: + $output['public_key_type'] = 'RSA'; + $output['public_key_size_ok'] = $output['public_key_size'] >= 2048; + break; + case OPENSSL_KEYTYPE_DSA: + $output['public_key_type'] = 'DSA'; + $output['public_key_size_ok'] = $output['public_key_size'] >= 2048; + break; + case OPENSSL_KEYTYPE_DH: + $output['public_key_type'] = 'DH'; + break; + case OPENSSL_KEYTYPE_EC: + $output['public_key_type'] = "EC ({$publicKeyDetails['ec']['curve_name']})"; + $output['public_key_size_ok'] = $output['public_key_size'] >= 224; + break; + } + } + } + + return $output; + } +} diff --git a/src/Model/Entity/LocalTool.php b/src/Model/Entity/LocalTool.php new file mode 100644 index 0000000..f996d77 --- /dev/null +++ b/src/Model/Entity/LocalTool.php @@ -0,0 +1,11 @@ + 'UNKNOWN', + 1 => 'OK', + 2 => 'ISSUES', + 3 => 'ERROR', + ]; + + private $connectors = null; + + public function initialize(array $config): void + { + parent::initialize($config); + } + + public function validationDefault(Validator $validator): Validator + { + return $validator; + } + + public function getConnectors(string $name = null): array + { + $connectors = []; + $dirs = [ + ROOT . '/src/Lib/default/local_tool_connectors', + ROOT . '/src/Lib/custom/local_tool_connectors' + ]; + foreach ($dirs as $dir) { + $dir = new Folder($dir); + $files = $dir->find('.*Connector\.php'); + foreach ($files as $file) { + require_once($dir->pwd() . '/'. $file); + $className = substr($file, 0, -4); + $classNamespace = '\\' . $className . '\\' . $className; + if (empty($name) || $name === $className) { + $connectors[$className] = new $classNamespace; + } + } + } + return $connectors; + + } + + public function extractMeta(array $connector_classes, bool $includeConnections = false): array + { + $connectors = []; + foreach ($connector_classes as $connector_type => $connector_class) { + $connector = [ + 'name' => $connector_class->name, + 'connector' => $connector_type, + 'connector_version' => $connector_class->version, + 'connector_description' => $connector_class->description + ]; + if ($includeConnections) { + $connector['connections'] = $this->healthCheck($connector_type, $connector_class); + } + $connectors[] = $connector; + } + return $connectors; + } + + public function healthCheck(string $connector_type, Object $connector_class): array + { + $query = $this->find(); + $query->where([ + 'connector' => $connector_type + ]); + $connections = $query->all()->toList(); + foreach ($connections as &$connection) { + $connection = $this->healthCheckIndividual($connector_class, $connection); + } + return $connections; + } + + public function healthCheckIndividual(Object $connector): array + { + $connector_class = $this->getConnectors($connector['connector']); + if (empty($connector_class[$connector['connector']])) { + return []; + } + $connector_class = $connector_class[$connector['connector']]; + $health = $connector_class->health($connector); + return $connection = [ + 'name' => $connector->name, + 'health' => $health['status'], + 'message' => $health['message'], + 'url' => '/localTools/viewConnection/' . $connector['id'] + ]; + } +} diff --git a/src/View/AppView.php b/src/View/AppView.php index e4b82b6..87b775b 100644 --- a/src/View/AppView.php +++ b/src/View/AppView.php @@ -40,6 +40,7 @@ class AppView extends View { parent::initialize(); $this->loadHelper('Hash'); + $this->loadHelper('PrettyPrint'); $this->loadHelper('FormFieldMassage'); $this->loadHelper('Paginator', ['templates' => 'cerebrate-pagination-templates']); } diff --git a/src/View/Helper/PrettyPrintHelper.php b/src/View/Helper/PrettyPrintHelper.php new file mode 100644 index 0000000..c288cb3 --- /dev/null +++ b/src/View/Helper/PrettyPrintHelper.php @@ -0,0 +1,29 @@ + $value) { + if (is_array($value)) { + $value = $this->ppArray($value, $depth+1); + } else { + $value = h($value); + } + $text .= sprintf( + '
%s: %s
', + $status_colours[$healthElement['health']], + $name, + h($healthElement['message']) + ); + } + echo $lines; +?> diff --git a/templates/element/genericElements/header_scaffold.php b/templates/element/genericElements/header_scaffold.php index 1fe6a2f..ac999d8 100644 --- a/templates/element/genericElements/header_scaffold.php +++ b/templates/element/genericElements/header_scaffold.php @@ -53,7 +53,7 @@ foreach ($data['menu'] as $name => $menuElement) { } } $logoutButton = sprintf( - ' ', + ' ', $baseurl, __('Logout') );