diff --git a/modules/admin/default-enable b/modules/admin/default-enable new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/modules/admin/lib/ConfigController.php b/modules/admin/lib/ConfigController.php new file mode 100644 index 0000000000000000000000000000000000000000..0a06413b2c0282a135c2420e8b468bdafdce6986 --- /dev/null +++ b/modules/admin/lib/ConfigController.php @@ -0,0 +1,388 @@ +<?php + +namespace SimpleSAML\Module\admin; + +use SimpleSAML\HTTP\RunnableResponse; +use SimpleSAML\Locale\Translate; +use SimpleSAML\Utils\HTTP; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Controller class for the admin module. + * + * This class serves the configuration views available in the module. + * + * @package SimpleSAML\Module\admin + */ +class ConfigController +{ + + const LATEST_VERSION_STATE_KEY = 'core:latest_simplesamlphp_version'; + const RELEASES_API = 'https://api.github.com/repos/simplesamlphp/simplesamlphp/releases/latest'; + + /** @var \SimpleSAML\Configuration */ + protected $config; + + /** @var \SimpleSAML\Session */ + protected $session; + + + /** + * ConfigController constructor. + * + * @param \SimpleSAML\Configuration $config The configuration to use. + * @param \SimpleSAML\Session $session The current user session. + */ + public function __construct(\SimpleSAML\Configuration $config, \SimpleSAML\Session $session) + { + $this->config = $config; + $this->session = $session; + } + + + /** + * Display basic diagnostic information on hostname, port and protocol. + * + * @param Request $request The current request. + * + * @return \SimpleSAML\XHTML\Template + */ + public function diagnostics(Request $request) + { + \SimpleSAML\Utils\Auth::requireAdmin(); + + $t = new \SimpleSAML\XHTML\Template($this->config, 'admin:diagnostics.twig'); + $t->data = [ + 'remaining' => $this->session->getAuthData('admin', 'Expire') - time(), + 'logouturl' => \SimpleSAML\Utils\Auth::getAdminLogoutURL(), + 'items' => [ + 'HTTP_HOST' => [$request->getHost()], + 'HTTPS' => $request->isSecure() ? ['on'] : [], + 'SERVER_PROTOCOL' => [$request->getProtocolVersion()], + 'getBaseURL()' => [HTTP::getBaseURL()], + 'getSelfHost()' => [HTTP::getSelfHost()], + 'getSelfHostWithNonStandardPort()' => [HTTP::getSelfHostWithNonStandardPort()], + 'getSelfURLHost()' => [HTTP::getSelfURLHost()], + 'getSelfURLNoQuery()' => [HTTP::getSelfURLNoQuery()], + 'getSelfHostWithPath()' => [HTTP::getSelfHostWithPath()], + 'getFirstPathElement()' => [HTTP::getFirstPathElement()], + 'getSelfURL()' => [HTTP::getSelfURL()], + ], + ]; + return $t; + } + + + /** + * Display the main admin page. + * + * @return \SimpleSAML\XHTML\Template + */ + public function main() + { + \SimpleSAML\Utils\Auth::requireAdmin(); + + $t = new \SimpleSAML\XHTML\Template($this->config, 'admin:config.twig'); + $t->data = [ + 'warnings' => $this->getWarnings(), + 'directory' => $this->config->getBaseDir(), + 'version' => $this->config->getVersion(), + 'links' => [ + [ + 'href' => \SimpleSAML\Module::getModuleURL('admin/diagnostics'), + 'text' => Translate::noop('Diagnostics on hostname, port and protocol') + ], + [ + 'href' => \SimpleSAML\Module::getModuleURL('admin/phpinfo'), + 'text' => Translate::noop('Information on your PHP installation') + ] + ], + 'enablematrix' => [ + 'saml20idp' => $this->config->getBoolean('enable.saml20-idp', false), + 'shib13idp' => $this->config->getBoolean('enable.shib13-idp', false), + ], + 'funcmatrix' => $this->getPrerequisiteChecks(), + 'logouturl' => \SimpleSAML\Utils\Auth::getAdminLogoutURL(), + ]; + + \SimpleSAML\Module::callHooks('configpage', $t); + + return $t; + } + + + /** + * Display the output of phpinfo(). + * + * @return RunnableResponse + */ + public function phpinfo() + { + return new RunnableResponse('phpinfo'); + } + + + /** + * @return array + */ + protected function getPrerequisiteChecks() + { + $matrix = [ + [ + 'required' => 'required', + 'descr' => 'PHP version >= 5.5. You run: '.phpversion(), + 'enabled' => version_compare(phpversion(), '5.5', '>=') + ] + ]; + $store = $this->config->getString('store.type', ''); + + // check dependencies used via normal functions + $functions = [ + 'time' => [ + 'required' => 'required', + 'descr' => [ + 'required' => Translate::noop('Date/Time Extension'), + ] + ], + 'hash' => [ + 'required' => 'required', + 'descr' => [ + 'required' => Translate::noop('Hashing function'), + ] + ], + 'gzinflate' => [ + 'required' => 'required', + 'descr' => [ + 'required' => Translate::noop('ZLib'), + ] + ], + 'openssl_sign' => [ + 'required' => 'required', + 'descr' => [ + 'required' => Translate::noop('OpenSSL'), + ] + ], + 'dom_import_simplexml' => [ + 'required' => 'required', + 'descr' => [ + 'required' => Translate::noop('XML DOM'), + ] + ], + 'preg_match' => [ + 'required' => 'required', + 'descr' => [ + 'required' => Translate::noop('Regular expression support'), + ] + ], + 'json_decode' => [ + 'required' => 'required', + 'descr' => [ + 'required' => Translate::noop('JSON support'), + ] + ], + 'class_implements' => [ + 'required' => 'required', + 'descr' => [ + 'required' => Translate::noop('Standard PHP library (SPL)'), + ] + ], + 'mb_strlen' => [ + 'required' => 'required', + 'descr' => [ + 'required' => Translate::noop('Multibyte String extension'), + ] + ], + 'curl_init' => [ + 'required' => $this->config->getBoolean('admin.checkforupdates', true) ? 'required' : 'optional', + 'descr' => [ + 'optional' => Translate::noop( + 'cURL (might be required by some modules)' + ), + 'required' => Translate::noop( + 'cURL (required if automatic version checks are used, also by some modules)' + ), + ] + ], + 'session_start' => [ + 'required' => $store === 'phpsession' ? 'required' : 'optional', + 'descr' => [ + 'optional' => Translate::noop('Session extension (required if PHP extensions are used)'), + 'required' => Translate::noop('Session extension'), + ] + ], + 'pdo_drivers' => [ + 'required' => $store === 'sql' ? 'required' : 'optional', + 'descr' => [ + 'optional' => Translate::noop('Session extension (required if PHP extensions are used)'), + 'required' => Translate::noop('Session extension'), + ] + ], + 'ldap_bind' => [ + 'required' => \SimpleSAML\Module::isModuleEnabled('ldap') ? 'required' : 'optional', + 'descr' => [ + 'optional' => Translate::noop('LDAP extension (required if an LDAP backend is used)'), + 'required' => Translate::noop('LDAP extension'), + ] + ], + 'radius_auth_open' => [ + 'required' => \SimpleSAML\Module::isModuleEnabled('radius') ? 'required' : 'optional', + 'descr' => [ + 'optional' => Translate::noop('Radius extension (required if a radius backend is used)'), + 'required' => Translate::noop('Radius extension'), + ] + ], + ]; + + foreach ($functions as $function => $description) { + $matrix[] = [ + 'required' => $description['required'], + 'descr' => $description['descr'][$description['required']], + 'enabled' => function_exists($function), + ]; + } + + // check object-oriented external libraries and extensions + $libs = [ + [ + 'classes' => ['\Predis\Predis'], + 'required' => $store === 'redis' ? 'required' : 'optional', + 'descr' => [ + 'optional' => Translate::noop('predis/predis (required if the redis data store is used'), + 'required' => Translate::noop('predis/predis library'), + ] + ], + [ + 'classes' => ['\Memcache', '\Memcached'], + 'required' => $store === 'memcache' ? 'required' : 'optional', + 'descr' => [ + 'optional' => Translate::noop( + 'Memcache or Memcached extension (required if the memcache backend is used' + ), + 'required' => Translate::noop('Memcache or Memcached extension'), + ] + ] + ]; + + foreach ($libs as $lib) { + $enabled = false; + foreach ($lib['classes'] as $class) { + $enabled |= class_exists($class); + } + $matrix[] = [ + 'required' => $lib['required'], + 'descr' => $lib['descr'][$lib['required']], + 'enabled' => $enabled, + ]; + } + + // perform some basic configuration checks + $matrix[] = [ + 'required' => 'optional', + 'descr' => Translate::noop('The <code>technicalcontact_email</code> configuration option should be set'), + 'enabled' => $this->config->getString('technicalcontact_email', 'na@example.org') !== 'na@example.org', + ]; + + $matrix[] = [ + 'required' => 'required', + 'descr' => Translate::noop('The auth.adminpassword configuration option must be set'), + 'enabled' => $this->config->getString('auth.adminpassword', '123') !== '123', + ]; + + return $matrix; + } + + + /** + * Compile a list of warnings about the current deployment. + * + * The returned array can contain either strings that can be translated directly, or arrays. If an element is an + * array, the first value in that array is a string that can be translated, and the second value will be a hashed + * array that contains the substitutions that must be applied to the translation, with its corresponding value. This + * can be used in twig like this, assuming an element called "e": + * + * {{ e[0]|trans(e[1])|raw }} + * + * @return array + */ + protected function getWarnings() + { + $warnings = []; + + // make sure we're using HTTPS + if (!\SimpleSAML\Utils\HTTP::isHTTPS()) { + $warnings[] = Translate::noop( + '<strong>You are not using HTTPS</strong> to protect communications with your users. HTTP works fine '. + 'for testing purposes, but in a production environment you should use HTTPS. <a '. + 'href="https://simplesamlphp.org/docs/stable/simplesamlphp-maintenance">Read more about the '. + 'maintenance of SimpleSAMLphp</a>.' + ); + } + + // make sure we have a secret salt set + if ($this->config->getValue('secretsalt') === 'defaultsecretsalt') { + $warnings[] = Translate::noop( + '<strong>The configuration uses the default secret salt</strong>. Make sure to modify the <code>'. + 'secretsalt</code> option in the SimpleSAMLphp configuration in production environments. <a '. + 'href="https://simplesamlphp.org/docs/stable/simplesamphp-install">Read more about the '. + 'maintenance of SimpleSAMLphp</a>.' + ); + } + + // check for URL limitations + if (extension_loaded('suhosin')) { + $len = ini_get('suhosin.get.max_value_length'); + if (empty($len) || $len < 2048) { + $warnings[] = Translate::noop( + 'The length of query parameters is limited by the PHP Suhosin extension. Please increase the '. + '<code>suhosin.get.max_value_length</code> option in your php.ini to at least 2048 bytes.' + ); + } + } + + /* + * Check for updates. Store the remote result in the session so we don't need to fetch it on every access to + * this page. + */ + if ($this->config->getBoolean('admin.checkforupdates', true) && $this->config->getVersion() !== 'master') { + if (!function_exists('curl_init')) { + $warnings[] = Translate::noop( + 'The cURL PHP extension is missing. Cannot check for SimpleSAMLphp updates.' + ); + } else { + $latest = $this->session->getData(self::LATEST_VERSION_STATE_KEY, "version"); + + if (!$latest) { + $ch = curl_init(self::RELEASES_API); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, 'SimpleSAMLphp'); + curl_setopt($ch, CURLOPT_TIMEOUT, 2); + curl_setopt($ch, CURLOPT_PROXY, $this->config->getString('proxy', null)); + curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->config->getValue('proxy.auth', null)); + $response = curl_exec($ch); + + if (curl_getinfo($ch, CURLINFO_HTTP_CODE) === 200) { + $latest = json_decode($response, true); + $this->session->setData(self::LATEST_VERSION_STATE_KEY, 'version', $latest); + } + curl_close($ch); + } + + + if ($latest && version_compare($this->config->getVersion(), ltrim($latest['tag_name'], 'v'), 'lt')) { + $warnings[] = [ + Translate::noop( + 'You are running an outdated version of SimpleSAMLphp. Please update to <a href="'. + '%latest%">the latest version</a> as soon as possible.' + ), + [ + '%latest%' => $latest['html_url'] + ] + ]; + } + } + } + + return $warnings; + } +} diff --git a/modules/admin/routes.yaml b/modules/admin/routes.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e19374da6b73a94b139884daf5b1f801b9139d3d --- /dev/null +++ b/modules/admin/routes.yaml @@ -0,0 +1,15 @@ +admin-main: + path: / + defaults: { _controller: 'SimpleSAML\Module\admin\ConfigController::main' } +admin-diagnostics: + path: /diagnostics + defaults: { _controller: 'SimpleSAML\Module\admin\ConfigController::diagnostics' } +admin-phpinfo: + path: /phpinfo + defaults: { _controller: 'SimpleSAML\Module\admin\ConfigController::phpinfo' } +admin-test: + path: /test/{as} + defaults: { _controller: 'SimpleSAML\Module\admin\TestController::main', as: null } +admin-fed: + path: /federation + defaults: { _controller: 'SimpleSAML\Module\admin\FederationController::main' } diff --git a/modules/admin/templates/config.twig b/modules/admin/templates/config.twig new file mode 100644 index 0000000000000000000000000000000000000000..58bbbbbbef5eba4df1f137c19e9536b4fddf4b27 --- /dev/null +++ b/modules/admin/templates/config.twig @@ -0,0 +1,60 @@ +{% set pagetitle = 'SimpleSAMLphp installation page'|trans %} +{% set frontpage_section = 'main' %} +{% extends "base.twig" %} + +{% block content %} + {%- include "@admin/includes/menu.twig" %} + + {%- for key, warning in warnings %} + {%- if warning is iterable %} + + <div class="message-box warning">{{ warning[0]|trans(warning[1])|raw }}</div> + {%- else %} + + <div class="message-box warning">{{ warning|trans|raw }}</div> + {%- endif %} + {%- endfor %} + + <br/> + <div><code class="simplesaml_version">{{ directory }} ({{ version }})</code></div> + <div class="enablebox mini"> + <table> + <tr class="{%- if enablematrix.saml20idp %}enabled{% else %}disabled{% endif -%}"> + <td>SAML 2.0 IdP</td> + <td><i class="fa fa-{%- if enablematrix.saml20idp %}check{% else %}ban{% endif %}"></i></td> + </tr> + <tr class="{%- if enablematrix.shib13idp %}enabled{% else %}disabled{% endif -%}"> + <td>Shib 1.3 IdP</td> + <td><i class="fa fa-{%- if enablematrix.shib13idp %}check{% else %}ban{% endif %}"></i></td> + </tr> + </table> + </div> + <h2>{% trans %}Configuration{% endtrans %}</h2> + <ul> + {%- for key, link in links %} + + <li><a href="{{ link.href }}">{{ link.text }}</a></li> + {%- endfor %} + + </ul> + <h2>{% trans %}Checking your PHP installation{% endtrans %}</h2> + <div class="enablebox"> + <table> + {%- for key, func in funcmatrix %} + + <tr class="{%- if func.enabled %}enabled{% else %}disabled{% endif -%}"> + <td><i class="fa fa-{%- if func.enabled %}check{% else %}ban{% endif -%}"></i></td> + <td> + {%- if func.required == 'required' %} + {%- trans %}required{% endtrans %} + {%- else %} + {%- trans %}optional{% endtrans %} + {%- endif -%} + </td> + <td>{{ func.descr|trans|raw }}</td> + </tr> + {%- endfor %} + + </table> + </div> +{% endblock %} diff --git a/modules/admin/templates/includes/menu.twig b/modules/admin/templates/includes/menu.twig new file mode 100644 index 0000000000000000000000000000000000000000..7400477dffae0928fc2cc3f65d90634d541d5d74 --- /dev/null +++ b/modules/admin/templates/includes/menu.twig @@ -0,0 +1,20 @@ +<div class="pure-g frontpage-menu"> + <div class="pure-u-2-3"> + <div class="pure-menu pure-menu-horizontal"> + <ul class="pure-menu-list"> + <li class="pure-menu-item{% if frontpage_section == "main" %} pure-menu-selected{% endif %}"> + <a href="/{{ baseurlpath }}module.php/admin/" class="pure-menu-link">Admin</a> + </li> + <li class="pure-menu-item{% if frontpage_section == "test" %} pure-menu-selected{% endif %}"> + <a href="/{{ baseurlpath }}module.php/admin/test" class="pure-menu-link">Test</a> + </li> + <li class="pure-menu-item{% if frontpage_section == "federation" %} pure-menu-selected{% endif %}"> + <a href="/{{ baseurlpath }}module.php/admin/federation" class="pure-menu-link">Federation</a> + </li> + <li class="pure-menu-item"> + <a href="{{ logouturl }}" class="pure-menu-link">Logout</a> + </li> + </ul> + </div> + </div> +</div> diff --git a/www/admin/index.php b/www/admin/index.php index 41e9c200af4a8218ad2d143ccd4dfe9f0ce14e5c..2541aa1d0198460667613b7bcc18fff9392f2fb0 100644 --- a/www/admin/index.php +++ b/www/admin/index.php @@ -2,6 +2,8 @@ require_once('../_include.php'); +\SimpleSAML\Utils\HTTP::redirectTrustedURL(\SimpleSAML\Module::getModuleURL('admin/')); + // Load SimpleSAMLphp configuration $config = \SimpleSAML\Configuration::getInstance(); $session = \SimpleSAML\Session::getSessionFromRequest();