openrat-cms

OpenRat Content Management System
git clone http://git.code.weiherhei.de/openrat-cms.git
Log | Files | Refs | README

commit 985c8651434792e6c5c9a8525c8c74e3ce6672ca
parent fcbde6c91e684c0516fa388f400d312bc668873c
Author: Jan Dankert <develop@jandankert.de>
Date:   Thu, 19 Nov 2020 00:45:44 +0100

Security fix: We must update the login token on every login; Administrators are able to see the login tokens of users.

Diffstat:
Mmodules/cms/Dispatcher.class.php | 100+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mmodules/cms/action/Action.class.php | 25++-----------------------
Mmodules/cms/action/login/LoginLoginAction.class.php | 220++++++++++++++++++++++++++++++++++++-------------------------------------------
Mmodules/cms/action/login/LoginLogoutAction.class.php | 5+++--
Amodules/cms/action/user/UserAdvancedAction.class.php | 26++++++++++++++++++++++++++
Mmodules/cms/api/API.class.php | 5+++--
Mmodules/cms/auth/CookieAuth.class.php | 6++----
Mmodules/cms/auth/InternalAuth.class.php | 2--
Mmodules/cms/auth/RememberAuth.class.php | 63++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mmodules/cms/model/User.class.php | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mmodules/cms/ui/UI.class.php | 2+-
Amodules/cms/ui/themes/default/html/views/user/advanced.php | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/cms/ui/themes/default/html/views/user/advanced.tpl.src.xml | 38++++++++++++++++++++++++++++++++++++++
Mmodules/database/Database.class.php | 1-
Mmodules/template_engine/components/html/component_date/component-date.php | 7++++---
Amodules/util/Cookie.class.php | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
16 files changed, 513 insertions(+), 276 deletions(-)

