diff options
| author | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
|---|---|---|
| committer | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
| commit | 4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch) | |
| tree | 5c07151ae61276d334e88f6309c30d439a85c12e /lib/classes/auth_plugins/StudipAuthAbstract.php | |
| parent | da0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff) | |
| parent | 97a188592c679890a25c37ab78463add76a52ff7 (diff) | |
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/classes/auth_plugins/StudipAuthAbstract.php')
| -rw-r--r-- | lib/classes/auth_plugins/StudipAuthAbstract.php | 578 |
1 files changed, 578 insertions, 0 deletions
diff --git a/lib/classes/auth_plugins/StudipAuthAbstract.php b/lib/classes/auth_plugins/StudipAuthAbstract.php new file mode 100644 index 0000000..19d5afa --- /dev/null +++ b/lib/classes/auth_plugins/StudipAuthAbstract.php @@ -0,0 +1,578 @@ +<?php +// +---------------------------------------------------------------------------+ +// This file is part of Stud.IP +// StudipAuthAbstract.php +// Abstract class, used as a template for authentication plugins +// +// Copyright (c) 2003 André Noack <noack@data-quest.de> +// Suchi & Berg GmbH <info@data-quest.de> +// +---------------------------------------------------------------------------+ +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or any later version. +// +---------------------------------------------------------------------------+ +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +---------------------------------------------------------------------------+ + +/** + * abstract base class for authentication plugins + * + * abstract base class for authentication plugins + * to write your own authentication plugin, derive it from this class and + * implement the following abstract methods: isUsedUsername($username) and + * isAuthenticated($username, $password, $jscript) + * don't forget to call the parents constructor if you implement your own, php + * won't do that for you ! + * + * @abstract + * @author André Noack <noack@data-quest.de> + * @package + */ +class StudipAuthAbstract +{ + + /** + * contains error message, if authentication fails + * + * + * @var string $error_msg + */ + public $error_msg; + + /** + * indicates whether the authenticated user logs in for the first time + * + * + * @var bool $is_new_user + */ + public $is_new_user = false; + + /** + * array of user domains to assign to each user, can be set in local.inc + * + * @access public + * @var array $user_domains + */ + public $user_domains; + + /** + * associative array with mapping for database fields + * + * associative array with mapping for database fields, + * should be set in local.inc + * structure : + * array('<table name>.<field name>' => array( 'callback' => '<name of callback method used for data retrieval>', + * 'map_args' => '<arguments passed to callback method>')) + * @var array $user_data_mapping + */ + public $user_data_mapping = null; + + /** + * name of the plugin + * + * name of the plugin (last part of class name) is set in the constructor + * @var string $plugin_name + */ + public $plugin_name; + + /** + * text, which precedes error message for the plugin + * + * + * @var string $error_head + */ + public $error_head; + + /** + * toggles display of standard login + * + * + * @var bool $show_login + */ + public $show_login; + + /** + * @var $plugin_instances + */ + private static $plugin_instances; + + private $config_data = []; + + /** + * static method to instantiate and retrieve a reference to an object (singleton) + * + * always use this method to instantiate a plugin object, it will ensure that only one object of each + * plugin will exist + * @param string $plugin_name name of plugin, if omitted an array with all plugin objects will be returned + * @return mixed either a reference to the plugin with the passed name, or an array with references to all plugins + */ + public static function getInstance($plugin_name = false) + { + if (!is_array(self::$plugin_instances)) { + foreach ($GLOBALS['STUDIP_AUTH_PLUGIN'] as $plugin) { + $config = $GLOBALS['STUDIP_AUTH_CONFIG_' . strtoupper($plugin)]; + $plugin_class = $config['plugin_class'] ?? 'StudipAuth' . $plugin; + if (empty($config['plugin_name'])) { + $config['plugin_name'] = strtolower($plugin); + } + self::$plugin_instances[strtoupper($plugin)] = new $plugin_class($config); + } + } + return ($plugin_name) ? self::$plugin_instances[strtoupper($plugin_name)]??null : self::$plugin_instances; + } + + /** + * static method to check if SSO login is enabled + * + * @return bool + */ + public static function isSSOEnabled(): bool + { + self::getInstance(); + foreach (self::$plugin_instances as $auth_plugin) { + if ($auth_plugin instanceof StudipAuthSSO) { + return true; + } + } + return false; + } + + /** + * static method to check if standard login is enabled + * + * @return bool + */ + public static function isLoginEnabled(): bool + { + self::getInstance(); + foreach (self::$plugin_instances as $auth_plugin) { + if ($auth_plugin->show_login === true) { + return true; + } + } + return false; + } + + /** + * static method to check authentication in all plugins + * + * if authentication fails in one plugin, the error message is stored and the next plugin is used + * if authentication succeeds, the uid element in the returned array will contain the Stud.IP user id + * + * @param string $username the username to check + * @param string $password the password to check + * @return array structure: array('uid'=>'string <Stud.IP user id>','error'=>'string <error message>','is_new_user'=>'bool') + */ + public static function CheckAuthentication($username, $password) + { + + $plugins = StudipAuthAbstract::GetInstance(); + $error = false; + $uid = false; + foreach ($plugins as $object) { + // SSO plugins can't be used + if ($object instanceof StudipAuthSSO) { + continue; + } + if ($user = $object->authenticateUser($username, $password)) { + if ($user) { + $uid = $user->id; + $locked = $user['locked']; + $key = $user['validation_key']; + $checkIPRange = ($GLOBALS['ENABLE_ADMIN_IP_CHECK'] && $user['perms'] === 'admin') + || ($GLOBALS['ENABLE_ROOT_IP_CHECK'] && $user['perms'] === 'root'); + + if ($user->isExpired()) { + $error .= _('Dieses Benutzerkonto ist abgelaufen.<br> Wenden Sie sich bitte an die Administration.') . '<BR>'; + return ['uid' => false, 'error' => $error]; + } else if ($locked) { + $error .= _('Dieser Benutzer ist gesperrt! Wenden Sie sich bitte an die Administration.') . '<BR>'; + return ['uid' => false, 'error' => $error]; + } else if ($key != '') { + return ['uid' => $uid, 'user' => $user, 'error' => $error, 'need_email_activation' => $uid]; + } else if ($checkIPRange && !self::CheckIPRange()) { + $error .= _('Der Login in Ihren Account ist aus diesem Netzwerk nicht erlaubt.') . '<BR>'; + return ['uid' => false, 'error' => $error]; + } + } + return ['uid' => $uid, 'user' => $user, 'error' => $error, 'is_new_user' => $object->is_new_user]; + } else { + $error .= (($object->error_head) ? ('<b>' . $object->error_head . ':</b> ') : '') . $object->error_msg . '<br>'; + } + } + return ['uid' => $uid, 'error' => $error]; + } + + /** + * static method to check if passed username is used in external data sources + * + * all plugins are checked, the error messages are stored and returned + * + * @param string $username the username + * @return array + */ + public static function CheckUsername($username) + { + $plugins = StudipAuthAbstract::GetInstance(); + $error = false; + $found = false; + foreach ($plugins as $object) { + if ($found = $object->isUsedUsername($username)) { + return ['found' => $found, 'error' => $error]; + } else { + $error .= (($object->error_head) ? ('<b>' . $object->error_head . ':</b> ') : '') . $object->error_msg . '<br>'; + } + } + return ['found' => $found, 'error' => $error]; + } + + /** + * static method to check for a mapped field + * + * this method checks in the plugin with the passed name, if the passed + * Stud.IP DB field is mapped to an external data source + * + * @param string the name of the db field must be in form '<table name>.<field name>' + * @param string the name of the plugin to check + * @return bool true if the field is mapped, else false + */ + public static function CheckField($field_name, $plugin_name) + { + if (!$plugin_name) { + return false; + } + $plugin = StudipAuthAbstract::GetInstance($plugin_name); + return (is_object($plugin) ? $plugin->isMappedField($field_name) : false); + } + + /** + * static method to check if ip address belongs to allowed range + * + * @return bool true if the client ip address is within the valid range + */ + public static function CheckIPRange() + { + $ip = $_SERVER['REMOTE_ADDR']; + $version = substr_count($ip, ':') > 1 ? 'V6' : 'V4'; // valid ip v6 addresses have atleast two colons + $method = 'CheckIPRange' . $version; + if (is_array($GLOBALS['LOGIN_IP_RANGES'][$version])) { + foreach ($GLOBALS['LOGIN_IP_RANGES'][$version] as $range) { + if (self::$method($ip, $range)) { + return true; + } + } + } + return false; + } + + /** + * @param $ip string IPv4 adress + * @param $range array assoc array with [start] & [end] + * @return bool + */ + public static function CheckIPRangeV4($ip, $range) + { + $ipv4 = ip2long($ip); + if ($ipv4 === false) { + return false; // invalid ip address + } + + $start = ip2long($range['start']); + $end = ip2long($range['end']); + + return $ipv4 >= $start && $ipv4 <= $end; + } + + /** + * @param $ip string IPv6 address + * @param $range array assoc array with [start] & [end] + * @return bool + */ + public static function CheckIPRangeV6($ip, $range) + { + $ipv6 = inet_pton($ip); + if ($ipv6 === false) { + return false; // invalid ip address + } + + $start = inet_pton($range['start']); + $end = inet_pton($range['end']); + + return strlen($ipv6) === strlen($start) + && $ipv6 >= $start && $ipv6 <= $end; + } + + /** + * Constructor + * + * you should use StudipAuthAbstract::GetInstance($plugin_name) + * to get a reference to a plugin object. Make sure the constructor in the base class is called + * when deriving your own plugin class, it assigns the settings from local.inc as members of the plugin + * each key of the $STUDIP_AUTH_CONFIG_<plugin name> array will become a member of the object + * + * @param array $config + */ + public function __construct($config = []) + { + //get configuration array set in local inc + if (empty($config)) { + $this->plugin_name = strtolower(substr(get_class($this), 10)); + $config = $GLOBALS['STUDIP_AUTH_CONFIG_' . strtoupper($this->plugin_name)]; + } + //assign each key in the config array as a member of the plugin object + foreach ($config as $key => $value) { + $this->$key = $value; + } + } + + /** + * authentication method + * + * this method authenticates the passed username, it is used by StudipAuthAbstract::CheckAuthentication() + * if authentication succeeds it calls StudipAuthAbstract::doDataMapping() to map data fields + * if the authenticated user logs in for the first time it calls StudipAuthAbstract::doNewUserInit() to + * initialize the new user + * @param string $username the username to check + * @param string $password the password to check + * @return string if authentication succeeds the Stud.IP user , else false + */ + public function authenticateUser($username, $password) + { + $username = $this->verifyUsername($username); + if ($this->isAuthenticated($username, $password)) { + if ($user = $this->getStudipUser($username)) { + $this->doDataMapping($user); + if ($this->is_new_user) { + $this->doNewUserInit($user); + } + $this->setUserDomains($user); + } + return $user; + } else { + return false; + } + } + + /** + * method to retrieve the Stud.IP user id to a given username + * + * + * @access private + * @param string the username + * @return User the Stud.IP or false if an error occurs + */ + function getStudipUser($username) + { + $user = User::findByUsername($username); + if ($user) { + $auth_plugin = $user->auth_plugin; + if ($auth_plugin === null) { + $this->error_msg = _('Dies ist ein vorläufiger Benutzer.') . '<br>'; + return false; + } + if ($auth_plugin != $this->plugin_name) { + $this->error_msg = sprintf(_('Dieser Benutzername wird bereits über %s authentifiziert!'), $auth_plugin) . '<br>'; + return false; + } + return $user; + } + $new_user = new User(); + $new_user->username = $username; + $new_user->perms = 'autor'; + $new_user->auth_plugin = $this->plugin_name; + $new_user->preferred_language = $_SESSION['_language']; + if ($new_user->store()) { + $this->is_new_user = true; + return $new_user; + } + } + + /** + * initialize a new user + * + * this method is invoked for one time, if a new user logs in ($this->is_new_user is true) + * place special treatment of new users here + * + * @access private + * @param User $user the user object + */ + function doNewUserInit($user) + { + // auto insertion of new users, according to $AUTO_INSERT_SEM[] (defined in local.inc) + AutoInsert::instance()->saveUser($user->id, $user->perms); + } + + /** + * This method sets the user domains for the current user. + * + * @access private + * @param User the user object + */ + function setUserDomains($user) + { + $user_domains = $this->getUserDomains(); + $uid = $user->id; + if (isset($user_domains)) { + $old_domains = UserDomain::getUserDomainsForUser($uid); + + foreach ($old_domains as $domain) { + if (!in_array($domain->id, $user_domains)) { + $domain->removeUser($uid); + } + } + + foreach ($user_domains as $user_domain) { + $domain = new UserDomain($user_domain); + + if ($domain->isNew()) { + $domain->name = $user_domain; + $domain->store(); + } + + if (!in_array($domain, $old_domains)) { + $domain->addUser($uid); + } + } + } + } + + /** + * Get the user domains to assign to the current user. + */ + function getUserDomains() + { + return $this->user_domains; + } + + /** + * this method handles the data mapping + * + * for each entry in $this->user_data_mapping the according callback will be invoked + * the return value of the callback method is then written to the db field, which is specified + * in the key of the array + * + * @access private + * @param User the user object + * @return bool + */ + function doDataMapping($user) + { + if ($user && is_array($this->user_data_mapping)) { + foreach ($this->user_data_mapping as $key => $value) { + $callback = null; + if (method_exists($this, $value['callback'])) { + $callback = [$this, $value['callback']]; + } else if (is_callable($value['callback'])) { + $callback = $value['callback']; + } + if ($callback) { + $split = explode('.', $key); + $table = $split[0]; + $field = $split[1]; + if ($table === 'auth_user_md5' || $table === 'user_info') { + $mapped_value = call_user_func($callback, $value['map_args']); + if (isset($mapped_value)) { + $user->setValue($field, $mapped_value); + } + } else { + call_user_func($callback, [$table, $field, $user, $value['map_args']]); + } + } + } + return $user->store(); + } + return false; + } + + /** + * method to check, if a given db field is mapped by the plugin + * + * + * @access private + * @param string the name of the db field (<table_name>.<field_name>) + * @return bool true if the field is mapped + */ + function isMappedField($name) + { + return isset($this->user_data_mapping[$name]); + } + + /** + * method to eliminate bad characters in the given username + * + * + * @access private + * @param string the username + * @return string the username + */ + function verifyUsername($username) + { + if ($this->username_case_insensitiv) { + $username = mb_strtolower($username); + } + if ($this->bad_char_regex) { + return preg_replace($this->bad_char_regex, '', $username); + } else { + return trim($username); + } + } + + /** + * method to check, if username is used + * + * abstract MUST be realized + * + * @access private + * @param string the username + * @return bool true if the username exists + */ + function isUsedUsername($username) + { + $this->error_msg = sprintf(_('Methode %s nicht implementiert!'), get_class($this) . '::isUsedUsername()'); + return false; + } + + /** + * method to check the authentication of a given username and a given password + * + * abstract, MUST be realized + * + * @access private + * @param string the username + * @param string the password + * @return bool true if authentication succeeds + */ + function isAuthenticated($username, $password) + { + $this->error = sprintf(_('Methode %s nicht implementiert!'), get_class($this) . '::isAuthenticated()'); + return false; + } + + // Store dynamically set dynamically created properties in $config_data + public function __isset($offset) + { + return isset($this->config_data[$offset]); + } + + public function __set($offset, $value) + { + $this->config_data[$offset] = $value; + } + + public function __get($offset) + { + return $this->config_data[$offset] ?? null; + } + + public function __unset($offset) + { + unset($this->config_data[$offset]); + } +} |