diff --git a/modules/cms/Dispatcher.class.php b/modules/cms/Dispatcher.class.php @@ -20,6 +20,7 @@ use cms\update\Update; use Exception; use language\Language; use language\Messages; +use util\Cookie; use util\ClassName; use util\ClassUtils; use util\exception\ObjectNotFoundException; @@ -248,8 +249,8 @@ class Dispatcher // Sprache lesen $languages = []; - if (isset($_COOKIE[ Action::COOKIE_LANGUAGE])) - $languages[] = $_COOKIE[Action::COOKIE_LANGUAGE]; + if ( Cookie::has( Action::COOKIE_LANGUAGE)) + $languages[] = Cookie::get(Action::COOKIE_LANGUAGE); $i18nConfig = (new Config($conf))->subset('i18n'); @@ -376,67 +377,64 @@ class Dispatcher */ private function connectToDatabase() { - $firstDbContact = ! Session::getDatabaseId() || $this->request->hasRequestVar('dbid'); + $dbConfig = Configuration::subset('database'); + + // Filter all enabled databases + $databases = array_filter($dbConfig->subsets(), function ($dbConfig) { + return $dbConfig->is('enabled',true); + }); + + $dbids = array_keys( $databases ); + + if ( ! $dbids ) + throw new \RuntimeException('No database configured.'); + + $firstDbContact = ! Session::getDatabaseId(); + + $possibleDbIds = []; if ( $this->request->hasRequestVar('dbid') ) - $dbid = $this->request->getRequestVar('dbid',RequestParams::FILTER_ALPHANUM); - elseif ( Session::getDatabaseId() ) - $dbid = Session::getDatabaseId(); - elseif ( isset($_COOKIE[Action::COOKIE_DB_ID]) ) - $dbid = $_COOKIE[Action::COOKIE_DB_ID]; - else { - $databases = Configuration::subset('database')->subsets(); - - // Filter all enabled databases - $databases = array_filter($databases, function ($dbConfig) { - return $dbConfig->is('enabled',true); - }); - - - $dbids = array_keys( $databases ); - - $defaultDbId = Configuration::subset('database-default')->get('default-id' ); - - if ( $defaultDbId && in_array($defaultDbId,$dbids) ) - // Default-Datenbankverbindung ist konfiguriert und vorhanden. - $dbid = $defaultDbId; - elseif ( count($dbids) > 0) - // Datenbankverbindungen sind vorhanden, wir nehmen die erste. - $dbid = $dbids[0]; - else - // Keine Datenbankverbindung vorhanden. Fallback: - throw new \RuntimeException('No database configured'); - } + $possibleDbIds[] = $this->request->getRequestVar('dbid',RequestParams::FILTER_ALPHANUM); + if ( Session::getDatabaseId() ) + $possibleDbIds[] = Session::getDatabaseId(); - $dbConfig = Configuration::subset('database'); + if ( Cookie::has(Action::COOKIE_DB_ID) ) + $possibleDbIds[] = Cookie::get(Action::COOKIE_DB_ID); - if ( ! $dbConfig->has( $dbid ) ) - throw new \LogicException( 'unknown DB-Id: '.$dbid ); + $possibleDbIds[] = Configuration::subset('database-default')->get('default-id' ); - $dbConfig = $dbConfig->subset($dbid ); + $possibleDbIds[] = $dbids[0]; - if ( ! $dbConfig->is('enabled',true ) ) - throw new \RuntimeException('Database connection \''.$dbid.'\' is not enabled'); + foreach( $possibleDbIds as $dbid ) { + if ( $dbConfig->has( $dbid ) ) { - try - { - $key = $this->request->isAction?'write':'read'; + $dbConfig = $dbConfig->subset($dbid ); - $db = new Database( $dbConfig->subset($key)->getConfig() + $dbConfig->getConfig() ); - $db->id = $dbid; + try + { + $key = $this->request->isAction?'write':'read'; + + $db = new Database( $dbConfig->merge( $dbConfig->subset($key))->getConfig() ); + $db->id = $dbid; + + Session::setDatabaseId( $dbid ); + Session::setDatabase ( $db ); + } + catch(\Exception $e) { + throw new UIException(Messages::DATABASE_CONNECTION_ERROR, $e->getMessage(),$e); + } - Session::setDatabaseId( $dbid ); - Session::setDatabase( $db ); - }catch(\Exception $e) - { - throw new UIException(Messages::DATABASE_CONNECTION_ERROR, $e->getMessage(),$e); - } + if ( $firstDbContact ) + // Test, if we should update the database scheme. + $this->updateDatabase( $dbid ); + + return; + } + } - if ( $firstDbContact ) - // Test, if we should update the database scheme. - $this->updateDatabase( $dbid ); + throw new LogicException('Unreachable code'); // at least the first db connection should be found } diff --git a/modules/cms/action/Action.class.php b/modules/cms/action/Action.class.php @@ -7,6 +7,7 @@ use cms\base\Language as L; use cms\model\ModelBase; use cms\model\User; use logger\Logger; +use util\Cookie; use util\ClassUtils; use util\Session; @@ -423,7 +424,6 @@ class Action const COOKIE_TIMEZONE_OFFSET = 'or_timezone_offset'; - /** * Sets a cookie. * @@ -432,27 +432,6 @@ class Action */ protected function setCookie($name, $value = '' ) { - $cookieConfig = Configuration::subset('security')->subset('cookie'); - - if ( ! $value ) - $expire = time(); // Cookie wird gelöscht. - else - $expire = time() + 60 * 60 * 24 * $cookieConfig->get('expire',2*365); // default: 2 years - - $cookieAttributes = [ - rawurlencode($name).'='.rawurlencode($value), - 'Expires='.date('r',$expire), - 'Path='.COOKIE_PATH - ]; - - if ( $cookieConfig->is('secure',false ) ) - $cookieAttributes[] = 'Secure'; - - if ( $cookieConfig->is('httponly',true ) ) - $cookieAttributes[] = 'HttpOnly'; - - $cookieAttributes[] = 'SameSite='.$cookieConfig->get('samesite','Lax'); - - header('Set-Cookie: '.implode('; ',$cookieAttributes) ); + Cookie::set( $name, $value ); } } diff --git a/modules/cms/action/login/LoginLoginAction.class.php b/modules/cms/action/login/LoginLoginAction.class.php @@ -8,7 +8,9 @@ use cms\auth\Auth; use cms\auth\AuthRunner; use cms\auth\InternalAuth; use cms\base\Configuration; +use cms\base\DB; use cms\model\User; +use language\Language; use language\Messages; use logger\Logger; use security\Password; @@ -85,6 +87,7 @@ class LoginLoginAction extends LoginAction implements Method { public function post() { + Session::setUser(null); // Altes Login entfernen. if ( Configuration::subset('login')->is('nologin',false ) ) @@ -96,21 +99,23 @@ class LoginLoginAction extends LoginAction implements Method { $newPassword2 = $this->getRequestVar('password2' ,RequestParams::FILTER_ALPHANUM); $token = $this->getRequestVar('user_token' ,RequestParams::FILTER_ALPHANUM); - // Der Benutzer hat zwar ein richtiges Kennwort eingegeben, aber dieses ist abgelaufen. - // Wir versuchen hier, das neue zu setzen (sofern eingegeben). - if ( empty($newPassword1) ) - { - // Kein neues Kennwort, - // nichts zu tun... - } - else - { - $auth = new InternalAuth(); - $passwordConfig = Configuration::subset(['security','password']); + // Jedes Authentifizierungsmodul durchlaufen, bis ein Login erfolgreich ist. + $authResult = AuthRunner::checkLogin('authenticate',$loginName,$loginPassword, $token ); + Password::delay(); - if ( $auth->login($loginName, $loginPassword,$token) || $auth->mustChangePassword ) + $mustChangePassword = ( $authResult === Auth::STATUS_PW_EXPIRED ); + $tokenFailed = ( $authResult === Auth::STATUS_TOKEN_NEEDED ); + $loginOk = ( $authResult === Auth::STATUS_SUCCESS ); + + if ( $mustChangePassword ) { + + // Der Benutzer hat zwar ein richtiges Kennwort eingegeben, aber dieses ist abgelaufen. + // Wir versuchen hier, das neue zu setzen (sofern eingegeben). + if ( $newPassword1 ) { + $passwordConfig = Configuration::subset(['security','password']); + if ( $newPassword1 != $newPassword2 ) { $this->addValidationError('password1',Messages::PASSWORDS_DO_NOT_MATCH); @@ -128,131 +133,106 @@ class LoginLoginAction extends LoginAction implements Method { // Kennwoerter identisch und lang genug. $user = User::loadWithName($loginName,User::AUTH_TYPE_INTERNAL); $user->setPassword( $newPassword1,true ); - - // Das neue gesetzte Kennwort für die weitere Authentifizierung benutzen. $loginPassword = $newPassword1; + + $loginOk = true; + $mustChangePassword = false; } } - else - { - // Anmeldung gescheitert. - $this->addNotice('user', 0, $loginName, 'LOGIN_FAILED', 'error', array('name' => $loginName)); - $this->addValidationError('login_name' ,''); - $this->addValidationError('login_password',''); - return; - } } - - // Cookie setzen - $this->setCookie(Action::COOKIE_USERNAME,$loginName ); - $this->setCookie(Action::COOKIE_DB_ID ,$this->getRequestVar('dbid')); - // Jedes Authentifizierungsmodul durchlaufen, bis ein Login erfolgreich ist. - $result = AuthRunner::checkLogin('authenticate',$loginName,$loginPassword, $token ); + if ( $tokenFailed ) { + // Token falsch. + $this->addNotice('user', 0, $loginName, 'LOGIN_FAILED_TOKEN_FAILED', 'error'); + $this->addValidationError('user_token',''); + return; + } - $mustChangePassword = ( $result === Auth::STATUS_PW_EXPIRED ); - $tokenFailed = ( $result === Auth::STATUS_TOKEN_NEEDED ); - $loginOk = ( $result === Auth::STATUS_SUCCESS ); + if ( $mustChangePassword ) { + // Anmeldung gescheitert, Benutzer muss Kennwort ?ndern. + $this->addNotice('user', 0, $loginName, 'LOGIN_FAILED_MUSTCHANGEPASSWORD', 'error'); + $this->addValidationError('password1',''); + $this->addValidationError('password2',''); + return; + } - if ( $loginOk ) - { - Logger::info('Login successful for '.$loginName); + $ip = getenv("REMOTE_ADDR"); - try - { - // Benutzer über den Benutzernamen laden. - $user = User::loadWithName($loginName,User::AUTH_TYPE_INTERNAL,null); - $user->setCurrent(); - $user->updateLoginTimestamp(); - - if ($user->passwordAlgo != Password::bestAlgoAvailable() ) - // Re-Hash the password with a better hash algo. - $user->setPassword($loginPassword); - - } - catch( ObjectNotFoundException $ex ) - { - // Benutzer wurde zwar authentifiziert, ist aber in der - // internen Datenbank nicht vorhanden - if ( Configuration::subset(['security','newuser'])->is('autoadd',true ) ) - { - // Neue Benutzer in die interne Datenbank uebernehmen. - $user = new User(); - $user->name = $loginName; - $user->fullname = $loginName; - $user->persist(); - } - else - { - // Benutzer soll nicht angelegt werden. - // Daher ist die Anmeldung hier gescheitert. - $loginOk = false; - } - } - } - - Password::delay(); - - $ip = getenv("REMOTE_ADDR"); - - if ( !$loginOk ) - { - // Anmeldung nicht erfolgreich - - Logger::debug( TextMessage::create('login failed for user ${name} from IP ${ip}', + if ( ! $loginOk ) { + Logger::debug(TextMessage::create('login failed for user ${name} from IP ${ip}', [ 'name' => $loginName, - 'ip' => $ip + 'ip' => $ip ] - ) ); + )); - if ( $tokenFailed ) - { - // Token falsch. - $this->addNotice('user', 0, $loginName, 'LOGIN_FAILED_TOKEN_FAILED', 'error'); - $this->addValidationError('user_token',''); - } - elseif ( $mustChangePassword ) - { - // Anmeldung gescheitert, Benutzer muss Kennwort ?ndern. - $this->addNotice('user', 0, $loginName, 'LOGIN_FAILED_MUSTCHANGEPASSWORD', 'error'); - $this->addValidationError('password1',''); - $this->addValidationError('password2',''); - } - else - { + // Anmeldung gescheitert. + $this->addNotice('user', 0, $loginName, 'LOGIN_FAILED', 'error', array('name' => $loginName)); + $this->addValidationError('login_name', ''); + $this->addValidationError('login_password', ''); + return; + } + + Logger::info(TextMessage::create('Login successful for user ${0}', [$loginName])); + Logger::debug("Login successful for user '$loginName' from IP $ip"); + + try { + // Benutzer über den Benutzernamen laden. + $user = User::loadWithName($loginName, User::AUTH_TYPE_INTERNAL, null); + $user->setCurrent(); + $user->updateLoginTimestamp(); + + if ($user->passwordAlgo != Password::bestAlgoAvailable()) + // Re-Hash the password with a better hash algo. + $user->setPassword($loginPassword); + + } catch (ObjectNotFoundException $ex) { + // Benutzer wurde zwar authentifiziert, ist aber in der + // internen Datenbank nicht vorhanden + if (Configuration::subset(['security', 'newuser'])->is('autoadd', true)) { + + // Neue Benutzer in die interne Datenbank uebernehmen. + $user = new User(); + $user->name = $loginName; + $user->fullname = $loginName; + $user->persist(); + Logger::debug( TextMessage::create('user ${0} authenticated successful and added to internal user table',[$loginName]) ); + $user->updateLoginTimestamp(); + } else { + // Benutzer soll nicht angelegt werden. + // Daher ist die Anmeldung hier gescheitert. // Anmeldung gescheitert. + Logger::warn( TextMessage::create('user ${0} authenticated successful, but not found in internal user table',[$loginName]) ); + $this->addNotice('user', 0, $loginName, 'LOGIN_FAILED', 'error', array('name' => $loginName)); - $this->addValidationError('login_name' ,''); - $this->addValidationError('login_password',''); - } + $this->addValidationError('login_name', ''); + $this->addValidationError('login_password', ''); - return; + return; + } } - else - { - - Logger::debug("Login successful for user '$loginName' from IP $ip"); - if ( $this->hasRequestVar('remember') ) - { - // Cookie setzen - $this->setCookie(Action::COOKIE_USERNAME,$user->name ); - $this->setCookie(Action::COOKIE_TOKEN ,$user->createNewLoginToken() ); - } - - // Anmeldung erfolgreich. - if ( Configuration::subset('security')->is('renew_session_login',false) ) - $this->recreateSession(); - - $this->addNoticeFor( $user,Messages::LOGIN_OK, array('name' => $user->getName() )); - - $config = Session::getConfig(); - $language = new \language\Language(); - $config['language'] = $language->getLanguage($user->language); - $config['language']['language_code'] = $user->language; - Session::setConfig( $config ); + // Cookie setzen + $this->setCookie(Action::COOKIE_DB_ID ,DB::get()->id ); + $this->setCookie(Action::COOKIE_USERNAME,$user->name ); + + if ( $this->hasRequestVar('remember') ) { + // Sets the login token cookie + $this->setCookie(Action::COOKIE_TOKEN ,$user->createNewLoginToken() ); } - + + // Anmeldung erfolgreich. + if ( Configuration::subset('security')->is('renew_session_login',false) ) + $this->recreateSession(); + + $this->addNoticeFor( $user,Messages::LOGIN_OK, array('name' => $user->getName() )); + + // Setting the user-defined language + $config = Session::getConfig(); + $language = new Language(); + $config['language'] = $language->getLanguage($user->language); + $config['language']['language_code'] = $user->language; + + Session::setConfig( $config ); } } diff --git a/modules/cms/action/login/LoginLogoutAction.class.php b/modules/cms/action/login/LoginLogoutAction.class.php @@ -5,6 +5,7 @@ use cms\action\LoginAction; use cms\action\Method; use cms\base\Configuration; use language\Messages; +use util\Cookie; use util\Session; @@ -17,9 +18,9 @@ class LoginLogoutAction extends LoginAction implements Method { $this->recreateSession(); // Reading the login token cookie - list( $selector,$token ) = array_pad( explode('.',@$_COOKIE[Action::COOKIE_TOKEN]),2,''); + list( $selector,$token ) = array_pad( explode('.',Cookie::get(Action::COOKIE_TOKEN)),2,''); - // Logout forces the removal of all login tokens + // Logout forces the removal of the login token for this device if ( $selector ) $this->currentUser->deleteLoginToken( $selector ); diff --git a/modules/cms/action/user/UserAdvancedAction.class.php b/modules/cms/action/user/UserAdvancedAction.class.php @@ -0,0 +1,26 @@ +<?php +namespace cms\action\user; +use cms\action\Method; +use cms\action\UserAction; +use cms\base\Configuration; +use cms\base\Startup; +use language\Messages; +use security\Base2n; +use security\Password; + +/** + * Shows the login tokens of this user. + * + * @package cms\action\user + */ +class UserAdvancedAction extends UserAction implements Method { + + public function view() { + $token = $this->user->getLoginTokens(); + + $this->setTemplateVar( 'token', $token ); + } + + public function post() { + } +} diff --git a/modules/cms/api/API.class.php b/modules/cms/api/API.class.php @@ -139,9 +139,10 @@ class API if (!headers_sent()) // HTTP Spec: - // Applications SHOULD use this field to indicate the transfer-length of the message-body, unless this is prohibited by the rules in section 4.4. + // "Applications SHOULD use this field to indicate the transfer-length of the + // message-body, unless this is prohibited by the rules in section 4.4." // - // And the overhead of Transfer-Encoding chunked is eliminated... + // And the overhead of 'Transfer-Encoding: chunked' is eliminated... header('Content-Length: ' . strlen($output)); echo $output; diff --git a/modules/cms/auth/CookieAuth.class.php b/modules/cms/auth/CookieAuth.class.php @@ -4,6 +4,7 @@ namespace cms\auth; use cms\action\Action; use cms\auth\Auth; +use util\Cookie; /** * Using the username from a cookie. @@ -14,10 +15,7 @@ class CookieAuth implements Auth { public function username() { - if (isset($_COOKIE[ Action::COOKIE_USERNAME ])) - return $_COOKIE[ Action::COOKIE_USERNAME ]; - else - return null; + return Cookie::get( Action::COOKIE_USERNAME,null ); } diff --git a/modules/cms/auth/InternalAuth.class.php b/modules/cms/auth/InternalAuth.class.php @@ -16,8 +16,6 @@ use security\Password; */ class InternalAuth implements Auth { - var $mustChangePassword = false; - /** * Ueberpruefen des Kennwortes * ueber die Benutzertabelle in der Datenbank. diff --git a/modules/cms/auth/RememberAuth.class.php b/modules/cms/auth/RememberAuth.class.php @@ -5,11 +5,16 @@ namespace cms\auth; use cms\action\Action; use cms\auth\Auth; use cms\base\Configuration; +use cms\base\DB; +use cms\base\Startup; use cms\model\Text; use database\Database; use cms\model\User; use logger\Logger; +use util\Cookie; +use security\Password; use \util\exception\ObjectNotFoundException; +use util\Session; use util\text\TextMessage; /** @@ -25,11 +30,11 @@ class RememberAuth implements Auth public function username() { // Ermittelt den Benutzernamen aus den Login-Cookies. - if (isset($_COOKIE[Action::COOKIE_TOKEN]) && - isset($_COOKIE[Action::COOKIE_DB_ID])) { + if ( Cookie::has(Action::COOKIE_TOKEN) && + Cookie::has(Action::COOKIE_DB_ID) ) { try { - list($selector, $token) = array_pad(explode('.', $_COOKIE[Action::COOKIE_TOKEN]), 2, ''); - $dbid = $_COOKIE[Action::COOKIE_DB_ID]; + list($selector, $token) = array_pad(explode('.', Cookie::get(Action::COOKIE_TOKEN)), 2, ''); + $dbid = Cookie::get( Action::COOKIE_DB_ID ); $dbConfig = Configuration::subset('database'); @@ -44,7 +49,7 @@ class RememberAuth implements Auth $key = 'read'; // Only reading in database. - $db = new Database($dbConfig->subset($key)->getConfig() + $dbConfig->getConfig()); + $db = new Database($dbConfig->merge( $dbConfig->subset($key) )->getConfig()); $db->id = $dbid; $db->start(); @@ -55,15 +60,41 @@ class RememberAuth implements Auth SQL ); $stmt->setString('selector', $selector); - $stmt->setInt('now', time()); + $stmt->setInt ('now' , Startup::getStartTime() ); $auth = $stmt->getRow(); + $db->disconnect(); + if ($auth) { - if (\security\Password::check($token, $auth['token'], $auth['token_algo'])) - return $auth['username']; + $this->makeDBWritable( $dbid ); // FIXME: This is a hack, how to do this better? + // serial was found. + $username = $auth['username']; + $userid = $auth['userid' ]; + $user = new User( $userid ); + + if (Password::check($token, $auth['token'], $auth['token_algo'])) { + Cookie::set(Action::COOKIE_TOKEN ,$user->createNewLoginTokenForSerial($selector) ); + DB::get()->commit(); + return $username; + } + else { + // serial match but token mismatched. + // this means, the token was used on another device before, probably stolen. + Logger::warn( TextMessage::create('Possible breakin-attempt detected for user ${0}',[$username])); + $user->deleteAllLoginTokens(); // Disable all token logins for this user. + Cookie::set(Action::COOKIE_TOKEN ); // Delete token cookie + + // we must not reset the password here, because the thief might not have it. + + return null; + } + } else { + // The serial is not found, maybe expired. + // There is nothing we should do here. } + } catch (ObjectNotFoundException $e) { // Benutzer nicht gefunden. } @@ -80,6 +111,16 @@ SQL { return null; } -} -?>- \ No newline at end of file + protected function makeDBWritable( $dbid ) { + + $dbConfig = Configuration::subset(['database',$dbid]); + + $key = 'write'; + $db = new Database($dbConfig->merge( $dbConfig->subset($key) )->getConfig()); + $db->id = $dbid; + $db->start(); + + Session::setDatabase( $db ); + } +}+ \ No newline at end of file diff --git a/modules/cms/model/User.class.php b/modules/cms/model/User.class.php @@ -21,6 +21,8 @@ namespace cms\model; use cms\base\Configuration; use cms\base\DB as Db; use cms\base\Startup; +use cms\base\Language; +use language\Messages; use security\Password; use util\exception\ObjectNotFoundException; @@ -174,11 +176,11 @@ SQL /** * Lesen aller Projekte, fuer die der Benutzer berechtigt ist. * - * @return Array [Projekt-Id] = Projekt-Name + * @return array [Projekt-Id] = Projekt-Name */ public function getReadableProjects() { - $db = \cms\base\DB::get(); + $db = Db::get(); if ( $this->isAdmin ) { @@ -212,7 +214,7 @@ SQL /** * Ermittelt alls Projekte, fuer die der Benutzer berechtigt ist. - * @return Array [0..n] = Projekt-Id + * @return array [0..n] = Projekt-Id */ function getReadableProjectIds() { @@ -220,10 +222,30 @@ SQL } + /** + * Gets all login tokens for this user. + * + * @return array + */ + public function getLoginTokens() { + + $stmt = Db::sql( <<<SQL + SELECT selector,expires,create_date,platform,name + FROM {{auth}} + WHERE userid={userid} +SQL + ); + + $stmt->setInt('userid',$this->getId() ); + + return $stmt->getAll(); + } /** - * Ermittelt zu diesem Benutzer den Login-Token. - */ + * Creates a completly new login token. + * + * @return string new login token + */ function createNewLoginToken() { $selector = Password::randomHexString(24); @@ -239,7 +261,7 @@ SQL VALUES( {id},{userid},{selector},{token},{token_algo},{expires},{create_date},{platform},{name} ) SQL ); - $expirationPeriodDays = \cms\base\Configuration::Conf()->subset('user')->subset('security')->get('token_expires_after_days',730); + $expirationPeriodDays = Configuration::Conf()->subset('user')->subset('security')->get('token_expires_after_days',730); $stmt->setInt( 'id' ,++$count ); $stmt->setInt( 'userid' ,$this->userid ); @@ -261,8 +283,42 @@ SQL } - /** - * Ermittelt zu diesem Benutzer den Login-Token. + + /** + * Creates a new login token for a serial. + * + * @param $selector string selector + * @return string new login token + */ + public function createNewLoginTokenForSerial( $selector ) + { + $algo = Password::ALGO_SHA1; + $token = Password::randomHexString(24); + + $tokenHash = Password::hash($token,$algo); + + $stmt = Db::sql( <<<SQL + UPDATE {{auth}} + SET token={token},token_algo={token_algo} + WHERE selector={selector} +SQL + ); + + $stmt->setString( 'selector' ,$selector ); + $stmt->setString( 'token' ,$tokenHash ); + $stmt->setInt ( 'token_algo' ,$algo ); + $stmt->execute(); + + // Zusammensetzen des Tokens + return $selector.'.'.$token; + } + + + + + /** + * Deletes a login token. + * @param $selector string selector */ function deleteLoginToken( $selector ) { @@ -324,7 +380,7 @@ SQL return null; // no user found. // Benutzer �ber Id instanziieren - $neuerUser = new \cms\model\User( $userId ); + $neuerUser = new User( $userId ); $neuerUser->load(); @@ -397,9 +453,9 @@ SQL * @param int Benutzer-Id * @return String Benutzername */ - function getUserName( $userid ) + public static function getUserName( $userid ) { - $db = \cms\base\DB::get(); + $db = Db::get(); $sql = $db->sql( 'SELECT name FROM {{user}}'. ' WHERE id={userid}' ); @@ -408,7 +464,7 @@ SQL $name = $sql->getOne(); if ( $name == '' ) - return \cms\base\Language::lang('UNKNOWN'); + return Language::lang(Messages::UNKNOWN); else return $name; } @@ -555,7 +611,7 @@ SQL */ public function delete() { - $db = \cms\base\DB::get(); + $db = Db::get(); // "Erzeugt von" f�r diesen Benutzer entfernen. $sql = $db->sql( 'UPDATE {{object}} '. @@ -590,14 +646,7 @@ SQL $sql->setInt ('userid',$this->userid ); $sql->query(); - $stmt = Db::sql( <<<SQL - DELETE FROM {{auth}} - WHERE userid={userid} -SQL - ); - $stmt->setInt ('userid',$this->userid ); - $stmt->execute(); - + $this->deleteAllLoginTokens(); // Benutzer loeschen $sql = $db->sql( 'DELETE FROM {{user}} '. 'WHERE id={userid}' ); @@ -609,6 +658,23 @@ SQL /** + * Delete all Login tokens for this user. + * + * @throws \util\exception\DatabaseException + */ + public function deleteAllLoginTokens() { + + $stmt = Db::sql( <<<SQL + DELETE FROM {{auth}} + WHERE userid={userid} +SQL + ); + $stmt->setInt ('userid',$this->userid ); + $stmt->execute(); + } + + + /** * Ermitteln der Eigenschaften zu diesem Benutzer * * @return array Liste der Eigenschaften als assoziatives Array @@ -627,8 +693,8 @@ SQL /** * Setzt ein neues Kennwort fuer diesen Benutzer. * - * @param password new password - * @param forever int true, wenn Kennwort dauerhaft. + * @param $password string new password + * @param $forever int true, wenn Kennwort dauerhaft. */ public function setPassword($password, $forever = true ) { @@ -655,7 +721,11 @@ SQL $sql->setString('password',Password::hash(User::pepperPassword($password),$algo) ); $sql->setInt ('userid' ,$this->userid ); - $sql->query(); + $sql->query(); // Updating the password + + // Delete all login tokens, because the user should + // use the new password on all devices + $this->deleteAllLoginTokens(); } @@ -668,7 +738,7 @@ SQL { if ( !is_array($this->groups) ) { - $db = \cms\base\DB::get(); + $db = Db::get(); $sql = $db->sql( 'SELECT {{group}}.id,{{group}}.name FROM {{group}} '. 'LEFT JOIN {{usergroup}} ON {{usergroup}}.groupid={{group}}.id '. @@ -701,7 +771,7 @@ SQL // Gruppen ermitteln, in denen der Benutzer *nicht* Mitglied ist function getOtherGroups() { - $db = \cms\base\DB::get(); + $db = Db::get(); $sql = $db->sql( 'SELECT {{group}}.id,{{group}}.name FROM {{group}}'. ' LEFT JOIN {{usergroup}} ON {{usergroup}}.groupid={{group}}.id AND {{usergroup}}.userid={userid}'. @@ -716,11 +786,11 @@ SQL /** * Benutzer zu einer Gruppe hinzufuegen. * - * @param groupid die Gruppen-Id + * @param $groupid int die Gruppen-Id */ function addGroup( $groupid ) { - $db = \cms\base\DB::get(); + $db = Db::get(); $sql = $db->sql('SELECT MAX(id) FROM {{usergroup}}'); $usergroupid = intval($sql->getOne())+1; @@ -741,11 +811,11 @@ SQL /** * Benutzer aus Gruppe entfernen. * - * @param groupid die Gruppen-Id + * @param $groupid int die Gruppen-Id */ function delGroup( $groupid ) { - $db = \cms\base\DB::get(); + $db = Db::get(); $sql = $db->sql( 'DELETE FROM {{usergroup}} '. ' WHERE userid={userid} AND groupid={groupid}' ); @@ -757,17 +827,6 @@ SQL /** - * Ermitteln aller Rechte des Benutzers im aktuellen Projekt. - * - * @param Integer $projectid Projekt-Id - * @param Integer $languageid Sprache-Id - */ - function loadRights( $projectid,$languageid ) - { - } - - - /** * Ermitteln aller Berechtigungen des Benutzers.<br> * Diese Daten werden auf der Benutzerseite in der Administration angezeigt. * @@ -798,7 +857,7 @@ SQL $acl->setDatabaseRow( $row ); $acl->projectid = $row['projectid' ]; if ( intval($acl->languageid) == 0 ) - $acl->languagename = \cms\base\Language::lang('ALL_LANGUAGES'); + $acl->languagename = Language::lang( Messages::ALL_LANGUAGES); else $acl->languagename = $row['languagename']; $aclList[] = $acl; @@ -882,8 +941,8 @@ SQL /** * Ueberpruft, ob der Benutzer ein bestimmtes Recht hat * - * @param $objectid Objekt-Id zu dem Objekt, dessen Rechte untersucht werden sollen - * @param $type Typ des Rechts (Lesen,Schreiben,...) als Konstante Acl::ACL_* + * @param $objectid int Objekt-Id zu dem Objekt, dessen Rechte untersucht werden sollen + * @param $type int Typ des Rechts (Lesen,Schreiben,...) als Konstante Acl::ACL_* */ public function hasRight( $objectid,$type ) { @@ -903,8 +962,8 @@ SQL /** * Berechtigung dem Benutzer hinzufuegen. * - * @param objectid Objekt-Id, zu dem eine Berechtigung hinzugefuegt werden soll - * @param Art des Rechtes, welches hinzugefuegt werden soll + * @param $objectid int Objekt-Id, zu dem eine Berechtigung hinzugefuegt werden soll + * @param $type int Art des Rechtes, welches hinzugefuegt werden soll */ function addRight( $objectid,$type ) { @@ -949,7 +1008,7 @@ SQL */ function checkPassword( $password ) { - $db = \cms\base\DB::get(); + $db = Db::get(); // Laden des Benutzers aus der Datenbank, um Password-Hash zu ermitteln. $sql = $db->sql( 'SELECT * FROM {{user}}'. ' WHERE id={userid}' ); @@ -970,7 +1029,7 @@ SQL */ public function createPassword() { - $passwordConfig = \cms\base\Configuration::subset('security')->subset('password'); + $passwordConfig = Configuration::subset('security')->subset('password'); $pw = ''; $c = 'bcdfghjklmnprstvwz'; // consonants except hard to speak ones @@ -990,17 +1049,17 @@ SQL /** - * Das Kennwort "pfeffern". + * Pepper the password. * * Siehe http://de.wikipedia.org/wiki/Salt_%28Kryptologie%29#Pfeffer * für weitere Informationen. * - * @param Kennwort - * @return Das gepfefferte Kennwort + * @param $pass string password + * @return string peppered password */ public static function pepperPassword( $pass ) { - $salt = \cms\base\Configuration::Conf()->subset('security')->subset('password')->get('pepper'); + $salt = Configuration::Conf()->subset('security')->subset('password')->get('pepper'); return $salt.$pass; } @@ -1013,7 +1072,7 @@ SQL */ public function getLastChanges() { - $db = \cms\base\DB::get(); + $db = Db::get(); $sql = $db->sql( <<<SQL SELECT {{object}}.id as objectid, @@ -1075,9 +1134,7 @@ SQL $secret = Password::randomHexString(64); - $db = \cms\base\DB::get(); - - $stmt = $db->sql('UPDATE {{user}} SET otp_secret={secret} WHERE id={id}'); + $stmt = DB::sql('UPDATE {{user}} SET otp_secret={secret} WHERE id={id}'); $stmt->setString( 'secret', $secret ); $stmt->setInt ( 'id' , $this->userid ); diff --git a/modules/cms/ui/UI.class.php b/modules/cms/ui/UI.class.php @@ -35,7 +35,7 @@ class UI try { - define('COOKIE_PATH',dirname($_SERVER['SCRIPT_NAME'])); + define('COOKIE_PATH',dirname($_SERVER['SCRIPT_NAME']).'/'); // Everything is UTF-8. header('Content-Type: text/html; charset=UTF-8'); diff --git a/modules/cms/ui/themes/default/html/views/user/advanced.php b/modules/cms/ui/themes/default/html/views/user/advanced.php @@ -0,0 +1,51 @@ +<?php /* THIS FILE IS GENERATED from advanced.tpl.src.xml - DO NOT CHANGE */ defined('APP_STARTED') || die('Forbidden'); use \template_engine\Output as O; ?> + <section class="<?php echo O::escapeHtml('or-group or-collapsible or-collapsible--is-open or-collapsible--show') ?>"><?php echo O::escapeHtml('') ?> + <h2 class="<?php echo O::escapeHtml('or-collapsible-title or-group-title or-collapsible-act-switch') ?>"><?php echo O::escapeHtml('') ?> + <i class="<?php echo O::escapeHtml('or-image-icon or-image-icon--node-closed or-collapsible--on-closed') ?>"><?php echo O::escapeHtml('') ?></i> + <i class="<?php echo O::escapeHtml('or-image-icon or-image-icon--node-open or-collapsible--on-open') ?>"><?php echo O::escapeHtml('') ?></i> + <span><?php echo O::escapeHtml(''.@O::lang('token').'') ?></span> + </h2> + <div class="<?php echo O::escapeHtml('or-collapsible-value or-group-value') ?>"><?php echo O::escapeHtml('') ?> + <div class="<?php echo O::escapeHtml('or-table-wrapper') ?>"><?php echo O::escapeHtml('') ?> + <div class="<?php echo O::escapeHtml('or-table-filter') ?>"><?php echo O::escapeHtml('') ?> + <input type="<?php echo O::escapeHtml('search') ?>" name="<?php echo O::escapeHtml('filter') ?>" placeholder="<?php echo O::escapeHtml(''.@O::lang('SEARCH_FILTER').'') ?>" class="<?php echo O::escapeHtml('or-input or-table-filter-input') ?>" /><?php echo O::escapeHtml('') ?> + </div> + <div class="<?php echo O::escapeHtml('or-table-area') ?>"><?php echo O::escapeHtml('') ?> + <table width="<?php echo O::escapeHtml('100%') ?>" class="<?php echo O::escapeHtml('or-table') ?>"><?php echo O::escapeHtml('') ?> + <tr><?php echo O::escapeHtml('') ?> + <th><?php echo O::escapeHtml('') ?> + <span><?php echo O::escapeHtml(''.@O::lang('created').'') ?></span> + </th> + <th><?php echo O::escapeHtml('') ?> + <span><?php echo O::escapeHtml(''.@O::lang('until').'') ?></span> + </th> + <th><?php echo O::escapeHtml('') ?> + <span><?php echo O::escapeHtml(''.@O::lang('name').'') ?></span> + </th> + <th><?php echo O::escapeHtml('') ?> + <span><?php echo O::escapeHtml(''.@O::lang('OPERATING_SYSTEM').'') ?></span> + </th> + </tr> + <?php foreach((array)$token as $list_key=>$list_value) { extract($list_value); ?> + <tr><?php echo O::escapeHtml('') ?> + <td><?php echo O::escapeHtml('') ?> + <?php include_once( 'modules/template_engine/components/html/component_date/component-date.php'); { component_date($create_date); ?> + <?php } ?> + </td> + <td><?php echo O::escapeHtml('') ?> + <?php include_once( 'modules/template_engine/components/html/component_date/component-date.php'); { component_date($expires); ?> + <?php } ?> + </td> + <td><?php echo O::escapeHtml('') ?> + <span><?php echo O::escapeHtml(''.@$name.'') ?></span> + </td> + <td><?php echo O::escapeHtml('') ?> + <span><?php echo O::escapeHtml(''.@$platform.'') ?></span> + </td> + </tr> + <?php } ?> + </table> + </div> + </div> + </div> + </section>+ \ No newline at end of file diff --git a/modules/cms/ui/themes/default/html/views/user/advanced.tpl.src.xml b/modules/cms/ui/themes/default/html/views/user/advanced.tpl.src.xml @@ -0,0 +1,38 @@ +<output xmlns="http://www.openrat.de/template" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.openrat.de/template ../../../../../../../template_engine/components/template.xsd"> + <group title="${message:token}"> + <table> + <row> + <column header="true"> + <text value="${message:created}"/> + </column> + <column header="true"> + <text value="${message:until}"/> + </column> + <column header="true"> + <text value="${message:name}"/> + </column> + <column header="true"> + <text value="${message:OPERATING_SYSTEM}"/> + </column> + </row> + <list list="${token}" extract="true"> + <row> + <column> + <date date="${create_date}"/> + </column> + <column> + <date date="${expires}"/> + </column> + <column> + <text value="${name}"/> + </column> + <column> + <text value="${platform}"/> + </column> + </row> + </list> + </table> + </group> + +</output> diff --git a/modules/database/Database.class.php b/modules/database/Database.class.php @@ -175,7 +175,6 @@ class Database /** * Startet eine Transaktion. - * Falls der Schalter 'transaction' nicht gesetzt ist, passiert nichts. */ public function start() { diff --git a/modules/template_engine/components/html/component_date/component-date.php b/modules/template_engine/components/html/component_date/component-date.php @@ -2,6 +2,7 @@ use cms\action\Action; use language\Messages; +use util\Cookie; use template_engine\Output; function component_date($time ) @@ -11,10 +12,10 @@ function component_date($time ) else { // Benutzereinstellung 'Zeitzonen-Offset' auswerten. - if ( isset($_COOKIE[Action::COOKIE_TIMEZONE_OFFSET]) ) + if ( Cookie::has(Action::COOKIE_TIMEZONE_OFFSET) ) { $time -= (int)date('Z'); - $time += ((int)$_COOKIE[Action::COOKIE_TIMEZONE_OFFSET]*60); + $time += ( ((int)Cookie::get(Action::COOKIE_TIMEZONE_OFFSET))*60); } echo '<span class="or-table-sort-value">'.str_pad($time, 20, "0", STR_PAD_LEFT).'</span>'; // For sorting a table. @@ -28,7 +29,7 @@ function component_date($time ) unset($dl); - $past = time()-$time; + $past = abs(time()-$time ); $units = [ [ 60, Messages::SECOND, Messages::SECONDS ], diff --git a/modules/util/Cookie.class.php b/modules/util/Cookie.class.php @@ -0,0 +1,67 @@ +<?php + + +namespace util; + + +use cms\base\Configuration; +use cms\base\Startup; + +class Cookie +{ + /** + * Gets a cookie + * @param $name string key + * @param $default string default value + * @return string + */ + public static function get( $name,$default=null ) { + $value = @$_COOKIE[ $name ]; + if ( !$value ) + return $default; + return $value; + } + + + /** + * is a cookie set? + * @param $name string key + * @return boolean + */ + public static function has( $name ) { + return isset( $_COOKIE[ $name ] ); + } + + + /** + * Sets a cookie. + * + * @param $name string cookie name + * @param $value string cookie value, null or empty to delete + */ + public static function set($name, $value = '' ) { + + $cookieConfig = Configuration::subset('security')->subset('cookie'); + + if ( ! $value ) + $expire = Startup::getStartTime(); // Cookie wird gelöscht. + else + $expire = Startup::getStartTime() + 60 * 60 * 24 * $cookieConfig->get('expire',2*365); // default: 2 years + + $cookieAttributes = [ + rawurlencode($name).'='.rawurlencode($value), + 'Expires='.date('r',$expire), + 'Path='.COOKIE_PATH + ]; + + if ( $cookieConfig->is('secure',false ) ) + $cookieAttributes[] = 'Secure'; + + if ( $cookieConfig->is('httponly',true ) ) + $cookieAttributes[] = 'HttpOnly'; + + $cookieAttributes[] = 'SameSite='.$cookieConfig->get('samesite','Lax'); + + header('Set-Cookie: '.implode('; ',$cookieAttributes),false ); + } +}+ \ No newline at end of file